问题一、判断二进制位是否只有一个1
这个问题也就是求判断一个数是否是2的整数次幂。
对于这个问题,看起来的第一感觉就是循环判断每一位,当然,这也是最简单的算法。
bool flag = false;bool result = true;while(number > 0){ if(number & 1) if(!flag) flag = true; else result = false; //该数二进制位有多个1 number >>= 1;}
显然,上述算法是O(n)的,n为二进制位数,怎么把算法降到O(1)呢,这就要用的巧妙的位运算了。直接上代码:
bool result = number & (number - 1);
本来写了说明,但是这么简单的相信都看得懂,所以删去了。。。。
问题二、求二进制位里有多少个1
相对于第一个问题,这个问题就稍稍有点复杂了,当然,是针对低复杂度的解法。
最水的方法当然是对每一位就行1的判断,只需要稍微改一下问题一的解法,就得到了:
int sum = 0;while(number > 0){ sum += (number & 1); number >>= 1;}
显然,这个方法也是O(n)的,但是,当我们见识过问题一的巧妙解法后,可以尝试对这个O(n)的解法做一下优化:
int sum = 0;while(number > 0){ number &= (number - 1); sum ++;}
上面已经说过,number & (number - 1)每次消除最右边的那个1,所有,一直消下去,当number等于0时,进行了多少次运算,也就有多少个1.
这个算法的复杂度当然不及O(n)但是,当number全为1的最坏情况下,也就达到O(n)了,那么,还有比这个更优的解法吗?当然,还可以采取空间换时间的方法。
例如,利用arr[] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4}这个数组,每次判断4个二进制位,然后通过数组找到对于的1的个数。例如判断10100011,即A3h,arr[10] + arr[3] 就是4。
这一类通过空间换时间的方法,复杂度根据判断的位数而定,但是就复杂度而言,还是属于O(n)范畴的。还能再降低复杂度吗?答案是能:
例如对于16位的数字:
number = (number & 0x5555) + ((number >> 1) & 0x5555);number = (number & 0x3333) + ((number >> 2) & 0x3333);number = (number & 0x0F0F) + ((number >> 4) & 0x0F0F);number = (number & 0x00FF) + ((number >> 8) & 0x00FF);
5次运算后,number的值就是原number二进制位中1的个数。
先举个例子吧,number = abcd 1111 1111 1111;(a、b、c、d = 0 or 1)
在这里,我们这样看待上面的每一位数:每一个数代表该位上有几个1。那么,1代表该位上有1个1,0代表有0个1.
number & 0x5555 = 0b0d 0101 0101 0101;
(number >> 1) & 0x5555 = 0a0c 0101 0101 0101;
现在a代表第一位的1的个数,b代表第二位1的个数,两者相加,则a+b的和代表了前两位上1的个数,c+d代表了第三、四位上1的个数。例如a = b = 1,则a+b=10 即 2,所以,第一次运算后,number= a+b c+d 1010 1010 1010 ,a+b,c+d各占两个二进制位。第二次运算:
number & 0x3333 = 00c+d 0010 0010 0010;
(number >> 1) & 0x3333= 00a+b 0010 0010 0010;
同理,两者相加,a+b+c+d代表了前4位上1的个数,新的number = a+b+c+d 0100 0100 0100;后面3个0100=4,即原来这4位上有4个1。再往下去,就是a+b+c+d + 0100得到前8位的1的个数,以此类推,就可以得到number所有位的1的个数了。
可以看出,该算法的复杂度是O(logn)的,可以说是最少的了。
问题三、求二进制最右边的1是第几位
现在,有了前两个问题的铺垫,第三个问题也就没有那么复杂了。
首先,小学生解法略。
接下来,针对32位整数,采用空间换时间的方法。声明一个数组
int arr[32] = { 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9};
那么,这个问题的解法就是:
int answer = arr[ ((number & -number) * 0x077CB531 ) >> 27 ];
咋一看,几个数莫名其妙,下面就来解释一下。
number & -number 保留最右边的一个1,将其他位的1置0,这样以后,就只有32种结果了。即2^i (i = 0,1....31),剩下的问题,就是如何建立2^i (i = 0,1....31) 和 {1,2.....32}这两个集合的双射(一一映射和满射)关系了。将这32个上述结果 * 0x077CB531后,得到的数,最高5个二进制位没有完全一样的,正好利用这一点,建立满射关系(其实就是hash)。得到的数右移27位,就是为了获得最高的5位。5位二进制数也就是0~31的范围,代入数组,就能获得最终答案。
0x077CB531 和 数组怎么得到的呢? 可以通过遍历1 ~ 2^32-1,测试每一个数,与2^i (i = 0,1....31)相乘后,看得到的32个数,最高5个二进制位组成的数a是不是唯一的。如果是唯一的,再通过a和2^i (i = 0,1....31)建立hash数组。int32范围内,这样的数可以找到4096个。