最优效率的位运算

433 阅读11分钟

位运算

基本的位运算包括与或还有异或,左移,无符号右移等操作。

循环n&n-1可以计算n中有多少个1

n & 1 是否是1,可以看出n的最低位是否是1

n ^ n是0

n >> 1 相当于除以2

n << 1 想当于乘以2

记住这些常用的技巧,在位运算中可能会比较帮助,但是大多数是需要综合使用这些技巧。

461. 汉明距离

题目描述

两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。

给出两个整数 x 和 y,计算它们之间的汉明距离。

例子1

Input: 1, 4

output: 2

解释:

1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑

上面的箭头指出了对应二进制位不同的位置

思考

1 可以首先异或两个数,则就得到了含有多少个1的数,然后计算这个数里边含有多少个1就可以了

参考实现1

实现1

/**
 * @param {number} x
 * @param {number} y
 * @return {number}
 */

export default (x, y) => {
  let z = x ^ y;
  let res = 0;
  while (z != 0) {
    res++;
    z &= z - 1;
  }
  return res;
};

190. 颠倒二进制位

题目描述

颠倒给定的 32 位无符号整数的二进制位。

例子1

Input: 00000010100101000001111010011100

output: 00111001011110000010100101000000

解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596, 因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。

例子2

Input: 11111111111111111111111111111101

output: 10111111111111111111111111111111

解释: 输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293, 因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。

思考

1 这个虽然是easy题目,但是感觉还是不好想到,后来看了下,可以通过设置一个结果res,让res有符号左移和n有符号右移

最后因为需要获得无符号的,所以res还得需要无符号右移0位

参考实现1

实现1

/**
 * @param {number} n - a positive integer
 * @return {number} - a positive integer
 */
// Runtime: 84 ms, faster than 93.00% of JavaScript online submissions for Reverse Bits.
// Memory Usage: 40.2 MB, less than 76.19% of JavaScript online submissions for Reverse Bits.
export default (n) => {
  if (n === 0) return 0;
  let result = 0;
  for (let i = 0; i < 32; i++) {
    result <<= 1;
    result |= n & 1;
    n >>= 1;
  }
  return result >>> 0;
};

136 只出现一次的数字

题目描述

给定一个整数数组,这个数组里只有一个数次出现了一次,其余数字出现了两次,求这个只 出现一次的数字。

例子1

Input: [4,1,2,1,2]

output: 4

例子2

Input: nums = [4,1,2,1,2]

output: 4

思考

1 很明显是使用异或,因为两个相同的数异或会是0

参考实现1

实现1

/**
 * @param {number[]} nums
 * @return {number}
 */
// Runtime: 84 ms, faster than 84.68% of JavaScript online submissions for Single Number.
// Memory Usage: 40.3 MB, less than 87.12% of JavaScript online submissions for Single Number.
export default (nums) => {
  let res = nums[0];
  for (let i = 1; i < nums.length; i++) {
    res ^= nums[i];
  }
  return res;
};

268. 丢失的数字

题目描述

给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

进阶:
你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

例子1

Input: [3,0,1]

output: 2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。

例子2

Input: nums = [0,1]

output: 2

解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。

例子3

Input: nums = [9,6,4,2,3,5,7,0,1]

output: 2

解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。

思考

1 如果想使用位运算,因为已经知道n^n === 0,所以想办法使用异或来解决。 但是首先是想到了先排序来解决

2 后来看了下,还可以借助i来解决,因为 n^0 === n 并且n^n === 0

参考实现1

参考实现2

实现1

/**
 * @param {number[]} nums
 * @return {number}
 */

// Runtime: 88 ms, faster than 65.50% of JavaScript online submissions for Missing Number.
// Memory Usage: 41.2 MB, less than 47.38% of JavaScript online submissions for Missing Number.
export default (nums) => {
  const len = nums.length;
  nums.sort((a, b) => a - b);
  let res = 0;
  for (let i = 0; i < nums.length; i++) {
    if (res === nums[i]) {
      res++;
    } else {
      if (res ^ (nums[i] != 0)) {
        return res;
      }
    }
  }
  return res;
};

实现2

/**
 * @param {number[]} nums
 * @return {number}
 */

// Runtime: 88 ms, faster than 65.50% of JavaScript online submissions for Missing Number.
// Memory Usage: 41.4 MB, less than 28.25% of JavaScript online submissions for Missing Number.

