题目
给你一个非负整数 num ,请你返回将它变成 0 所需要的步数。 如果当前数字是偶数,你需要把它除以 2 ;否则,减去 1 。
示例 1:
输入:num = 14
输出:6
解释:
步骤 1) 14 是偶数,除以 2 得到 7 。
步骤 2) 7 是奇数,减 1 得到 6 。
步骤 3) 6 是偶数,除以 2 得到 3 。
步骤 4) 3 是奇数,减 1 得到 2 。
步骤 5) 2 是偶数,除以 2 得到 1 。
步骤 6) 1 是奇数,减 1 得到 0 。
示例 2:
输入:num = 8
输出:4
解释:
步骤 1) 8 是偶数,除以 2 得到 4 。
步骤 2) 4 是偶数,除以 2 得到 2 。
步骤 3) 2 是偶数,除以 2 得到 1 。
步骤 4) 1 是奇数,减 1 得到 0 。
示例 3:
输入:num = 123
输出:12
提示:
0 <= num <= 10^6
思路
思路一:
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:38.6 MB, 在所有 Java 提交中击败了30.81%的用户
思路一
这应该就是最普通的思路了,循规蹈矩,没有一点特别。
steps记录步骤数,当num不等于0时循环,将num和2取余,如果不为0则奇数,执行减一,如果为0则偶数,执行/2,输出步骤数。
class Solution {
public int numberOfSteps(int num) {
int steps = 0;
while(num != 0){
if(num%2 != 0){
num = num - 1;
}else{
num/=2;
}
steps++;
}
return steps;
}
}
思路二 模拟
将num与 1 进行位运算(&按位与)来判断num的奇偶性。
记录操作次数时:
如果num>1,我们需要加上一次除以 2 的操作。
如果num 是奇数(按位与1),我们需要加上一次减 1 的操作。
然后使num 的值变成[num/2](右移1位).
重复以上操作直到num=0 时结束操作。
class Solution {
public int numberOfSteps(int num) {
int ret = 0;
while (num > 0) {
ret += (num > 1 ? 1 : 0) + (num & 0x01);
num >>= 1;
}
return ret;
}
}
思路三 直接计算
由方法一的步骤可知,当 num>0 时,总操作次数等于总减 1 的操作数与总除以 2 的操作数之和。总减 1 的操作数等于 num 二进制位 1 的个数,总除以 2 的操作数等于num 二进制数长度减 1,即最高位右移到最低位的距离。
二进制数长度len 可以通过前导零数目clz 间接求解,即len=W−clz,其中W=32 是int 类型的位数。
使用二分法加速求解前导零数目,算法如下:
首先判断num 前半部分是否全为零,如果是,则将clz 加上前半部分的长度,然后将后半部分作为处理对象,否则将前半部分作为处理对象。重复以上操作直到处理的对象长度为 1,直接判断是否有零,有则将clz 加 1。
使用分治法来加速求解二进制数位 1 的个数(SWAR算法),算法如下:
对二进制数num,它的位 11 的个数等于所有位的值相加的结果,比如 10110101(2)= 1 + 0 + 1 + 1 + 0 + 1 + 0 +1。我们可以将 8 个位的求和分解成 4 个相邻的位的求和,然后将 4 个中间结果分解成 2 个相邻的求和,即 10110101(2) =(1+0)+(1+1)+(0+1)+(0+1)=((1+0)+(1+1))+((0+1)+(0+1))=5。32 位数的求解过程同理。
class Solution {
public int numberOfSteps(int num) {
return num == 0 ? 0 : length(num) - 1 + count(num);
}
public int length(int num) {
int clz = 0;
if ((num >> 16) == 0) {
clz += 16;
num <<= 16;
}
if ((num >> 24) == 0) {
clz += 8;
num <<= 8;
}
if ((num >> 28) == 0) {
clz += 4;
num <<= 4;
}
if ((num >> 30) == 0) {
clz += 2;
num <<= 2;
}
if ((num >> 31) == 0) {
clz += 1;
}
return 32 - clz;
}
public int count(int num) {
num = (num & 0x55555555) + ((num >> 1) & 0x55555555);
num = (num & 0x33333333) + ((num >> 2) & 0x33333333);
num = (num & 0x0F0F0F0F) + ((num >> 4) & 0x0F0F0F0F);
num = (num & 0x00FF00FF) + ((num >> 8) & 0x00FF00FF);
num = (num & 0x0000FFFF) + ((num >> 16) & 0x0000FFFF);
return num;
}
}
求1的数量的算法,也常被称为汉明重量(Hamming Weight),通过一系列的位移和位运算操作,可以在常数时间内计算多个字节的汉明重量,而且不需要使用额外的内存。接下来分析一下SWAR算法:
step1:首先我们可以很容易的知道,0x55555555对应的二进制的数为0B 01010101 01010101 01010101 01010101,而第一步运算相当于,把num奇偶位的数字进行相加。并且存放在了奇数位,相加如有进位则放在偶数位。
step2:0x33333333对应的二进制的数为0B 00110011 00110011 00110011 00110011,把num的奇数位,与下一个奇数位相加(第一位加第三位,第五位加第七位),把num的偶数位,与下一个偶数位相加(第二位加第四位,第六位加第八位)。如有进位,则保存到第三位,或者第七位。
step3:0x0F0F0F0F对应的二进制的数为0B 00001111 00001111 00001111 00001111,把num的每个字节中,前四位,与后四位相加。此时,每个字节中所含1的个数,都集中到了前四位。此时可以用0x0m0n0i0j来表示这个数,其中m,n,i,j代表之前num每个字节所含1的个数。
step4:也是最神奇的一步,通过这一步,把m,n,i,j这四个数相加。得到最终的个数。在这一步,我们不需要把0x01010101化为二进制。而是直接带入运算。通过下面的计算式,我们可以看出相乘,然后右移24位,刚好就是我们所要的结果。神奇~
SWAR算法细节分析
0111 ( 十进制7
a) 0111 & 0101 = 0101 (num & 0x55555555) ===> 保留原数字奇数位的1
b) 0011 & 0101 = 0001 ((num >> 1) & 0x55555555) ===> 保留原数字偶数位的1
c) 0101 + 0001 = 0110 ( + 把统计到的奇数1和偶数1的数目相加,每两个二进制位表示原数字对应的二进制位的1的数量,比如
01 表示原数字在对应的前两个位置里,奇数1和偶数1相加一共只有1个1,这个1来自 a) 的结果
10 表示原数字在对应的后两个位置里,奇数1和偶数1相加一共有2个1,一个来自 a) 的结果,另一个来自 b) 的结果
d) 0110 & 0011 = 0010 (num & 0x33333333) ===> 保留上一步求出的和的右边两个位
e) 0001 & 0011 = 0001 ((num >> 2) & 0x33333333) ===> 保留上一步求出的和的左边两个位
f) 0010 + 0001 = 0011 ( + ===> 左右两位相加,现在每四个二进制位表示原数字对应二进制位的1的数量
g) 0000 0011 & 0000 1111 = 0000 0011 (num & 0x0F0F0F0F) ===> 保留上一步求出的右边四个位
h) 0000 0000 & 0000 0011 = 0000 0000 ((num >> 4) & 0x0F0F0F0F) ===> 保留上一步求出的和的左边四个位
i) 0000 0011 + 0000 0000 = 0000 0011 (+ ===> 左右四位相加,现在每八个二进制位表示原数字对应二进制位的1的数量
j) 0000 0000 0000 0000 0000 0000 0000 0011
* 0000 0001 0000 0001 0000 0001 0000 0001
------------------------------------------
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0011
0000 0000 0000 0000 0000 0000 0000 0011
结果相加,取右边32位,之后又移24位,得到 0000 0011 十进制结果为 3
其他
求汉明重量
求汉明重量还有一种比较简单的方法:
首先我们要知道n & (n - 1)代表着把n的最后一个1变成0,为什么?举例说明,令n = 10110,那么n - 1 = 10101,n & (n - 1) = 10110 & 10101 = 11100,确实把最后一个1变成了0。
所以,只要重复n & (n - 1)的操作,直到n == 0,那么操作了几次,n中就有几个1。
public class Solution {
public int hammingWeight(int n) {
int ans = 0;
while (n != 0) {
ans++;
n &= (n - 1);
}
return ans;
}
}
逻辑运算符
&&:短路与。两侧要求必须是布尔表达式
int k = 2;
//++k==2 先进行i自增,k=3,再进行k==2,返回false
//k++==3 不会执行
System.out.println((++k==2) && (k++==3));//false
System.out.println(k);//3 可以简单来想,因为只有左侧的boolean表达式执行计算,那么k只有1次++,就会只加1。
||(短路或)同上理
位运算符
位运算符用来对二进制位进行操作,包括按位取反(~)、按位与(&)、按位或(|)、异或(^)、右移(>>)、左移(<>>)。位运算符只能对整数型和字符型数据进行操作。
&:按位与,也叫逻辑与。&的两侧可以是int,也可以是boolean表达式
//第一种情况:&的两侧是int
//当&两侧是int时,要先把运算符两侧的数转化为二进制数再进行运算
//12 转为二进制 0000 1100
//5 转为二进制 0000 0101
// 1为真,0为假,按逻辑,只有前后都为真,结果才为真 也就是只有在 1&1 时,结果才为1 ,其他情况结果都是0
// 所以返回结果是 0000 0100,转为10进制数就是4
System.out.println(12 & 5);//4
//第二种情况:&的两侧是boolean表达式
//特点:两侧的boolean表达式都要执行计算
int i = 2;
//++i==2 先进行i自增,i=3,再进行i==2,返回false
//i++==3 先进行i==3,返回true,然后i自增,i=4
System.out.println((++i==2) & (i++==3));//false
System.out.println(i);//4 可以简单来想,因为前后的boolean表达式都会计算,那么i有2次++,就会加2。
|(按位或)同上理
异或(^)
参加运算的两个数据,按二进制位进行“异或”运算。
运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
左移<<
运算规则:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。相当于乘2,例如: 12345 << 1,则是将数字12345左移1位:位移后十进制数值变成:24690,刚好是12345的二倍。若左移的位数超过32位,则会进行求余操作,所以左移32位相当于左移0位。
右移>>
低位移出(舍弃),高位的空位补符号位。相当于除2。超过32位求余。负数右移需要先求补码(反码+1,求时符号位不变)再右移再求值。
无符号右移(>>>)
无符号右移运算符和右移运算符是一样的,不过无符号右移运算符在右移的时候是补0的,而右移运算符是补符号位的。
参考
链接: 二进制位统计算法(swar)