位运算是把数字用二进制表示之后,对每一位进行0或者1的运算。二进制中的数字每个都是0或者1,没有其它的数字。进制数除了二进制,十进制以外,还有我们比较熟悉的八进制、十六进制和三十六进制。
二进制中的位运算共有五种:与(&)、或(|)、异或(^)、左移(<<)、右移(>>)五种。
首先我们来看一下前三种的运算方式:
与(&):只有两个二进制数对应位上的数字全为1,得到的最终二进制数的那个位置上的数才为1。如下图所示:
或(|):两个二进制数对应位上的数字只要有一个为1,最终得到结果的二进制数对应位上的数字就为1。如下图所示:
异或(^):只要两个二进制数对应位上的数字不相同得到最终结果的二进制数对应位上的数字就为1。如下图所示:
接下来我们看一下左移(<<)和右移(>>)运算:
左移(<<):
左移运算符m<<n表示把m左移n位,在左移n位的时候,最左边的n位将被丢弃,同时在最右边补上n个0。如下图所示:
右移(>>):
右移运算符m>>n表示把m右移n位。在右移的时候,最右边的n位将被丢弃掉。但是在处理最左边的数的时候相比于左移就有点麻烦了,需要分为两种情况讨论:
(1)如果数字是一个无符号数值,则用0填补最左边的n位;
(2)如果数字是一个有符号数值,则用数字的符号位填补最左边的n位。也就是如果说原来的数字是一个正数,右移后在最左边补n个0;如果原来的数字是一个负数,则右移后在最左边补n个1。下图是对两个8位有符号数进行右移的示例:
介绍完五种位运算之后,我们来看一道面试官常考的面试题,题目是这样说的:
请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如,把9表示成二进制是1001,有2为是1。因此如果输入9,该函数应该输出2。
一般很多人看到这个题目都想到的是用位运算的右移解决,然后很快就会写出如下的代码:
int NumberOf1(int n)
{
int count=0;//统计1的个数
while(n!=0)
{
if(n&1)//n&1的作用是判断二进制数中最右边的一位是否为1
{
count++;//如果是1,计数器count++
}
n>>=1;//>>的作用就是上面说到的丢弃最右边的一位数
}
return count;
}
首先运行这个程序我们可以发现测试用例为正数和0的时候这个程序是没有任何问题的,如下图:
但是当我们的测试用例是一个负数的时候,我们运行这个程序会发现它进入了死循环了,比如我们输入-1,它的结果是下图这样的:
那么出现这种情况的原因就是对于有符号的整数并且是负数进行右移的时候,丢弃最右边的数字,给最左边补的符号位,也就是补1,这样的话可想而知程序会陷入死循环。
如果这个题目中要求输入的是一个无符号整数,那么上面缩写的这个程序就没有任何问题,就需要把传入参数的类型改为无符号整型,代码如下:
int NumberOf1(unsigned int n)
{
int count=0;//统计1的个数
while(n!=0)
{
if(n&1)//n&1的作用是判断二进制数中最右边的一位是否为1
{
count++;//如果是1,计数器count++
}
n>>=1;//>>的作用就是上面说到的丢弃最右边的一位数
}
return count;
}
下面是这个程序的运行结果:
但若题目要求我们输入的数据是一个整型数据,为了避免负数测试的时候陷入死循环,我们得完善第一次写的代码。思路是这样的:
首先判断n的最左边第一位是否为1,接着判断n的
最左边第二位是否为1…一直判断到n的最右边那位是否为1。如下图这样:
基于这种想法我们可以写出下面代码:
int NumberOf1(int n)
{
int count=0;//统计1的个数
unsigned int flag=1;
while(flag)
{
if(n&flag)
{
count++;
}
flag<<=1;
}
return count;
}
运行结果如下:
这种解法中,循环的次数为二进制的位数。下面再介绍一种算法,整数中有几个1就只需要循环几次。
首先我们来看一下n&n-1这个表达式的作用:
从上面几个例子中我们可以发现,n&n-1这个表达式可以把n最低位的1变为0,借助这个我们可以用下面这个程序来解决这道题目:
int NumberOf1(int n)
{
int count=0;//统计1的个数
while(n)
{
count++;
n&=n-1;
}
return count;
}
这里还有一个小小的细节,就是把一个整数右移一位和把一个整数除以2在数学上是等价的,在实际编程中尽量不要使用除以2的办法,因为除法的效率比移位运算的效率要低很多。
以上就是介绍的位运算的内容。