题目:
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
**输入:**nums = [2,2,1]
**输出:**1
示例 2 :
**输入:**nums = [4,1,2,1,2]
**输出:**4
示例 3 :
**输入:**nums = [1]
**输出:**1
看到题目时,第一反应是使用hashmap存储每个元素及元素出现的次数,然后将出现次数为1的座位返回值给出。但这种靠直觉做出来的方法提交上去往往排在低效的20%中。
异或算法
后来看到前90%的高效算法 都是使用异或算法做的,实现方式如下:
java
/**
* 给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
* 你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
*/
public class OnlyOnceNum {
static void main() {
int[] nums = {4,4,1,2,1,2,3};
System.out.println(test1(nums));
}
static int test1(int[] nums) {
int n = 0;
for (int num : nums) {
n = n ^ num;
}
return n;
}
}
异或运算的三个"超能力"
在二进制的世界里,异或运算的规则是:相同为0,不同为1。基于这个规则,它衍生出了三个极其重要的性质:
- 归零律(自己打自己) :任何数和自己做异或,结果都是 0。
- 比如:
a ^ a = 0
- 比如:
- 恒等律(和平主义者) :任何数和 0 做异或,结果还是它自己。
- 比如:
a ^ 0 = a
- 比如:
- 交换律和结合律(随意插队) :异或运算的顺序可以随便换,不影响最终结果。
- 比如:
a ^ b ^ a等价于a ^ a ^ b。
- 比如:
为什么这段代码能找出"单身狗"?
让我们把这三个性质结合起来看。题目告诉我们:"除了某个元素只出现一次以外,其余每个元素均出现两次"。
这意味着,如果我们把数组里所有的数字都拿来一起做异或运算:
- 那些成对出现的数字(比如两个 2,两个 1),会因为归零律 (
a ^ a = 0)全部互相抵消变成 0。 - 剩下的那个只出现一次的数字,会因为恒等律 (
a ^ 0 = a),和一堆 0 异或后,依然保留它原本的值。
再加上交换律,无论这些数字在数组里排得多乱,我们都可以脑补成把所有相同的数字挪到一起先抵消掉。
java
public int singleNumber(int[] nums) {
int res = 0; // 初始值为0,因为 0 异或任何数都不改变它
for(int i = 0; i < nums.length; i++) {
res = res ^ nums[i]; // 把所有数字串起来做异或
}
return res; // 成对的都抵消成0了,只剩下那个唯一的数字
}
复杂度分析
- 时间复杂度:O(N)。只需要把数组从头到尾遍历一遍。
- 空间复杂度 :O(1)。全程只用了一个变量
res来存结果,没有开辟任何额外的数组或哈希表,完美符合题目要求的"常量额外空间"。
但是我将入参数组改为:
int[] nums = {4,4,1,2,1,2,2,3};
跑出来的结果是1,跑去问通义,给出的回答是:
这里其实藏着一个小陷阱,是因为测试用的数组并不符合这道算法题的"前置条件"。
有一个非常严格的前提:"除了某个元素只出现一次以外,其余每个元素均出现两次。"
也就是说,这个算法能直接得出"那个唯一的单身狗",是建立在其他所有干扰项都必须是成双成对(偶数次)的基础上的。而你的测试数组里,数字 2 出现了 3 次,打破了这个规则,所以最后剩下的就不是单纯的某一个原数组里的数了,而是 2 和 3 异或后的混合产物。
就这?我继续追问"这个算法对数据的要求挺高,现实中怎么可能成对出现呢"
哈哈,你这一针见血!确实,现实世界的数据要是都像力扣题目里这样"成双成对",那月老都得失业了。
不过,虽然现实数据不会乖乖地"成对出现",但这个异或算法在真实的工程开发中,其实有一个超级经典且硬核的应用场景------校验数据的完整性(找不同)。
假设你在用网盘或者聊天软件传一个超大的文件(比如一个 2GB 的电影)。网络传输过程中难免会丢包或者出错,你怎么保证对方收到的文件和原文件一模一样呢?
这时候就可以用到异或的思想(进阶版叫 CRC 校验):
发送方:把整个大文件的所有字节数据做一遍异或(或者更复杂的校验运算),得出一个小小的"指纹"(校验码)。
接收方:收到文件后,也用同样的方法把所有字节异或一遍,算出自己的"指纹"。
对比:如果两个"指纹"不一样,说明文件在传输过程中被篡改或者损坏了,需要重新传。
在这个场景下,只要两边的数据完全一致,所有的字节异或下来都会完美抵消;一旦中间混进了哪怕一个字节的"杂音"(不一样的数据),最终的结果就会天差地别。这就是异或运算在底层通信、分布式存储(比如 RAID 磁盘阵列的容错备份)里的真实威力。
算法题的意义:
所以,刷算法题的时候,咱们可以换个角度来看待这些"苛刻的条件":
它们其实是在人为地创造一个极端的环境,逼着我们去发现某种数据结构或数学规律(比如位运算、单调栈、前缀和等)的极致特性。
做题时:我们学习的是这种"屠龙技"(特定的技巧和思维)。
工作时:现实中虽然没有现成的"龙"(完美的成对数据),但当我们遇到类似"海量数据找差异"、"极速比对"的需求时,脑子里蹦出的那个"异或"的念头,就是当初刷题留下的肌肉记忆。
#算法 #职场 #力扣