2的n次方都有一个特性,在2进制中,只有1个1。
比如 1,10,100,1000,分别对应1,2,4,8。
16 | 8 | 4 | 2 | 1 |
0 | 0 | 0 | 0 | 1 |
16 | 8 | 4 | 2 | 1 |
0 | 0 | 0 | 1 | 0 |
16 | 8 | 4 | 2 | 1 |
0 | 0 | 1 | 0 | 0 |
以此类推,假如1个2进制数中,只有1个1,那就是2的n次方。
那知道了这个特性,又怎么确定呢?
我们来看看有哪些位运算能帮到我们
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1,否则为0 |
| | 或 | 两个位都为0时,结果才为0,否则为1 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 |
各二进位全部右移若干位, 对无符号数,高位补0, 有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
一、&运算
两个位都为1时,结果才为1,否则为0
例1 例2
001001 (9) 001111 (15)
& 011111 (31) & 000000 (0)
001001 = 9 000000 = 0
例3 例4
101001 (41) 101110 (46)
& 010111 (23) & 001100 (12)
000001 = 1 001100 = 12
二、|运算
两个位都为0时,结果才为0,否则为1
例1 例2
001001 (9) 001111 (15)
| 011111 (31) | 000000 (0)
011111 = 31 001111 = 15
例3 例4
101001 (41) 101110 (46)
| 010111 (23) | 001100 (12)
000001 = 1 001100 = 12
^运算
两个位相同为0,相异为1
例1 例2
001001 (9) 001111 (15)
^ 011111 (31) ^ 000000 (0)
010110 = 22 001111 = 15
例3 例4
101001 (41) 101110 (46)
^ 010111 (23) ^ 001100 (12)
111110 = 62 100010 = 34
~运算
取反,0变1,1变0
计算机存储数据是以二进制的补码形式来存储的,正数的补码是它本身(如:有二进制00000110,因为他的第一位是0,即代表是正数,反码、补码就是它本身);
负数的补码是它的反码加1,也就是你说的‘取反加一’(如:有二进制10000110,第一位是1,代表它是负数,反码就是每一位都取反,为01111001,所以,补码就是01111010)。
例1 例2
符号位 符号位
~ 0 0001001 (+9) ~ 1 0010000 (-15的补码)
1 0001010 = -10 0 0001110 = +14
<<运算
数学意义:在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。
2 << 2 = 8 3 << 2 = 12
10 (2) -> 1000 (8) 11 (3) -> 1100 (12)
>>运算
>>,有符号右移位,将运算数的二进制整体右移指定位数,整数高位用0补齐,负数高位用1补齐(保持负数符号不变)。
数学意义:在数字没有溢出的前提下,对于正数和负数,右移一位代表除以2的1次方,右移N位代表除以2的N次方。
2 >> 1 = 1 9 >> 2 = 2
10 (2) -> 1 (1) 1001 (9) -> 10 (2)
分析题干判断2的n次方,肯定不能使用>>和<<对该数直接操作,一旦溢出,结果就不准确了。
刚才也说了2的n次方,只有一个1。那么这个数减去一个1,二进制就全是1。
比如:1000 - 1 = 111 100 - 1 = 11 10000 - 1 = 1111
利用好这个特性,从&看,两个位都为1时,结果才为1,否则为0
1000 100
& 0111 & 011
———— ————
0 0
那么我们找到了答案,设要求的数为a,那么 a & (a-1) 如果等于0,那么a一定是2的n次方
/// <summary>
/// 判断一个数是否为2的n次方
/// </summary>
/// <param name="n">欲求的数</param>
/// <returns>true=是,false=不是</returns>
public static bool is2pow(int n) {
return (n & (n - 1)) == 0;
}
举一反三,如果要使用|来求解呢,我们可以发现结果全是1,那联想到刚才的^运算,是不是也全是1。
(两个位都为0时,结果才为0,否则为1)
1000 100
| 0111 | 011
———— ————
1111 111
(两个位相同为0,相异为1)
1000 100
^ 0111 ^ 011
———— ————
1111 111
所以我们这么看 n |(n-1) == n^(n-1)成立的话,是不是也可以判断该数为2的n次方呢,虽然看起来效率要低得多。
/// <summary>
/// 判断一个数是否为2的n次方
/// </summary>
/// <param name="n">欲求的数</param>
/// <returns>true=是,false=不是</returns>
public static bool is2pow_2(int n)
{
return (n | (n - 1)) == (n ^ ( n - 1)) ;
}
验证猜想
哈哈,过程是辛苦的,结果是让人欣慰的。
这就完啦?再来想一想还有没有其他方法呢?
回到刚才第一个方法,a & (a-1) == 0 可以判断,其实还可以简化一下,初中数学,左右同时加a,减a,得出 n == (n & -n)
于是咱们又得到一条新的公式,这个省去了0,比第一条效率高。
/// <summary>
/// 判断一个数是否为2的n次方
/// </summary>
/// <param name="n">欲求的数</param>
/// <returns>true=是,false=不是</returns>
public static bool is2pow_3(int n)
{
return n == (n & -n);
}
看到了这个结果,我们发现了-n,好像想到了啥,!!!刚才的取反符号!
来吧,基于此,又来一条效率不怎么样的公式
/// <summary>
/// 判断一个数是否为2的n次方
/// </summary>
/// <param name="n">欲求的数</param>
/// <returns>true=是,false=不是</returns>
public static bool is2pow_4(int n)
{
return (n & (~n + 1)) == n;
}
再来验证,以上公式3和公式4是否正确。
验证猜想
红红火火恍恍惚惚~~~~
结尾
还有位移运算,虽然刚才说不能直接对数进行左右位移运算,但咱可以新建一个数,让它左移,每左移动一次就对该数进行判断,相等就说明是2的次方。
其实抛开位运算,数学的方法可以一直除以2,如果结果不等于1,而且期间余数不等于0就说明不是2的n次方,还有很多方法,比如就计算成二进制字符串,再看看是不是只有一个1。又或者int类型中,二进制的长度是有限的,2的次方就那几个,咱给做成数组,一个一个判断。等等等等。