《剑指offer》:[14]位运算以及求二进制中1的个数

   位运算是把数字用二进制表示之后,对每一位上的0或者1的运算。二进制及其位运算是现代计算机学科的基石,很多底层的技术都离不开位运。
   下面主要介绍几种常见的位运算
1、按位与运算符(&)
参加运算的两个数据,按二进制位进行“与”运算。
运算规则:两位同“1”为“1”,否则为0;
          即: 0&0=0;   0&1=0;    1&0=0;     1&1=1;
例如:7&13  即 0000 0111 & 
                          0000 1101 = 0000 0101   因此,3&5的值得5。
注意:负数按补码形式参加按位与运算。
“与运算”的作用:
(1)清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
(2)取一个数中的指定位的值
    方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。
 例:设X=10101110,
    取X的低4位,用 X & 0000 1111 = 0000 1110 即可得到;
    还可用来取X的2、4、6位。
2、按位或运算符(|)
参加运算的两个对象,按二进制位进行“或”运算。
运算规则 :参加运算的两个对象只要有一个为1,其值为1。
       即:0|0=0;   0|1=1;   1|0=1;    1|1=1;
例如:7|13 即 0000 0111 | 
                       0000 1101 = 0000 1111   因此,7|13的值得15。 
注意:负数按补码形式参加按位或运算。
“或运算”的作用:
(1)常用来对一个数据的某些位置1。
方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。
例:将X=10100000的低4位置1 ,用 X | 0000 1111 = 1010 1111即可得到。
3、异或运算符(^)
参加运算的两个数据,按二进制位进行“异或”运算。
运算规则:参加运算的两个对象,相同为0,不同为1。通俗讲:如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
    即:0^0=0;   0^1=1;   1^0=1;   1^1=0;
  例如:7|13 即 0000 0111 | 
                         0000 1101 = 0000 1010   因此,7|13的值得10。 
“异或运算”的作用:
(1)使特定位翻转找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。
     例:X=10101110,使X低4位翻转,用X ^ 0000 1111 = 1010 0001即可得到。
(2)与0相异或,保留原值 ,X ^ 0000 0000 = 1010 1110。
    从上面的例题可以清楚的看到这一点。
4、取反运算符(~)
参加运算的一个数据,按二进制位进行“取反”运算。
运算规则:对一个二进制数按位取反,即将0变1,1变0。
         即:~1=0;   ~0=1;
    例如:~7 即 ~0000 0111=1111 1000,因此,~7=248;
使一个数的最低位为零,可以表示为:a&~1。
~1的值为1111111111111110,再按“与”运算,最低位一定为0。因为“~”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。
5、左移运算符(<<)
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
例:a = a << 2 将a的二进制位左移2位,右补0,
7<<2  0000 0111<<2  得: 0001 1100 7<<2=28;
左移1位后a = a * 2; 
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
6、右移运算符(>>)
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
操作数每右移一位,相当于该数除以2。
例如:a = a >> 2 将a的二进制位右移2位,
正数:+7 7>>2   0000 0111>>2  7>>2=1;
负数:-7 -7>>2  1000 0111>>2 -7>>2=1110 0001=225;
左补0 or 补1 得看被移数是正还是负。
7、无符号右移运算符(>>>)
>>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。
例如:var temp = -14 >>> 2
变量 temp 的值为 -14 :                 (即二进制的 11111111 11111111 11111111 11110010),
向右移两位后等于 1073741820    (即二进制的 00111111 11111111 11111111 11111100)。
8、复合赋值运算符
位运算符与赋值运算符结合,组成新的复合赋值运算符,它们是:
&=    例:a &= b        相当于a=a & b
|=    例:a |= b        相当于a=a | b
>>=   例:a >>= b       相当于a=a >> b
<<= 例:a <<= b       相当于a=a << b
^=   例:a ^= b       相当于a=a ^ b
运算规则:和前面讲的复合赋值运算符的运算规则相似。
9、不同长度的数据进行位运算
如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。
以“与”运算为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与”运算,右端对齐后,左边不足的位依下面三种情况补足,
(1)如果整型数据为正数,左边补16个0。
(2)如果整型数据为负数,左边补16个1。
(3)如果整形数据为无符号数,左边也补16个0。
如:long a=123;int b=1;计算a & b。
a 0000 0000 0111 1011 &
b 0000 0000 0000 0001  
= 0000 0000 0000 0001
如:long a=123;int b=-1;计算a & b。
a 0000 0000 0111 1011 &
b 1111 1111 1111 1111
= 0000 0000 0111 1011
如:long a=123;unsigned int b=1;计算a & b。
a 0000 0000 0111 1011
b 0000 0000 0000 0001
= 0000 0000 0000 0001
问题一:二进制中1的个数。
请实现一个函数,输入一个正数,输出该二进制表示中1的个数。例如把9表示成二进制是1001,有两位是1.因此如果输入9,该函数输出2.
解法一:
int NumFind1(int number)
{
	int count=0;
	while(number)
	{
		if(number&1)
			count++;
		number>>=1;
	}
	return count;
}
    但是此解法有一个问题:因为我们知道正数的移位没有问题,但是如果是负数呢?按照上面的规则6我们知道如果是负数将会陷入无穷无尽的循环中,因为移位后左边补的是1而不是0。
   注意的是:右移位相当于除以2,但是效率是不一样的,除法比移位的效率要低得多,所以编程中常用移位代替除法。
    基于以上可能陷入循环的缺点,我们将代码改进为左移,这样避免了负数的死循环,因为负数的左移就是在右边补0,但是如果是右移,最后则可能变成OXFFFFFFFF,即:-1。