export default (nums) => {
  let res = nums.length;
  for (let i = 0; i < nums.length; i++) {
    res ^= i;
    res ^= nums[i];
  }
  return res;
};

260. 只出现一次的数字 III

题目描述

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

例子1

Input: [1,2,1,3,2,5]

output: [3,5]

思考

1 这里使用位运算,确实很难思考到,甚至看了结果仍然不是很好理解,刚刚开始是想因为n^n === 0,所以想通过两次遍历分别求出两个不同的数字,但是发现不行,因为两个不同的数字可能是相邻在一起,后来发现测试用例过不了。

后来看了下题解,这个确实很难想到

假设a和b是数组中的两个不同的数字
那么a^b肯定是不等于0,因为如果等于0,那么a===b
如果a^b不等于0,那么在二进制表示中肯定有有一位是1
那么我们假设m = a^b^数组中其它值, 那么我们可以通过m & ~(m - 1) 获取最低位是1的值
所以数组肯定分成两部分,一部分是该位置是1,一部分该位置是0,否则不可能是异或出来该位置等于1
所以该位置都是1的最后异或出来一个结果,该位置都是0的异或出来另外一个结果

参考实现1

实现1

/**
 * @param {number[]} nums
 * @return {number[]}
 */

// Runtime: 80 ms, faster than 92.98% of JavaScript online submissions for Single Number III.
// Memory Usage: 39.4 MB, less than 87.72% of JavaScript online submissions for Single Number III.
export default (nums) => {
  let res = [0, 0];
  let sum = 0;
  for (let i = 0; i < nums.length; i++) {
    sum ^= nums[i];
  }

  sum = sum & ~(sum - 1);

  for (let i = 0; i < nums.length; i++) {
    if ((nums[i] & sum) === 0) {
      res[0] ^= nums[i];
    } else {
      res[1] ^= nums[i];
    }
  }

  return res;
};

时间复杂度O(n),空间复杂度O(1)

342. 4的幂

题目描述

给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。

整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == 4x

例子1

Input: n = 16

output: true

例子2

Input: n = 16

output: false

思考

1 这里是如果是4的平方,需要符合三个条件

// 如果是4的平方必须符合三种情况
// 1 n > 0
// 2 n是2的平方 也就是n & n-1
// 3 n里边所有1的位置都在奇数位上

参考实现1

实现1

/**
 * @param {number} n
 * @return {boolean}
 */
// Runtime: 84 ms, faster than 97.69% of JavaScript online submissions for Power of Four.
// Memory Usage: 39.9 MB, less than 48.52% of JavaScript online submissions for Power of Four.
export default (n) => {
  // 如果是4的平方必须符合三种情况
  // 1 n > 0
  // 2 n是2的平方 也就是n & n-1
  // 3 n里边所有1的位置都在奇数位上

  return n > 0 && (n & (n - 1)) === 0 && (n & 0b1010101010101010101010101010101) > 0;
};

318. 最大单词长度乘积

题目描述

给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 0。 br/>

例子1

Input: ["abcw","baz","foo","bar","xtfn","abcdef"]

output: 16

解释:这两个单词为 "abcw", "xtfn"。

例子2

Input: ["a","ab","abc","d","cd","bcd","abcd"]

output: 4

解释:这两个单词为 "ab", "cd"。

例子3

Input: ["a","aa","aaa","aaaa"]

output: 0

解释:不存在这样的两个单词。

思考

1 这里思路很简单,主要难点是如果确认两个字符串中不存在相同的字符
可以使用一个长度是26的数组,如果第一位是1,则表示字符串中“a”存在,

如果两个表示字符串的数组使用与操作等于0的时候,表示两个字符串不包含相同字符

参考实现1

实现1

/**
 * @param {string[]} words
 * @return {number}
 */
// Runtime: 100 ms, faster than 91.23% of JavaScript online submissions for Maximum Product of Word Lengths.
// Memory Usage: 42.2 MB, less than 85.96% of JavaScript online submissions for Maximum Product of Word Lengths.
export default (words) => {
  if (words.length < 1) return 0;
  const len = words.length;
  const value = new Array(len).fill(0);
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < words[i].length; j++) {
      // 也就是使用26个数组表示字符串,如果数组第一位是1,则表示存在a
      value[i] |= 1 << (words[i].charCodeAt(j) - 97);
    }
  }
  let maxProduct = 0;
  for (let i = 0; i < len; i++)
    for (let j = i + 1; j < len; j++) {
      if ((value[i] & value[j]) === 0 && words[i].length * words[j].length > maxProduct)
        maxProduct = words[i].length * words[j].length;
    }
  return maxProduct;
};


