Files
leetcode-go/技巧/96-只出现一次的数字.md
T

168 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [技巧, 位运算, 异或, 数组]
create time: 2026-05-16 14:30
---
# 96-只出现一次的数字
## 题面
给你一个**非空**整数数组 `nums` ,除了某个元素**只出现一次**以外,其余每个元素均出现**两次**。找出那个只出现了一次的元素。
**要求:**
- 线性时间复杂度 $O(n)$
- 常量额外空间 $O(1)$
**示例 1:**
> 输入:`nums = [2, 2, 1]` → 输出:`1`
**示例 2:**
> 输入:`nums = [4, 1, 2, 1, 2]` → 输出:`4`
**示例 3:**
> 输入:`nums = [1]` → 输出:`1`
---
## 思路
### 先思考一个问题 🤔
如果不用任何算法,你会怎么找到那个唯一的数字?
> [!tip] 直觉方案
> 遍历数组,对每个数字统计出现次数,然后找出出现一次的——但这需要哈希表或排序,不满足 $O(1)$ 空间的要求。
### 核心洞察:异或运算的性质 💡
回忆一下**异或(XOR)**的三条基本性质:
| 性质 | 表达式 | 含义 |
|------|--------|------|
| **归零律** | `a ^ a = 0` | 相同数字异或为 0 |
| **恒等律** | `a ^ 0 = a` | 任何数与 0 异或等于自身 |
| **交换律 & 结合律** | `a ^ b ^ c = a ^ (b ^ c) = (a ^ c) ^ b` | 异或的顺序不影响结果 |
> [!question] 启发式提问
> 如果一个数组中除了一个数字外,其余都恰好出现两次,把这些数字全部异或起来会发生什么?
根据归零律,每对相同的数字异或后变为 `0`;再根据恒等律,所有的 `0` 异或起来还是 `0`;最后剩下的就是那个只出现一次的数字!
### 流程演示
以 `nums = [4, 1, 2, 1, 2]` 为例:
```mermaid
flowchart LR
A["开始: result = 0"] --> B["result ^= 4 → 4"]
B --> C["result ^= 1 → 5"]
C --> D["result ^= 2 → 7"]
D --> E["result ^= 1 → 6"]
E --> F["result ^= 2 → 4"]
F --> G["result = 4 ✅"]
style A fill:#e1f5fe
style G fill:#c8e6c9
```
**更直观的理解**(利用交换律和结合律重新排列):
$$4 \oplus 1 \oplus 2 \oplus 1 \oplus 2 = 4 \oplus (1 \oplus 1) \oplus (2 \oplus 2) = 4 \oplus 0 \oplus 0 = 4$$
> [!info] 关键结论
> 不需要关心顺序——异或的**交换律**保证了无论成对的数字在数组中的位置如何,它们最终一定会被配对抵消。
---
## 代码提示
### 伪代码
```
初始化 result = 0
遍历数组中的每个数字 num:
result = result XOR num
返回 result
```
### 复杂度分析
| 指标 | 结果 | 说明 |
|------|------|------|
| **时间复杂度** | $O(n)$ | 只需遍历数组一次 |
| **空间复杂度** | $O(1)$ | 只使用了一个变量 `result` |
完美满足题目要求!
---
## 技巧
### 1. 异或找唯一元素的通用模式
> [!summary] 模板记忆法
> ```go
> ans := 0
> for _, v := range nums {
> ans ^= v
> }
> return ans
> ```
> 看到「除一个元素出现一次/奇数次外,其余出现偶数次」→ 优先考虑异或。
### 2. 异或的小应用清单
这些面试高频技巧都源于异或的相同两条性质:
| 应用场景 | 方法 | 核心原理 |
|----------|------|---------|
| 找唯一元素 | 全数组异或 | `a ^ a = 0`, `a ^ 0 = a` |
| 判断两个数是否相等 | `a ^ b == 0` | 相等则结果为 0 |
| 交换两个变量(经典面试题) | `a ^= b; b ^= a; a ^= b` | 三步异或完成交换,无需临时变量 |
| 翻转指定位 | `num ^= mask` | mask 中为 1 的位被翻转 |
### 3. 变体题延伸 🚀
| 题目 | 变化点 | 思路升级 |
|------|--------|---------|
| **136. Single Number**(本题) | 其余出现两次 | 直接全异或 ✅ |
| **137. Single Number II** | 其余出现三次 | 按位统计每个位上 1 的出现次数,%3 剩余的拼接 |
| **260. Single Number III** | 有两个只出现一次的数 | 先全异或得到 `x ^ y`,找最低位的 1 分组,再分别异或 |
> [!note] 思考方向
> 当「其余元素出现 k 次」时,本质上是把逐位计数的模运算从 %2 推广到 %k。异或是最自然的 %2 计数器。
---
## 代码
```go
// SingleNumber finds the element that appears exactly once
// while all other elements appear exactly twice.
func singleNumber(nums []int) int {
// result 初始化为 0(异或的单位元)
// 根据恒等律:a ^ 0 = a
result := 0
// 遍历所有数字,依次异或
// 根据归零律:a ^ a = 0,成对的数字会相互抵消
// 根据交换律:无论顺序如何,结果不变
for _, num := range nums {
result ^= num
}
// 此时 result 就是那个只出现一次的数字
return result
}
```
### Go 运行细节
> [!example] Go 中异或运算符
> Go 语言使用 `^` 作为按位异或运算符:
> - `5 ^ 3 = 0b0101 ^ 0b0011 = 0b0110 = 6`
> - 注意区分逻辑异或 `!=` —— 这里用的是**按位**异或