使用位运算计算一个二进制数中值为1的个数

使用位运算计算一个二进制数中值为1的个数

二进制

电脑采用二进制的原因其实很简单!

因为二进制的0和1正好对应了计算机中逻辑电路开和关两种状态

所以为了方便表示,就用1代表高电平,0代表低电平

(不止开关,很多元器件的状态大多数也只有两种)

这样更加容易表示,运算规则也比较简单(计算机比较笨!)

传输数据是也可以只用电压高低就能表示数据

(网线就是八根铜丝儿电压高高低低的传输数据)


十六进制

十六进制的存在更多是为了服务二进制

毕竟一个正常的二进制数都老长老长了

(一个字节有8位二进制数,但是转化成十六进制就只有2位)

(0xFF和0B11111111的差别)

为了方便运算,而且不让程序员看0和1看的头晕眼花,所以有了十六进制

与十进制不同的是,十六进制由于基数(就是逢多少进一)的问题

所以每四位二进制数对应一位十六进制数

所以二进制和十六进制间的转换非常容易

2to16:从小数点开始,分别向左、右按四位一组转换成对应的十六进制数字字符,最后不满四位的,补0

16to2:每位十六进制转化成四位二进制即可


位运算

因为计算机的本质还是各种电路集成起来的

想要完成复杂的运算首先要学会1+1等于几

那么计算机中最简单的小盒子可以完成的运算我们就叫位运算(门电路)

按位或 |

|两边只要有1结果就为1

0|0=0, 0|1=1, 1|0=1, 1|1=1

按位与&

&两边只要有0结果就为0

0&0=0, 0&1=0, 1&0=0, 1&1=1

按位异或^

^两边不同则为1,相同则为0

0&0=0, 0&1=1, 1&0=1, 1&1=0

所有的复杂运算都是通过位运算完成的

开始我们提到的1+1(二进制中)由于涉及到进位可以看成


​	     1

+            1

----------------

 (1&1)    (1^1)

也就是10

这样我们所有一位的二进制加法就掌握了(好耶!)

相比于+、-、*、/位运算速度更加的快(不是一个量级)

这是我们用位运算的主要原因

x>>1和x/2都是除以二的意思,但是速度差了好几倍

而且位运算也有一些神奇的用法(这里不详细说了)


计算一个二进制数中值为1的个数

先贴一下源码

    x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
    x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
    x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
    x = (x & 0x0000FFFF) + ((x >>16) & 0x0000FFFF); 

终于到正题了,呼呼~

想要算值为1的位数的数量有很多办法

(字符串处理啊,不断对2取余再除以2啊)

但是位运算是坠快滴!

首先!

我们的问题是数出二进制中值为1的个数

这个问题其实数据已经给我们答案了

因为这个二进制数上每一位记录的就是这一位上1的个数

(这当然是废话,1就是有1个1,0就是没有呗)

但是!

在分支思想中,我们已经完成了对小问题的解答!

(分治就是把一个大问题拆分成很多小问题,再把小问题拆成更小的问题)

我们要做的是求总体的数量,但是现在还存储在每一位上,我们要做的就是把他们加起来。

一个int型有四个字节

转换成二进制有32位(太长啦)

我们这里就拿两个字节做演示

假设

x=0B 1011 1101 0011 1100

(它的每一位上都是数字1的个数,废话!)

但是通过x = (x & 0x5555) + ((x >> 1) & 0x5555)

计算过程:

x & 0x5555:
​        1011 1101 0011 1100

&       0101 0101 0101 0101

\---------------------------------------

​        0001 0101 0001 0100



 (x >> 1) & 0x5555 :

​          101 1110 1001 1110

&        0101 0101 0101 0101

---------------------------------------

​         0101 0100 0001 0100

我们可以看到位运算的好处是不会产生进位,所以看着很庞大的数据其实每两位所做的运算都是一样的。

对0x5555按位与留下了奇数位上的数字

而右移一位再对0x5555按位与留下了偶数位上的数字(右移了一位,所以结果也显示在奇数位)

(x & 0x5555) + ((x >> 1) & 0x5555):

​        0001 0101 0001 0100

+       0101 0100 0001 0100

---------------------------------------

​        0110 1001 0010 1000

那么这步的运算有什么意义呢

刚刚我们每一位放的1的个数被拿出来了!

加号左边奇数位上的数字保存着奇数位1的个数

而加号右边奇数位上的数字保存着偶数位1的个数

这样一加每两位二进制数存储的就是这两位上1的个数(质的飞跃!)

与原数对比也可以看到

10 11 11 01 00 11 11 00(原来)

01 10 10 01 00 10 10 00(计算后)
1 2 2 1 0 2 2 0(每两位转换成十进制)

计算后每两位代表原数分别存储着1,2,2,1,0,2,2,0个1
即相邻 2 位中 1 的个数

(因为数据会越来越小所以不用担心放不下)

那么!

我们需要做的就是重复这么做,直到整个数代表每一位上1的个数

第二步

x=0110 1001 0010 1000(现在)

x = (x & 0x3333) + ((x >> 2) & 0x3333)

计算过程:

x & 0x3333:

​        0110 1001 0010 1000

&       0011 0011 0011 0011 

\---------------------------------------

​        0010 0001 0010 0000



 (x >> 2) & 0x3333:

​        01 1010 0100 1010

&     0011 0011 0011 0011 

---------------------------------------

​      0001 0010 0000 0010



(x & 0x3333) + ((x >> 2) & 0x3333):

​        0010 0001 0010 0000

+       0001 0010 0000 0010

---------------------------------------

​        0011 0011 0010 0010

即计算相邻 4 位中 1 的个数

与原数对比也可以看到

0110 1001 0010 1000(原来)

0011 0011 0010 0010(计算后)
3 3 2 2(每四位转换成十进制)

计算后每四位代表原数分别存储着3,3,2,2个1

同理后面的计算也是这样

原数的变化过程为

最后
x=0B0000000000001010=10

即共有10个1,4个字节的也同理
结束!

posted @ 2022-01-10 17:58  毕业以后  阅读(270)  评论(0)    收藏  举报