今天在火车站候车,实在无聊,不如把之前领略过的算法复个盘。先亮出题目:
这一题本身并不难,难的是进一步的优化。直接AC过去简单,但能不能经得住更深入的提问,非常考验面试者的数学功底和科班素养。简而言之,看你到底懂不懂计算机的二进制操作。
思路1: 哈希表遍历一遍+找出值为1的键
/**
* @param {number[]} nums
* @return {number[]}
*/
var singleNumber = function(nums) {
let hash = {};
let res = [];
for(let i = 0; i < nums.length; i++) {
if(hash[nums[i]] !== undefined) {
hash[nums[i]]++;
}else {
hash[nums[i]] = 1;
}
}
Object.keys(hash).map(item => {
if(hash[item] === 1) res.push(item);
})
return res;
};
事实上这个算法的时间复杂度是O(n),已经很优秀了。空间复杂度也是O(n),有没有优化的余地呢?
有,而且可以让空间复杂度降到O(1),也就是常数级别。
思路2: 所有数进行异或 + 分组
首先简单科普一下异或操作,相同数异或为0,相异则结果为1。
如 1 ^ 1 = 0, 0 ^ 1 = 1
那么由此可以推出一些结论:
- 任何数和0异或,结果一定为它本身。
- 两个相等的数异或,结果一定为0
因此,我们第一步是这样:
let sign = 0;
for(let i = 0; i < nums.length; i ++){
sign ^= nums[i];
}
现在sign的结果是什么呢?如果只有一个不重复的数,那还好说,重复两次的数都抵消了,异或完的结果就是这个数。但是现在是两个数,应该如何来处理呢?
首先,有一点是非常确定的,那就是两个不重复数的二进制位一定有一位不同,也就是说异或完成的结果一定有一位是1。我们找出这一位,然后自己另外定义一个只有这一位是1、其它位都是0的数,用这个数和数组中每个数相与,如果相与结果为0,放到第一组,否则放到另外一组。每组的数字都全部进行异或,最后每组重复的数已经消掉,只剩下一个数,即不重复的数,这就把两个不重复的数分开了。
let n = 1;
let res = [0, 0];
while((n & sign) === 0){
n <<= 1;//左移一位
}
for(let i = 0; i < nums.length; i++) {
if((nums[i] & n) === 0) {
res[0] ^= nums[i];
}else {
res[1] ^= nums[i];
}
}
return res;
可以看到,这一次的空间复杂度彻底地降到了O(1)级别,已经非常优秀了。再次提交,性能也是有了质的飞跃。
不过,大家回过头反思我们寻找第一次遍历后寻找异或结果中1的位置这个过程,可能会有更直接、更本质的操作,那就是:可以让它和它的相反数进行与操作。
因为计算机里面对于负数的存储是采用符号位置1,后面的位取反加一,这样会有一个规律:
一个正整数和它的相反数进行相与操作,结果中一定只含一个一,其他位位0,而且这个1的位置正好是这个正整数最后一个1的位置。
即上面求n的过程可以直接写为:
let n = sign & (-sign);
再次提交,依然AC。