剑指Offer对答如流系列 - 二进制中 1 的个数
Posted jefferychenxiao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指Offer对答如流系列 - 二进制中 1 的个数相关的知识,希望对你有一定的参考价值。
面试题14:二进制中 1 的个数
题目描述
请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。
问题分析与解决
这道面试题归属于 《剑指Offer》位运算章节。遇到二进制相关的问题,很容易想到位运算,虽然种类不多(与、或、异或、左移、右移),但是搞起来是千变万化的。待会再和你侃一些骚操作,我们先看这道题。
(一)思路一
“与运算”有一个性质:通过与对应位上为1,其余位为0的数进行与运算,可以指定整数某一位上的值。
这道题中,先把整数n与1做与运算,判断最低位是否为1;如果是1,则计数器加1;否则,没有计数器的事情。
接着把1左移一位(移位运算总要快于乘除),与n做与运算,可以判断次低位是否为1……反复左移,即可对每一个位置都进行判断,从而可以获得1的个数。这种方法需要循环判断32次。(int类型4个字节,一个字节8位)
这是最容易想到的一种方法。
当然,你可能会觉得为什么不右移整数n?那是因为 对于负数而言,最高位是1,右移的时候会保证负数的性质,即最高位仍是1,源源不断的右移,源源不断的1,是死循环啊。
public int NumberOf1(int n) {
int count = 0;
int flag = 1;
while (flag != 0) {
if ((flag & n) != 0) {
count++;
}
flag = flag << 1;
}
return count;
}
(二)思路二
与运算还有一个性质:把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0。
动笔画画呗,非常好证明。
循环利用与运算能将1变成0的这条性质,计算出1的个数
public int NumberOf1(int n) {
int count = 0;
while (n != 0) {
count++;
n = (n - 1) & n;
}
return count;
}
盘点位运算的骚操作
(1)移位运算(左移or右移)快于乘除
如果只针对2 进行乘除 强烈建议采用这种方法。
(2)一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0
上面题中有体现
判断一个数是否是2的次幂 也可以用这种方法呀。 n&(n-1)为0 说明这个数就是2的次幂
(3)通过右移判断二进制某位是0还是1
上面思路一用到的是这种思想,不过是用1进行左移实现的,思想保持一致。
(4)判断奇偶数
二进制中,末位是0→偶数,末位是1→奇数。
n&1=0 --- 偶数
n&1=1 --- 奇数
求求你,看完这篇文章后,判断奇偶不要再取余了
(5)两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身
如果给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出这只出现一次的数 。直接一个for循环,所有的值进行异或,结果就是那个只出现一次的数。
(6)两个数的交换
高逼格啊,相信你会在某些框架的源码中看到的
number1 = number1 ^ number2;
number2 = number1 ^ number2;
number1 = number1 ^ number2;
(7)不用判断语句,求整数的绝对值
正负通用 : i^(i>>31) + i>>>3
负数专用 : i^-1 + 1
相信我 这个看框架源码 会遇见的。
public static void main(String[] args) {
int i=-11,j=11;
// 正负通用
System.out.println(i+"的绝对值是"+((i^(i>>31))+(i>>>31)));
System.out.println(j+"的绝对值是"+((j^(j>>31))+(j>>>31)));
//已知i是负数的用法
System.out.println(i+"的绝对值是"+((i^-1)+1));
}
位运算高阶操作
上面举的用起来已经很欢乐了。要不要见一下一个大厂的专门考察位运算的面试题?
不用 + - * / 实现 加减乘除
(一)加法
对于位运算,还有一些你需要了解的基本性质
(1)二进制位异或运算相当于对应位相加,不考虑进位
1 ^ 1 = 0 ---> 1 + 1 = 0 (当前位值为0,进一位)
1 ^ 0 = 1 ---> 1 + 0 = 1 (当前位值为1)
0 ^ 0 = 0 ---> 0 + 0 = 0 (当前位值为0)
(2)二进制位与运算相当于对应位相加之后的进位
1 & 1 = 1 ---> 1 + 1 = 0 (当前位的值进一位)
1 & 0 = 0 ---> 1 + 0 = 1 (当前位的值不进位)
0 & 0 = 0 ---> 0 + 0 = 0 (当前位的值不进位)
(3)两个数相加:对应二进制位相加的结果 + 进位的结果
3 + 2 --> 0011 + 0010 --> 0011 ^ 0010 + ((0011 & 0010) << 1) ---> (0011 ^ 0010) ^ ((0011 & 0010) << 1)
当进位之后的结果为0时,相加结束
实现如下:
int add(int a,int b){
int s;
int c;
while(b != 0){
// 获得求和位
s = (a^b);
// 获得进位,然后需要向做移动一位表示进位
c = ((a&b)<<1);
a = s;
b = c;
}
return a;
}
换成递归的写法会更清晰点
int add(int a,int b){
// 假如进位为0
if(b ==0){
return a;
}
// 获得求和位
int s = a^b;
// 获得进位,然后需要向做移动一位表示进位
int c = ((a&b)<<1);
return add(s,c);
}
(二)减法
本质上是使用加法来做的,只是加了一个负数嘛
首先我们得先将被减数取反(a取反就是~a+1,补码知道吧)
// 取得相反数
int adverse(int a){
return add(~a,1);
}
// 减法函数 其中add就是前面的加法函数
int subtract(int a,int b){
return add(a,adverse(b));
}
(三)乘法
这个本质上也是靠加法来实现的,比如说6 * 100, 不就6个100相加吗?
不过我们要多考虑的事情是 正负的 处理
// 负数返回-1,正数返回0
int getsign(int i){
return (i >> 31);
}
// 如果为负数,则进行取反
int toPositive(int a) {
if (a >> 31 == -1)
// 进行取反
return add(~a, 1);
else
return a;
}
然后就用加法实现呗
int multiply(int a, int b){
boolean flag = true;
// 如果相乘为正数,flag为false
if (getsign(a) == getsign(b))
flag = false;
// 将a取正数
a = toPositive(a);
b = toPositive(b);
int re = 0;
while (b!=0) {
// 相加
re = add(re, a);
// b进行次数减一
b = subtract(b, 1);
}
// 假如结果是负数,则进行取反
if (flag)
re = adverse(re);
return re;
}
但是 若是单纯靠加法来实现,数字比较大的时候感觉实在太慢了。
考虑我们现实生活中手动求乘积的过程,这种方式同样适用于二进制,下面我以13*14为例,演示如何用手动计算的方式求乘数和被乘数绝对值的乘积。
步骤和十进制的类似
(1) 判断乘数是否为0,为0跳转至步骤(4)
(2) 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
(3) 被乘数左移一位,乘数右移一位;回到步骤(1)
(4) 确定符号位,输出结果;
实现的话,就这样
int multiply(int a, int b) {
boolean flag = true;
// 如果相乘为正数,flag为false
if (getsign(a) == getsign(b))
flag = false;
// 将a取正数
a = toPositive(a);
b = toPositive(b);
int re = 0;
while (b!=0) {
// 假如b的最后一位为1 不就是奇偶的判断嘛 上面骚操作提过了 别问 问就是看上面
if((b&1) == 1){
// 相加
re = add(re, a);
}
b = (b>>1);
a = (a<<1);
}
// 假如结果是负数
if (flag)
re = adverse(re);
return re;
}
(四)除法
除法运算很容易想到可以转换成减法运算,就是不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。
这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。
实现上与乘法第一种思路大同小异,但是效率低。
要想提高效率,可以参考乘法实现的第二种思路就是列竖式。这里就只提供第二种思路咯
int divide(int a,int b){
boolean flag = true;
// 如果相除为正数,flag为false
if (getsign(a) == getsign(b))
flag = false;
// 将a取正数
a = toPositive(a);
b = toPositive(b);
int re = 0;
int i = 31;
while(i>=0){
// 如果够减
// 不用(b<<i)<a是为了防止溢出
if((a>>i)>=b){
// re代表结果
re = add(re, 1<<i);
a = subtract(a, (b<<i));
}
// i减一
i = subtract(i, 1);
}
// 假如结果是负数
if (flag)
re = adverse(re);
return re;
}
以上是关于剑指Offer对答如流系列 - 二进制中 1 的个数的主要内容,如果未能解决你的问题,请参考以下文章