刷题路上,总会遇到一些看似简单却暗藏巧思的题目,LeetCode 136. 只出现一次的数字就是其中之一。这道题不仅考察对数组的基本操作,更核心的是对「位运算」的灵活运用——既要满足线性时间复杂度,又要控制常量额外空间,常规思路很容易陷入瓶颈,而最优解却简洁到让人眼前一亮。
一、题目解析:明确约束,找准核心
先再仔细读一遍题目,避免遗漏关键条件:
给定一个非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。要求找出那个只出现了一次的元素。
最关键的约束的是:必须设计线性时间复杂度(O(n))的算法,且只使用常量额外空间(O(1))。
这里要注意两个坑:
-
不能用哈希表(HashMap):虽然哈希表可以做到O(n)时间,但空间复杂度是O(n),不符合常量空间要求;
-
不能用排序+遍历:排序的时间复杂度是O(n log n),超出了线性时间的限制;
-
不能用双重循环:时间复杂度是O(n²),同样不满足要求。
所以,这道题的核心考点,不是数组遍历,而是「如何用位运算实现无额外空间的去重匹配」。
二、最优解法:异或运算(^)的妙用
这道题的最优解,就是利用异或运算的三个核心性质,直接实现线性时间、常量空间的求解,先看最终代码(题目给出的代码已最优,我们逐行拆解):
function singleNumber(nums: number[]): number {
let res = 0;
for (const num of nums) {
res ^= num;
}
return res;
};
代码只有5行,却完美满足所有约束,关键就在于「res ^= num」这一步。要理解它,必须先掌握异或运算的三个核心性质(记牢这三点,类似题目都能秒解):
1. 异或运算的核心性质
-
性质1:任何数与0异或,结果还是它本身(a ^ 0 = a)。比如 5 ^ 0 = 5,0 ^ (-3) = -3;
-
性质2:任何数与自身异或,结果为0(a ^ a = 0)。比如 3 ^ 3 = 0,10 ^ 10 = 0;
-
性质3:异或运算满足交换律和结合律(a ^ b ^ c = a ^ c ^ b = (a ^ b) ^ c)。也就是说,运算顺序不影响最终结果。
2. 解法逻辑拆解
结合题目条件(除一个元素外,其余元素均出现两次),我们可以利用异或的性质推导:
假设数组为 [a, b, a, c, b],其中 c 是只出现一次的元素。将数组中所有元素依次异或:
res = 0 ^ a ^ b ^ a ^ c ^ b
根据交换律和结合律,调整顺序:
res = (a ^ a) ^ (b ^ b) ^ (0 ^ c)
再根据性质1和性质2,简化后:
res = 0 ^ 0 ^ c = c
也就是说,遍历数组,将所有元素与初始值0进行异或,最终的结果就是那个只出现一次的元素——因为所有出现两次的元素,异或后都会抵消为0,最后只剩下单独的那个元素。
三、代码逐行解析,一看就懂
再回到代码,逐行拆解每一步的作用,确保理解无死角:
-
「let res = 0;」:初始化结果变量为0,因为异或运算的初始值设为0,才能满足「0 ^ 元素 = 元素」的性质,为后续运算铺垫;
-
「for (const num of nums)」:遍历数组中的每一个元素,时间复杂度为O(n),符合线性时间要求;
-
「res ^= num;」:等价于「res = res ^ num」,将当前res与数组元素逐次异或,利用异或的性质抵消重复元素;
-
「return res;」:遍历结束后,res的值就是只出现一次的元素,直接返回即可。
四、总结:这道题的核心收获
这道题看似简单,却能帮我们跳出「常规遍历、哈希表」的固定思维,学会用位运算解决「去重、匹配」类问题——尤其是当题目限制「常量空间」时,异或运算往往是最优选择。
核心要点回顾:
-
异或运算的三个性质是解题关键,一定要牢记;
-
线性时间(O(n))+ 常量空间(O(1))的最优解,只能通过位运算实现;
-
代码简洁,但背后的逻辑需要结合异或性质推导,理解本质比死记代码更重要。