338. 比特位计数

题目描述

给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。 br/>

例子1

Input: 2

output: [0,1,1]

例子2

Input: 5

output: [0,1,1,2,1,2]

进阶:

1 给出时间复杂度为O(n * sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?

2 要求算法的空间复杂度为O(n)

3 你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount) 来执行此操作。

思考

1 O(n * sizeof(integer)) 解法很简单,直接看实现1就可以

2 可是如果想使用O(n)的算法,一直局限在实现1中如何更新,后来想到了可以使用dp来解决
dp也很容易 dp[i] 表示i拥有的个数,那么很容易想到如果i第一位是0,dp[i] = dp[i>>1] ,如果i第一位为1,则dp[i] = dp[i>>1]

参考实现1

参考实现2

实现1

/**
 * @param {number} num
 * @return {number[]}
 */

// Runtime: 96 ms, faster than 83.28% of JavaScript online submissions for Counting Bits.
// Memory Usage: 44.6 MB, less than 47.00% of JavaScript online submissions for Counting Bits.

export default (num) => {
  let res = [0];
  for (let i = 1; i <= num; i++) {
    let bits = 0;
    while (i !== 0) {
      bits++;
      i &= i - 1;
    }
    res.push(bits);
  }
  return res;
};

实现2

/**
 * @param {number} num
 * @return {number[]}
 */

// Runtime: 92 ms, faster than 94.32% of JavaScript online submissions for Counting Bits.
// Memory Usage: 44.8 MB, less than 34.70% of JavaScript online submissions for Counting Bits.
// dp[i] 表示i拥有的个数,那么很容易想到如果i第一位是0,dp[i] = dp[i>>1] ,如果i第一位为1,则dp[i] = dp[i>>1]
export default (num) => {
  const dp = [];
  dp[0] = 0;
  if (num === 0) {
    return dp;
  }
  dp[1] = 1;
  if (num === 1) {
    return dp;
  }
  for (let i = 2; i <= num; i++) {
    dp[i] = dp[i >> 1] + (i & 1);
  }
  return dp;
};

693. 交替位二进制数

题目描述

给定一个正整数,检查它的二进制表示是否总是 0、1 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

例子1

Input: 5

output: true
解释:5 的二进制表示是:101

例子2

Input: 7

output: false
解释:7 的二进制表示是:111.

思考

1 如果只是看题解,你会发现很简单,可是到底是如何想出来的呢?

如果想求n是否都是交替表示,如果是交替表示的话,可以先求n^n/2,这时候n^n/2 所有位置肯定都是1,然后再和n^n/2+1执行与操作,如果结果为0就是交替位,否则就不是

参考实现1

实现1

/**
 * @param {number} n
 * @return {boolean}
 */
//  Runtime: 76 ms, faster than 80.61% of JavaScript online submissions for Binary Number with Alternating Bits.
//  Memory Usage: 38.7 MB, less than 54.08% of JavaScript online submissions for Binary Number with Alternating Bits.
export default (n) => {
  n = n ^ Math.floor(n >> 1);
  return !(n & (n + 1));
};

476. 数字的补数

题目描述

给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。

例子1

Input: 5

output: 2
解释: 5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。

例子2

Input: 1

output: 0
解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。

思考

1 这里很明显想取反,比如当是5的时候,二进制是101,但是如果直接取反5,高位都是1,明显不符合结果,所以必须想办法把高位给置0

所以可以通过取得和5同样的mask,比如111,高位都是0

参考实现1

实现1

/**
 * @param {number} num
 * @return {number}
 */
// Runtime: 72 ms, faster than 94.29% of JavaScript online submissions for Number Complement.
// Memory Usage: 38.8 MB, less than 30.00% of JavaScript online submissions for Number Complement.
export default (n) => {
  let mask = 1;
  while (mask < n) {
    mask = (mask << 1) | 1;
  }
  return ~n & mask;
};