五分钟带你领略: 位运算操做之美

今天在火车站候车,实在无聊,不如把以前领略过的算法复个盘。先亮出题目:面试

这一题自己并不难,难的是进一步的优化。直接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 = 1ui

那么由此能够推出一些结论:spa

  1. 任何数和0异或,结果必定为它自己。
  2. 两个相等的数异或,结果必定为0

所以,咱们第一步是这样:code

let sign = 0;
for(let i = 0; i < nums.length; i ++){
    sign ^= nums[i];
}
复制代码

如今sign的结果是什么呢?若是只有一个不重复的数,那还好说,重复两次的数都抵消了,异或完的结果就是这个数。可是如今是两个数,应该如何来处理呢?cdn

首先,有一点是很是肯定的,那就是两个不重复数的二进制位必定有一位不一样,也就是说异或完成的结果必定有一位是1。咱们找出这一位,而后本身另外定义一个只有这一位是一、其它位都是0的数,用这个数和数组中每一个数相与,若是相与结果为0,放到第一组,不然放到另一组。每组的数字都所有进行异或,最后每组重复的数已经消掉,只剩下一个数,即不重复的数,这就把两个不重复的数分开了。blog

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。

相关文章
相关标签/搜索