如下:
int NumFind2(int number)
{
	int count=0;
	unsigned int flag=1;
	while(flag)
	{
		if(number&flag)
			count++;
		flag<<=1;
	}
	return count;
}
  但是这个解法主要存在的问题是:程序循环的次数是二进制的位数,如果是32位,则需要循环32次。下面对其继续改进,正数中有几个1就循环几次。
  主要思想是:对于一个整数减去1后,我们会发现,对于二进制来说主要是将最右边1变成了0,或者是将1变成了0;总结起来就是把一个整数减去1,再和原来整数做与运算,会把该整数最右边一个1变成0,总是会减少一个1,有多少个1就可以进行多少次循环。
具体代码实现如下:
int NumFind3(int number)
{
	int count=0;
	while(number)
	{
		count++;
		number=(number-1)&number;
	}
	return count;
}
问题二:用一条语句判断一个整数是不是2的整数次方。
分析:一个整数如果是2的整数次方,那么它的二进制表示中有且仅有一位是1,而其他所有位都为0。根据前面的分析,把这个整数减去1再和原整数相&,这个整数中唯一的1就会变为0。
return number&(number-1)?false:true;
问题三:输入两个整数m和n,计算需要改变m的二进制中的多少位才能得到n。
分析:需要改变多少位,那么我们需要改变其中不同位置1的个数。可以用两步就可以解决:
第一步:求这两个数的异或;
第二步:统计异或结果的1的个数,就是需要改变的位数。
例如:8和3
8:0000 1000 ^
3:0000 0011
这样的话就需要改变3位即可。

小结:把一个整数减去1之后再和原来的整数做位的&运算,相当于把原整数的二进制的的最右边的1变成了0,很多二进制的问题都可以用这个思路来解决。

问题四:int和unsigned int相加疑问
unsigned int a = 20;
         int b = -80;
输出a+b结果很大,但是如果b=-10,结果就是10,
分析:
第一点:int 和unsigned int 运算时,由于隐式类型规则, int型数据会转成unsigned
第二点:你print的时候,要看你是用的%d[即有符号数十进制],还是%u[无符号数]
第三点:数值是用补码 表示的而且也是用补码运算的, 当 a = 20,b=-10时候,则
b的补码是1111 1111 1111 1111 1111 1111 1111 0110
a的补码是0000 0000 0000 0000 0000 0000 0001 0100
     1111 1111 1111 1111 1111 1111 1111 0110
 +   0000 0000 0000 0000 0000 0000 0001 0100
--------------------------------------------------
     0000 0000 0000 0000 0000 0000 0000 1010
也就是说b的最高位的的符号位变成0了.我们可以看见,加20后溢出了,最高位没有了,因为无符号和有符号运算是按无符号来运算的。
当你 a =20, b =-80的时候,原理一样,这个结果按带符号数解释:printf("%d",a+b)的结果是-60;
而printf("%u",a+b)的结果是一个很大的数,按无符号数解释就是2^32-70(32位系统中)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值