位运算的奇技淫巧-位运算分治

对一个32 位无符号整数,如何颠倒它的二进制位?

一个朴素的办法是,从低位到高位,逐位使其二进制数就位。

uint32_t reserveBits(uint32_t n) {
    uint32_t ret = 0;
    for (int i = 0; i < 32; i++) {
        ret |= ((n >> i) & 1) << (31 - i);
    }

    return ret;
}

对第i位,使用(n >> i) & 1取得其二进制数,并且放到ret的第31-i位上。

不过这个算法的效率不高,对于任何数,for循环总会执行32次,我们对其改进。

uint32_t reserveBits(uint32_t n) {
    uint32_t ret = 0;
    for (int i = 0; i < 32 && n > 0; i++) {
        ret |= (n & 1) << (31 - i);
        n >>= 1;
    }

    return ret;
}

n很小的时候,只要执行几次循环n就会变成0,提高了效率。

或许我们可以用C++自带的bitset,它可以像数组一样单独操作数字的每个二进制位(记得包含头文件#include <bitset>),它的for循环总会执行16次。

uint32_t reverseBits(uint32_t n) {
    bitset<32> bs(n);
    char temp;
    for (int i = 0; i < 16; i++) {
        temp = bs[i];
        bs[i]=bs[32 - i - 1];
        bs[32 - i - 1] = temp;
    }
    return (uint32_t)bs.to_ulong();
}

如果多次调用这个函数,你该如何优化?

答案是,使用位运算分治。

uint32_t reserveBits(uint32_t n) {
    n = (n & 0x55555555) << 1 | ((n >> 1) & 0x55555555);
    n = (n & 0x33333333) << 2 | ((n >> 2) & 0x33333333);
    n = (n & 0x0f0f0f0f) << 4 | ((n >> 4) & 0x0f0f0f0f);
    n = (n & 0x00ff00ff) << 8 | ((n >> 8) & 0x00ff00ff);

    return n << 16 | n >> 16;
}

上述代码共进行5步,对每位二进制用序号排列,我以表格的形式展示其翻转过程。

12345678910111213141516
17181920212223242526272829303132
21436587109121114131615
18172019222124232625282730293231
43218765121110916151413
20191817242322212827262532313029
87654321161514131211109
24232221201918173231302928272625
16151413121110987654321
32313029282726252423222120191817
32313029282726252423222120191817
16151413121110987654321

对于位与和位或运算,我们要知道(x是二进制位):x & 0 = 0x & 1 = x,即&是清0。x | 0 = xx | 1 = 1,即|是置1。

接下来依次解析每条语句:

  • n = (n & 0x55555555) << 1 | ((n >> 1) & 0x55555555);

0x55555555变成二进制是0101 0101 0101 ...,所以n & 0x55555555是取其奇数位,(n & 0x55555555) << 1是把其奇数位的二进制数放在偶数位上(原偶数位被清零)。n >> 1是把偶数位移到奇数位上,(n >> 1) & 0x55555555是把其偶数位的二进制数放在奇数位上(原奇数位被清零),最后再合并。于是这一条代码就是将奇偶位的二进制数交换。

  • n = (n & 0x33333333) << 2 | ((n >> 2) & 0x33333333);

0x33333333变成二进制是0011 0011 0011 ...,这一条就是隔2位交换二进制数(上一条也可看成隔1位交换二进制数)

  • n = (n & 0x0f0f0f0f) << 4 | ((n >> 4) & 0x0f0f0f0f);

0x0f0f0f0f变成二进制是0000 1111 0000 1111 ...,这一条就是隔4位交换二进制数。

  • n = (n & 0x00ff00ff) << 8 | ((n >> 8) & 0x00ff00ff);

0x00ff00ff变成二进制是0000 0000 1111 1111 ...,这一条就是隔8位交换二进制数。

  • return n << 16 | n >> 16;
    最后一条,隔16位交换二进制数。

从自顶向上的角度看,我们把整个序列的翻转分成了:隔1位交换二进制数、隔2位交换二进制数、隔4位交换二进制数、隔8位交换二进制数、隔16位交换二进制数五个子问题,属于分治法思想。

从自底向上的角度看,这个翻转方法实际上是元素被看做完全逆序的二路归并排序,每次归并的时候都要将归并的序列交换位置。

对一个32 位无符号整数,如何计算它二进制位中“1”的个数?

一个朴素的方法是,让这个数&自己-1的结果,这样可以每次去除它二进制位中位数最低的一个“1”。

int hammingWeight(uint32_t n) {
        int ret = 0;
        while(n){
            n & = n - 1;
            ret++;
        }
        return ret;
    }

但看懂了之前解析的位运算分治后,我们可以使用该方法解决。

uint32_t getNum(uint32_t n) {
    n = (n & 0x55555555) + ((n >> 1) & 0x55555555);
    n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
    n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);
    n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);
    n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);

    return n;
}

不能说有些相似,只能说是一模一样。我们倒着看,传入了一个数,对这个数进行处理,最后输出。也就是说,这个数处理之后就是其二进制位中“1”的个数,对吧?

它其实实现的思路就是把“1”尽量移到低位(对应位置都为1的时候会出现合并的情况),在进行二路归并的时候(这里把排在前面的序列称为第一路),使用新的交换方式,把第一路的序列按位+到第二路的序列中去。所以,整个过程完全也可以看成一个元素完全逆序的二路归并排序

最后用两个简短的序列简单展示一下该代码的思路.
序列1:0110(隔1位“交换”)->0101(隔2位“交换”)->0010,输出“2”
序列2:1111 1111(隔1位“交换”)->1010 1010(隔2位“交换”)->0100 0100(隔4位“交换”)->0000 1000,输出“8”

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值