LeetCode 136. 只出现一次的数字
题目:给定一个非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次,找出那个只出现一次的元素。
例如:
输入:nums = {4, 1, 2, 1, 2}
输出:4
这道题主要有两种解法:哈希表(unordered_map)和异或运算(XOR)。
一、方法一:哈希表(unordered_map)
1. 完整代码(精简注释)
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int,int> hash; // 数字 -> 出现次数
// 统计每个数字的出现次数
for(auto num : nums){
hash[num]++;
}
// 找到出现次数为1的数字
for(auto [num,count] : hash){
if(count == 1) return num;
}
return -1;
}
};
2. 核心思路
利用哈希表存储每个数字及其出现次数。
unordered_map<int,int> hash;
其中:
-
Key:数组中的数字。
-
Value:数字出现的次数。
例如:
nums = {4, 1, 2, 1, 2};
统计后:
| Key(数字) | Value(次数) |
|---|---|
| 4 | 1 |
| 1 | 2 |
| 2 | 2 |
遍历哈希表,找到 count == 1 的数字并返回。
3. 重点语法
① 统计出现次数
hash[num]++;
如果 num 不存在,哈希表会自动创建对应的键值对,将整数值初始化为 0,再执行加 1。
② 遍历键值对
for(auto [num,count] : hash)
其中:
-
num:键(Key)。 -
count:值(Value)。 -
[]:结构化绑定,用于拆分键值对。
也可以写成:
for(auto p : hash){
if(p.second == 1){
return p.first;
}
}
其中 p.first 表示 Key,p.second 表示 Value。
4. 复杂度分析
-
时间复杂度:平均 \(O(n)\),遍历数组统计次数,再遍历哈希表。
-
空间复杂度:\(O(n)\),需要额外的哈希表。
5. 方法总结
哈希表解法主要分为三步:
-
创建
unordered_map<int,int>哈希表。 -
遍历数组,利用
hash[num]++统计次数。 -
遍历哈希表,返回出现次数为 1 的数字。
核心:利用 Key 存储数字,Value 存储出现次数。
二、方法二:异或运算(XOR)
1. 完整代码(精简注释)
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0; // 初始化异或结果
// 将所有数字依次异或
for(auto num : nums){
ret ^= num;
}
return ret; // 返回只出现一次的数字
}
};
2. 核心思路
异或运算符为 ^。
异或规则:二进制位相同为 0,不同为 1。
| a | b | a ^ b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
异或具有三个重要性质:
① 相同数字异或为 0
\ a\\oplus a=0 \\
② 任何数字与 0 异或等于本身
\ a\\oplus 0=a \\
③ 异或满足交换律和结合律
\ a\\oplus b\\oplus a=(a\\oplus a)\\oplus b=b \\
因此,可以利用异或运算消除所有出现两次的数字,最终只剩下出现一次的数字。
3. 举例模拟
nums = {4, 1, 2, 1, 2};
初始化:
int ret = 0;
执行过程:
| 当前数字 num | 异或操作 | ret |
|---|---|---|
| 4 | 0 ^ 4 |
4 |
| 1 | 4 ^ 1 |
5 |
| 2 | 5 ^ 2 |
7 |
| 1 | 7 ^ 1 |
6 |
| 2 | 6 ^ 2 |
4 |
最终返回:
return ret; // 4
从数学上理解:
\ 4\\oplus1\\oplus2\\oplus1\\oplus2 \\
根据交换律和结合律:
\ =4\\oplus(1\\oplus1)\\oplus(2\\oplus2) \\
\ =4\\oplus0\\oplus0 \\
\ =4 \\
所有出现两次的数字都会抵消为 0,最终就是 0 与只出现一次的数字异或,结果为该数字本身。
4. 重点语法
ret ^= num;
等价于:
ret = ret ^ num;
其中:
-
^:按位异或运算。 -
^=:异或赋值运算。
注意 ^ 不是乘方运算。
5. 复杂度分析
-
时间复杂度:\(O(n)\),只遍历数组一次。
-
空间复杂度:\(O(1)\),只使用一个额外变量
ret。
6. 方法总结
异或解法主要分为三步:
-
初始化
ret = 0。 -
遍历数组,将每个数字与
ret异或。 -
所有成对出现的数字相互抵消,最终返回
ret。
核心:相同数字异或为 0,0 与任何数字异或等于数字本身。
三、两种方法对比
| 对比 | 哈希表 | 异或 |
|---|---|---|
| 核心思想 | 统计出现次数 | 相同数字相互抵消 |
| 数据结构 | unordered_map |
不需要 |
| 时间复杂度 | 平均 \(O(n)\) | \(O(n)\) |
| 空间复杂度 | \(O(n)\) | \(O(1)\) |
| 优点 | 容易理解,适用范围广 | 代码简洁,空间效率高 |
| 缺点 | 需要额外空间 | 依赖题目中特定的出现次数条件 |
四、最终记忆模板
哈希表:统计次数,找到出现一次的数字。
unordered_map<int,int> hash;
for(auto num : nums){
hash[num]++;
}
for(auto [num,count] : hash){
if(count == 1) return num;
}
异或:相同数字抵消,剩下唯一数字。
int ret = 0;
for(auto num : nums){
ret ^= num;
}
return ret;
最后总结:
哈希表和异或都能解决这道题,但题目要求线性时间复杂度和常数额外空间复杂度,因此异或才是本题最优解法。
以后遇到类似题目,可以这样判断:
-
需要统计数字出现次数:优先考虑
unordered_map。 -
需要判断数字是否出现过:优先考虑
unordered_set。 -
所有数字出现两次,只有一个数字出现一次:优先考虑异或运算。