算法编程系列之计算int类型中1的个数

1,221 阅读4分钟

  刚看这道题,第一想法比较朴素,就是直接计算1的个数。每次取二进制最右边的数,如果是1,就记下。之后再右移一位,重复操作,直到全零为止。

#include <iostream>
using namespace std;

int get_one_bits(int num) {
    int cnt = 0;
    while (num != 0) {
        if (num & 1 == 1) {  //判断最右边的数是0还是1
            ++cnt;
        }
        num = num >> 1; //向右移动一位
    }
    return cnt;
}

int main()
{
    cout << get_one_bits(8);
    return 0;
}

  我们根据上面的逻辑可以稍微简化一下代码:

int get_one_bits(int num) {
    int cnt = 0;
    while (num) {
        cnt += num & 1; //判断最后一位
        num = num >> 1; //向右移动一位
    }
    return cnt;
}

  还有一种方法按采用num&(num-1),可以消除num二进制的最右侧的1。从而实现计算1的个数。这种方法的原理是什么呢?我们先看下下面的例子:

1: 0001 & 0000 = 0000
2: 0010 & 0001 = 0000
7: 0111 & 0110 = 0110
14: 1110 & 1101 = 1100

  可以看到,二进制最右边的1只存在两种情况:1.最右侧的1。2.非最右侧的1。

  对于情况1而言,如果num=xxxx xxx1,那么num-1一定是xxx0。进行"与"运算的时候,最右侧一定是1&0=0。其他位保持不变。所以能消除最右侧的1。

  对于情况2而言,如果num=xxxx x100。由于借位,那么num-1一定是xxxx x011。进行与运算,100&011一定也是0。其他位保持不变,所以消除了最右侧的1。

  那么根据这个方法,我们可以将代码优化如下:

int get_one_bits(int num) {
    int cnt = 0;
    while (num) {
        cnt ++;
        num = num & (num - 1); 
    }
    return cnt;
}

  此种方法仍然存在问题,就是随着num增大,循环的次数也不断增大。所以还有一种更好的方法。先展示下代码,再进行分析。

int get_one_bits(int num) { //先只针对8位,如果是int32,需要做些许更改,但是核心思想还是一样。
    num = (num & 0x55) + ((num >> 1) & 0x55);
    num = (num & 0x33) + ((num >> 2) & 0x33);
    num = (num & 0x0f) + ((num >> 4) & 0x0f);
    return num;
}

  我第一次看到这段代码也是一脸懵x。我们先看下代码中用到的几个数。

0x55 = 0101 0101
0x33 = 0011 0011
0x0f = 0000 1111

  这么一看,突然发现还挺有规律的。我们举一个例子,走一遍这个运算,看看会发生什么!

0x34 = 0b0011 0100
0011 0100 & 0101 0101 = 0001 0100 // 表示第三位,第五各有1。
0001 1010 & 0101 0101 = 0001 0000 // 表示第六位,有1。(因为往后移了一位)
0001 0100 + 0001 0000 = 0010 0100 // 第三四位是01,即有一个1,五六位为10,恰好是两个1。

依次类推:

0010 0100 & 0011 0011 = 0010 0000 // 表示五六位,共2个1,一二位没有1
0000 1001 & 0011 0011 = 0000 0001 // 表示七八位,没有1,三四位有1个1 (因为往后移了两位)
0010 0000 + 0001 0010 = 0010 0001 // 表示五六七八位,共2个1,一二三四位,共1个1

继续类推:

0010 0001 & 0000 1111 = 0000 0001 // 表示一二三四位,共1个1
0000 0010 & 0000 1111 = 0000 0010 // 表示五六七八位,共2个1 (因为往后移了四位) 0000 0001 + 0000 0010 = 0000 0011 // 表示一二三四五六七八位共3个1。

  最终得到结果,用图简述如下:

  如果明白了这个原理,你大概就知道才能从8位扩展到32位改怎么写代码了。

int get_one_bits(int num) {
    num = (num & 0x55555555) + ((num >> 1) & 0x55555555);
    num = (num & 0x33333333) + ((num >> 2) & 0x33333333);
    num = (num & 0x0f0f0f0f) + ((num >> 4) & 0x0f0f0f0f);
    num = (num & 0x00ff00ff) + ((num >> 8) & 0x00ff00ff);
    num = (num & 0x0000ffff) + ((num >> 16) & 0x0000ffff);
    return num;
}

  代码优化:

int get_one_bits(int num) {
    register int xx=num;
    xx=xx-((xx>>1)&0x55555555);
    xx=(xx&0x33333333)+((xx>>2)&0x33333333);
    xx=(xx+(xx>>4))&0x0f0f0f0f;
    xx=xx+(xx>>8);
    return (xx+(xx>>16)) & 0xff;
}