一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
每日刷题 2021.04.05
- leetcode原题链接:leetcode-cn.com/problems/pr…
- 难度:中等
- 方法:位运算
题目
- 给你两个整数
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 <= 1060 <= right - left <= 10
解题思路
- 注意:1既不是质数也不是合数
- 类似的位运算题目191. 位1的个数
判断一个数有几个1的三种解法
- 按位与
& 1和 移位运算符>>>- 解决了在使用取模
%运算符运算时,需要向下取整的操作parseInt(cur % 2)num += cur & 1; cur = cur >>> 1;
- 解决了在使用取模
- 按位与
n & n - 1- 得到的结果:当前这个数的最后一个1
cur &= (cur - 1); num++;
- 得到的结果:当前这个数的最后一个1
- 树状数组的
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已经判定是质数),因此直接从i的2倍开始判断.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的因数就是2和3,当你遍历完因数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和其本身外