一、题目描述
给你一个 非空 整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现一次的元素。
要求:线性时间复杂度,常量额外空间。
示例 1:
输入: nums = [2,2,1]
输出: 1
示例 2:
输入: nums = [4,1,2,1,2]
输出: 4
示例 3:
输入: nums = [1]
输出: 1
二、解题思路总览
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 异或运算 | O(n) | O(1) | 利用相同数字异或为0的特性 |
| 哈希表 | O(n) | O(n) | 统计每个数字出现次数 |
| 排序后遍历 | O(n log n) | O(1) | 排序后相邻比较 |
异或运算是最优解,同时满足 O(n) 时间和 O(1) 空间。
三、方法一:异或运算(推荐)
3.1 核心思想
利用异或运算的两个性质:
- 相同数字异或结果为 0 :
a ^ a = 0 - 任何数字与 0 异或结果为 本身 :
a ^ 0 = a - 满足交换律和结合律 :
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b
所有数字异或在一起,重复出现的数字互相抵消,最后剩下的就是只出现一次的数字。
3.2 算法流程图
数组: [4, 1, 2, 1, 2]
异或运算过程:
初始: ans = 0
+------------------------------------------------------------+
| 第1步: ans = 0 ^ 4 = 4 |
| ans = 4 |
+------------------------------------------------------------+
+------------------------------------------------------------+
| 第2步: ans = 4 ^ 1 = 5 |
| ans = 5 |
+------------------------------------------------------------+
+------------------------------------------------------------+
| 第3步: ans = 5 ^ 2 = 7 |
| ans = 7 |
+------------------------------------------------------------+
+------------------------------------------------------------+
| 第4步: ans = 7 ^ 1 = 6 |
| ans = 6 |
| (注意:这里 7 ^ 1,7 的二进制包含第一个 1 的信息) |
+------------------------------------------------------------+
+------------------------------------------------------------+
| 第5步: ans = 6 ^ 2 = 4 |
| ans = 4 |
| (重复的 1 和 2 被抵消,只剩下只出现一次的 4) |
+------------------------------------------------------------+
结果: ans = 4
异或抵消的直观理解:
原始数组: [4, 1, 2, 1, 2]
按顺序异或:
0 ^ 4 = 4
4 ^ 1 = 5 (4 和 1 合并结果)
5 ^ 2 = 7 (加入了 2)
7 ^ 1 = 6 (1 和前面的 1 抵消)
6 ^ 2 = 4 (2 和前面的 2 抵消)
最终 = 4
3.3 完整代码
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for (int num : nums) {
ans ^= num;
}
return ans;
}
};
3.4 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 遍历数组一次 |
| 空间 | O(1) | 只用一个变量 |
四、方法二:哈希表
4.1 核心思想
遍历数组,将每个数字的出现次数存入哈希表,最后找到计数为 1 的数字。
4.2 算法流程图
数组: [4, 1, 2, 1, 2]
遍历并统计:
+------------------------------------------------------------+
| 遍历数组,插入哈希表 |
+------------------------------------------------------------+
插入 4: map = {4: 1}
插入 1: map = {4: 1, 1: 1}
插入 2: map = {4: 1, 1: 1, 2: 1}
插入 1: map = {4: 1, 1: 2, 2: 1} (1 已存在,计数+1)
插入 2: map = {4: 1, 1: 2, 2: 2} (2 已存在,计数+1)
+------------------------------------------------------------+
| 查找计数为 1 的元素 |
+------------------------------------------------------------+
map[4] = 1 -> 答案 = 4
结果: 4
4.3 完整代码
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> freq;
for (int num : nums) {
freq[num]++;
}
for (auto& [num, count] : freq) {
if (count == 1) {
return num;
}
}
return 0; // 不会走到这里
}
};
4.4 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 遍历数组两次 |
| 空间 | O(n) | 哈希表存储所有数字 |
五、方法三:排序后遍历
5.1 核心思想
将数组排序后,相同的数字会相邻排列。遍历数组,每两个一组比较,如果两个数字不相等,则第一个就是只出现一次的元素。
5.2 算法流程图
数组: [4, 1, 2, 1, 2] -> 排序后: [1, 1, 2, 2, 4]
+------------------------------------------------------------+
| 从左到右,每两个一组检查 |
+------------------------------------------------------------+
索引 0-1: nums[0]=1, nums[1]=1 -> 相等,跳过下一组
索引 2-3: nums[2]=2, nums[3]=2 -> 相等,跳过下一组
索引 4: 遍历结束,没找到相等的 -> nums[4]=4 是答案
结果: 4
5.3 完整代码
cpp
class Solution {
public:
int singleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 1; i += 2) {
if (nums[i] != nums[i + 1]) {
return nums[i];
}
}
return nums[nums.size() - 1];
}
};
5.4 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n log n) | 排序的时间复杂度 |
| 空间 | O(1) | 若原地排序 |
六、方法对比总结
| 方法 | 时间复杂度 | 空间复杂度 | 推荐指数 |
|---|---|---|---|
| 异或运算 | O(n) | O(1) | ★★★★★ |
| 哈希表 | O(n) | O(n) | ★★★☆☆ |
| 排序后遍历 | O(n log n) | O(1) | ★★☆☆☆ |
为什么异或是最优解?
1. 时间 O(n),满足要求
2. 空间 O(1),满足要求
3. 代码极简,一行搞定
4. 充分利用数学性质
异或的物理意义:
- 相同为 0,不同为 1
- 出现两次的数字,每一位都被抵消
- 只剩出现一次的那个数字
七、面试追问 FAQ
| 问题 | 回答 |
|---|---|
| Q: 为什么异或能消除重复出现的数字? | 因为 a ^ a = 0,且异或满足交换律结合律,所以 a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b |
| Q: 初始 ans 为什么设为 0? | 因为 a ^ 0 = a,任何数字与 0 异或不变,0 是异或的单位元 |
| Q: 如果有三个相同的数字呢? | 不能用简单异或,需要用其他方法(如哈希表或位运算统计每一位出现次数) |
| Q: 异或和加法减法有什么区别? | 异或不会进位,不会有进位溢出问题。对于只出现两次的场景,异或更直接 |
| Q: 负数能用异或吗? | 能,负数在计算机中以补码形式存储,异或操作对补码同样有效 |
| Q: 这种题还有变体吗? | 有,比如:只出现一次的其他数字(2个)、其他数字出现3次等,需要用位运算统计每一位 |
八、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 136. 只出现一次的数字 | 简单 | 异或运算 |
| 137. 只出现一次的数字 II | 中等 | 位运算统计每一位 |
| 260. 只出现一次的数字 III | 中等 | 分组异或 |
| 268. 丢失的数字 | 简单 | 异或 / 数学公式 |
| 287. 寻找重复数 | 中等 | 环形链表 / 二分 |
九、总结
| 要点 | 说明 |
|---|---|
| 核心思想 | 异或运算:相同为0,不同为1 |
| 关键性质 | a ^ a = 0,a ^ 0 = a,交换律/结合律 |
| 为什么有效 | 出现两次的数字互相抵消 |
| 时间复杂度 | O(n),遍历一次 |
| 空间复杂度 | O(1),只用一个变量 |
| 适用条件 | 只有一个数字出现一次,其余出现偶数次 |