Appearance
实战:只出现一次的数字
这是位运算最经典的入门题——用异或的性质,一行代码解决。
题目描述
LeetCode 136. 只出现一次的数字
给你一个非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法,且使用常数空间来解决此问题。
示例 1:
输入:nums = [2,2,1]
输出:1示例 2:
输入:nums = [4,1,2,1,2]
输出:4示例 3:
输入:nums = [1]
输出:1提示:
- 1 ≤ nums.length ≤ 3 × 10⁴
- -3 × 10⁴ ≤ nums[i] ≤ 3 × 10⁴
- 除了某个元素只出现一次外,其余每个元素均出现两次
题目分析
约束分析:
- 线性时间 O(n):不能排序(O(n log n))
- 常数空间 O(1):不能用哈希表(O(n))
普通方法行不通,需要用位运算。
异或运算回顾
异或(XOR)的三个关键性质:
| 性质 | 表达式 | 说明 |
|---|---|---|
| 自反性 | a ^ a = 0 | 相同的数异或为 0 |
| 零元素 | a ^ 0 = a | 任何数异或 0 是它本身 |
| 交换律/结合律 | a ^ b ^ a = b | 可以任意调换顺序 |
关键推论:一组数连续异或,成对的数会抵消,只留下出现奇数次的数。
a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b解法
把所有数异或在一起,成对出现的数会抵消,剩下的就是单独的数。
javascript
function singleNumber(nums) {
let result = 0;
for (const num of nums) {
result ^= num;
}
return result;
}一行代码版本(使用 reduce):
javascript
function singleNumber(nums) {
return nums.reduce((acc, num) => acc ^ num, 0);
}执行过程
以 nums = [4, 1, 2, 1, 2] 为例:
初始:result = 0
result ^= 4: 0 ^ 4 = 4 (二进制: 0000 ^ 0100 = 0100)
result ^= 1: 4 ^ 1 = 5 (二进制: 0100 ^ 0001 = 0101)
result ^= 2: 5 ^ 2 = 7 (二进制: 0101 ^ 0010 = 0111)
result ^= 1: 7 ^ 1 = 6 (二进制: 0111 ^ 0001 = 0110)
result ^= 2: 6 ^ 2 = 4 (二进制: 0110 ^ 0010 = 0100)
结果:4为什么有效?
根据异或的交换律和结合律,可以重新排列:
4 ^ 1 ^ 2 ^ 1 ^ 2
= 4 ^ (1 ^ 1) ^ (2 ^ 2) // 重新分组
= 4 ^ 0 ^ 0 // 相同数异或为 0
= 4 // 任何数异或 0 是它本身边界情况
javascript
// 只有一个元素
singleNumber([1]); // 1
// 负数
singleNumber([-1, 1, 1]); // -1
// 大数组
singleNumber([...Array(10000).fill(1), 2, ...Array(10000).fill(1)]); // 2常见错误
错误一:初始值设错
javascript
// 错误:初始值不是 0
let result = nums[0];
for (let i = 1; i < nums.length; i++) {
result ^= nums[i];
}
// 这样也能工作,但不如 0 清晰
// 推荐:初始值为 0
let result = 0;
for (const num of nums) {
result ^= num;
}错误二:尝试用其他方法
javascript
// 哈希表法:空间 O(n),不满足要求
function singleNumber(nums) {
const set = new Set();
for (const num of nums) {
if (set.has(num)) set.delete(num);
else set.add(num);
}
return [...set][0];
}
// 排序法:时间 O(n log n),不满足要求
function singleNumber(nums) {
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i += 2) {
if (nums[i] !== nums[i + 1]) return nums[i];
}
}扩展:其他出现次数
| 题目变体 | 解法 |
|---|---|
| 其他出现 2 次,1 个出现 1 次 | 全部异或(本题) |
| 其他出现 3 次,1 个出现 1 次 | 按位统计(LeetCode 137) |
| 其他出现 2 次,2 个出现 1 次 | 分组异或(LeetCode 260) |
相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 136. 只出现一次的数字 | 简单 | 本题 |
| 137. 只出现一次的数字 II | 中等 | 其他出现3次 |
| 260. 只出现一次的数字 III | 中等 | 两个只出现一次 |
| 268. 丢失的数字 | 简单 | 异或找缺失 |
总结
- 核心性质:异或的自反性(a ^ a = 0)和零元素(a ^ 0 = a)
- 适用条件:其他元素出现偶数次
- 时间空间:O(n) 时间,O(1) 空间
- 一行代码:
nums.reduce((acc, num) => acc ^ num, 0)
复杂度
- 时间:O(n)
- 空间:O(1)