[ 位运算 ]762. 二进制表示中质数个计算置位

209 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

每日刷题 2021.04.05

题目

  • 给你两个整数 left 和 right ,在闭区间 [left, right] 范围内,统计并返回 计算置位位数为质数 的整数个数。
  • 计算置位位数 就是二进制表示中 1 的个数。
  • 例如, 21 的二进制表示 10101 有 3 个计算置位。

示例

  • 示例1
输入:left = 6, right = 10
输出:4
解释:
6 -> 110 (2 个计算置位,2 是质数)
7 -> 111 (3 个计算置位,3 是质数)
9 -> 1001 (2 个计算置位,2 是质数)
10-> 1010 (2 个计算置位,2 是质数)
共计 4 个计算置位为质数的数字。
  • 示例2
输入:left = 10, right = 15
输出:5
解释:
10 -> 1010 (2 个计算置位, 2 是质数)
11 -> 1011 (3 个计算置位, 3 是质数)
12 -> 1100 (2 个计算置位, 2 是质数)
13 -> 1101 (3 个计算置位, 3 是质数)
14 -> 1110 (3 个计算置位, 3 是质数)
15 -> 1111 (4 个计算置位, 4 不是质数)
共计 5 个计算置位为质数的数字。

提示

  • 1 <= left <= right <= 106
  • 0 <= right - left <= 10

解题思路

判断一个数有几个1的三种解法

  • 按位与& 1 和 移位运算符>>>
    • 解决了在使用取模%运算符运算时,需要向下取整的操作parseInt(cur % 2)
        num += cur & 1;
        cur = cur >>> 1;
      
  • 按位与n & n - 1
    • 得到的结果:当前这个数的最后一个1
        cur &= (cur - 1);
        num++;
      
  • 树状数组的lowbit( x & (-x))
    • (效率最快)原理:当前的数x 按位与 与其相反的负数-x
    • -x: x除去符号位后,按位取反再加一,即:补码。因为负数在计算机中存储的形式,就是补码
    • 举例: 10 => (二进制)1010 -10 => 按位取反:0101,+1 => 0110
      • 1011 与 0110按位与,得到0010,即:最后一个1后面的数字,即:0010
        cur -= cur & (-cur);
        num++;
      

判断一个数是否是质数

  • 即当前数为x
  • 通常的思路:对当前这个数,从2开始往后遍历,直到x
    • 如果在遍历过程中,遇到可以整除的数,那么x就不是质数;否则x为质数。
    • 注意:0和1既不是质数也不是合数,2是最小的质数
  • 优化:循环遍历的次数大小,可以到x的二分之一次方,即:x开平方根
    • 因为开平方根可能存在小数的情况,因此书写i * i < x即可。

拓展:筛素数(埃式筛法)

原版:假设需要查找n = 100以内的质数

  • 创建一个长度为n + 1的数组,只需要遍历n的开平方根一半的数据即可。
  let n = 100;
  let prime = new Array(n + 1);
  for(let i = 0; i <= n; i++) {
    // 1: 表示为质数
    prime[i] = 1;
  }
  // 0 和 1 都不是质数,因此将其调整为0
  prime[0] = 0,prime[1] = 0;
  // 对每个数进行质数和合数的判断
  for(let i = 2; i * i <= n; i++) {
    // 如果遍历当前是质数的话,那么就需要遍历其后面的倍数,将其标记为合数0
    if(prime[i] == 1){
      for(let j = 2 * i; j <= n; j += i) {
        prime[j] = 0;
      }
    }
  }

改进版:优化双层for循环,第二层的开始条件

// 原版: for(let j = 2 * i; j <= n; j += i)
// 优化后:
for(let j = i * i; j <= n; j += i)
  • Why? 为什么可以这样优化呢?即:为什么可以将初始值2 * i修改为i * i呢?
    • 2 * i本质上的意思:当前i是质数(即:1倍i已经判定是质数),因此直接从i2倍开始判断.
    • i * i根据上述的含义:推断=> 前面的2 * i ~ (i - 1) * i之间的数都已经被判定过了,因此直接从i * i开始判断。
    • 那么是如何断定前面的2 * i ~ (i - 1) * i都已经被判定过了呢?举个例子:假设当前的i = 2,那么2 * 2 = 4会被标记,2 * 2 + 2 = 6也会被标记;当i = 3进来时,如果还是i * 2 = 3 * 2 = 6,此时就会发现6已经被2标记过了,因此i = 3的时候,直接从i * i = 3 * 3开始即可。
    • 因为:6 = 2 * 3,6的因数就是23,当你遍历完因数2能够组成的所有的合数后,2 * 3也会被遍历到,因此就不需要在3的时候再去遍历3 * 2

AC代码

var countPrimeSetBits = function(left, right) {
  let res = 0;
  while(left <= right) {
    // 判断每一位有多少个1
    let cur = left, num = 0;
    while(cur != 0) {
      cur -= cur & (-cur);
      num++;
    }
    // 判断和是否是素数
    if(num > 2){
      let flag = false;
      for(let i = 2; i < num; i++) {
        if(num % i == 0) {
          flag = true;
          break;
        } 
      }
      if(!flag) res++;
    }else if(num == 2){
      // 1既不是质数也不是合数
      res++;
    }
    left++;
  }
  return res;
};

总结

  • 0 和 1既不是质数也不是合数。
  • 素数:只能除尽1和其本身外