从三道算法题浅析位运算符和x&-x的意义

281 阅读5分钟

题目一

题目描述

求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:

输入: n = 3
输出: 6

示例 2:

输入: n = 9
输出: 45

时间、空间超100%的解答

等差数列的前n项和,自然是用求和公式,时间空间都是O(1),但是根据题目,不允许使用乘除法,自然只能使用递归了

function multi(a, b){
  return b && ((b & 1 && a) + multi(a << 1, b >> 1))
}
var sumNums = function(n) {
    return multi(n, n+1)>>1
};

递归实现乘法

大致思路就是不断的把右边的2移动到左边去,因为2的变化可以通过<<、>>实现,遇到奇数就停下来进行+运算,否则一直进行乘除运算

  1. 判断b不能为零
  2. 判断b的奇偶性,b&1 == 0是偶数执行3,否则b&1 == 1是奇数执行4
  3. 直接b的一个2移动给a,进入下一次递归
  4. 因为b是奇数,multi(a << 1, b >> 1)) == multi(a << 1, b-1 >> 1)),所以多加一个a,进入下一次递归
  5. 重复1,知道b为0返回0,开始退出递归
6 * 7
= 6 + multi(12, 3)
= 6 + 12 + multi(24, 1)
= 6 + 12 + 24 + multi(48, 0)
= 42
8 * 8
= multi(16, 4)
= multi(32, 2)
= multi(64, 1)
= 64 + multi(128, 0)

小结

  1. x & 1返回x的奇偶性,为零则是偶数,否则为奇数
  2. a && b的返回值要么是0或flase,要么是b或true

题目二

题目描述

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4

示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1

时间、空间超100%的解答

常规思路是储存一个map:键是数字,值是出现的次数。下列题解思路大致:

  • num第一次出现时:a = nums; b =0;
  • num第二次出现时:a = 0; b = nums;
  • num第三次出现时:a = 0; b =0;
var singleNumber = function (nums) {
  let a = 0;
  let b = 0;
  nums.forEach(num => {
    a = (a ^ num) & ~b;
    b = (b ^ num) & ~a;
  });
  return a;
};

小结

位运算的性质:a^a=0、a^0=a、a^b^c=a^c^b

题目三

题目描述

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

时间、空间超100%的解答

这题同上一题的思路一样,利用性质a^a=0,所以,7^2^3^4^4^3^2 = 7。

var singleNumbers = function(nums) {
  let sum = 0;
  let result = [0,0]
  nums.forEach(element => {
    sum^=element
  });
  let flag = (-sum)&sum
  nums.forEach(element => {
    if((flag&element)==0){
      result[0]^=element
    }else{
      result[1]^=element
    }
  });
  return result;
};

上述代码的时间空间复杂的都是超100%,说明位运算符才是这些题的正确解题姿势

x & -x的意义

我们都知道, -x 的值, 其实就是在x的值的基础上进行按位取反(~x)之后在增加1所得, 也就是说x & -x == x & (~x + 1)

x 为奇数

x 为奇数比较简单, 因为奇数取反后的值一定是偶数, 而偶数的值 + 1之后, 并不会影响进位。

如果是x是奇数, 那x & -x 的结果一定是1

x 为偶数

我们都知道, 当一个奇数 + 1时,表示的二进制数则会发生进位,这样的话, 会产生一个连锁反应,也就是最低位的那些连续的1都会被清0, 如:

0000 0000 0111 1111 + 1=0000 0000 1000 0000 

如果一个偶数, 如 0000 0100 1110, 取反后的结果就变成了 1111 1011 0001,而当这个值 + 1之后由于发生了进位, 即

1111 1011 0001 + 1 = 1111 1011 0010

这个结果再与最初的值相与后, 只会有一位保留为

0000 0100 1110 & 1111 1011 0010 = 0000 0000 0010

这个结果与最初的值又有什么关系呢?很显然, 这个值的与原值的末位0的个数是一致的.仔细一想, 这个原值肯定是能被结果值整除的, 而这个结果值又是2^ n

当一个偶数与它的负值相与时,结果是能整除这个偶数的最大的2的幂, 即: m = n & -n , 则 n % m = 0, 且 m = 2 ^ k

最终结论

当一个数与其取负后的值相与, 如果这个数是偶数, 则结果是能整除这个偶数的最大的2的幂(即: m = n & -n , 则 n % m = 0, 且 m = 2 ^ k), 如果这个数是奇数, 则结果必为1