计算变量中有多少个bit位是1的方法

在工作中经常遇到如下问题:任意给定一个32位无符号整数value,求value的二进制表示中1的个数,比如value = 0x05(0b0101)时,返回2,value =  0x8e(0b1000 1110)时,返回4。下面给出几种常用的解法,在文章的最后给出这几种方法的对比结果。

方法一:基础法

基础法最容易理解,直接遍历每一位,代码如下,两种方法的思路是一样的,只是第一种方法必须循环32次,而第二种方法可能会提前结束循环,效率比第一种稍高。最后的测试,是以for循环的这种方法计算的。

uint8_t count_bits_1(uint32_t value)
{
    uint8_t count = 0,i = 0;
    
    for(i = 0; i < 32 ; i++)
    {
        if(value & 0x01)
        {
            count ++;
        }
        value >>= 1;
    }
    return count;
}

uint8_t count_bits_1(uint32_t value)
{
    uint8_t count = 0,i = 0;
    
    while(value)
    {
        if(value & 0x01)
        {
            count ++;
        }   
        value >>= 1;    
    }
    return count;
}

方法二:快速法

这种方法速度比较快,只与value中1的个数有关。如果value的二进制表示中有n个1,那么这个方法只需要循环n次即可。其原理是不断清除value的二进制表示中最右边的1,同时累加计数器,直至value为0,代码如下。

例如当value = 0x0A(0b1010)时,第一次进入while循环后,value = 0b1010 & 0b1001 = 0xb1000,count = 1;第二次进入while循环后,value = 0b1000 & 0b0111 = 0,count = 2;至此value已经为0,函数返回2。

uint8_t count_bits_2(uint32_t value)
{
    uint8_t count = 0;
 
    while(value)
    {
        value &= value - 1;
        count ++;
    }
    return count;
}

方法三:查表法

查表法原理比较简单,直接根据value的值来查询该数据有bit位是置1的,其思想就是空间换时间。表可以是4bit的,也可以是8bit、16bit、32bit的,4bit相对32bit来说,更省空间,但需要计算的时间相对就长了,这里主要是时间和空间的权衡,例如value是uint32_t类型时,用4bit表来查需要查询8次,而用32bit表来查询只需要1次。8bit表查询法是一个较中庸的方法。

注意:如果表使用const修饰,则查询时会读FLASH,不定义为const时,表会被拷贝到RAM中,查询时会读RAM。读RAM的速度要快于读FLASH。

static uint8_t table[256] = 
{ 
    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
    4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, 
}; 

uint8_t count_bits_3(uint32_t value)
{
    uint8_t count = 0;

    count  =  table[ value & 0xff ] +
              table[( value >>8 ) & 0xff ] +
              table[( value >>16 ) & 0xff ] +
              table[( value >>24 ) & 0xff ] ;
    return count ;
}

方法四:移位法

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

    return value  ; 
}

这种方法很粗暴,咋一眼不容易看懂,实际上就是将value中的所有bit相加。每次都是将相邻的1组bit位相加,第一次累加相邻的2个bit位,第二次累加相邻的4个bit位,第三次累加相邻的8个bit位,以此类推。

为了方便讲解,我们可以先将问题简单化,假设value是个uint2_t类型的数据(uint2_t类型实际不存在,是假想的,只有2位),那么求value中有多少个置1的bit位就可以直接用下面的一行代码搞定。这应该一看就能懂了吧?就是将bit1取出来和bit0相加,相加的和就是置1的个数。

/* value为uint2_t类型时 */
value = ( value & 0x01 ) +( (value >> 1) & 0x01 );

/* 或者这样,反正0x05中只有最后2个bit位参与计算 */
value = ( value & 0x05 ) +( (value >> 1) & 0x05 );

好了,再稍微复杂点,假设value是个uint4_t类型的数据,那么代码就可以这样写:

/* value为uint4_t类型时 */
value = ( value & 0x05 ) +( (value >> 1) & 0x05 );
value = ( value & 0x03 ) +( (value >> 2) & 0x03 );

最后再以value为uint8_t类型时举例。

/* value为uint8_t类型时 */
value = ( value & 0x55 ) + ( (value >> 1)  & 0x55 ) ; 
value = ( value & 0x33 ) + ( (value >> 2)  & 0x33 ) ; 
value = ( value & 0x0f ) + ( (value >> 4)  & 0x0f ) ; 

例如当value为0x6D(0b01101101)时,结果为0+1+1+0+1+0+1+1 = 5。下图是计算流程:先把它们相邻的二进制位先相加起来,然后把相邻的两位的数加起来,最后把相邻的四位数加起来。将0x6D带入计算时,第一步得到0b01011001,第二步得到0b00100011,第三部得到0b00000101,即5。搞清楚2、4、8bit的情况后,32bit的情况就是一样的套路。另外,可以注意到在求uint2_t类型时,需要计算1次;计算uint4_t类型时,需要计算2次;计算uint8_t类型时,需要计算3次。那么我们就可以猜测计算uint32_t类型时需要计算5次,计算uint64_t类型时需要计算6次,即\log_2 n

 方法五:八进制法

uint8_t count_bits_5(uint32_t value)
{
    uint32_t temp= value - ((value >>1) & 033333333333) - ((value >>2) & 011111111111);
    return ((temp+ (temp>>3)) &030707070707) % 63;
}

先说明一点,以0开头的是8进制数,以0x开头的是十六进制数,上面代码中使用了三个8进制数。将value的二进制表示写出来,然后每3bit分成一组,求出每一组中1的个数,再表示成二进制的形式。比如value = 50,其二进制表示为110010,分组后是110和010,这两组中1的个数本别是2和3。2对应010,3对应011,所以第一行代码结束后,tmp = 010011,具体是怎么实现的呢?由于每组3bit,所以这3bit对应的十进制数都能表示为2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,这里a,b,c的值为0或1,如果为0表示对应的二进制位上是0,如果为1表示对应的二进制位上是1,所以a + b + c的值也就是4a + 2b + c的二进制数中1的个数了。举个例子,十进制数6(0110)= 4 * 1 + 2 * 1 + 0,这里a = 1, b = 1, c = 0, a + b + c = 2,所以6的二进制表示中有两个1。现在的问题是,如何得到a + b + c呢?注意位运算中,右移一位相当于除2,就利用这个性质!

4a + 2b + c 右移一位等于2a + b,4a + 2b + c 右移量位等于a,然后做减法,4a + 2b + c –(2a + b) – a = a + b + c,这就是第一行代码所作的事。

第二行代码的作用:在第一行的基础上,将tmp中相邻的两组中1的个数累加,由于累加到过程中有些组被重复加了一次,所以要舍弃这些多加的部分,这就是&030707070707的作用,又由于最终结果可能大于63。

需要注意的是,经过第一行代码后,从右侧起,每相邻的3bit只有四种可能,即000, 001, 010, 011,为啥呢?因为每3bit中1的个数最多为3。所以下面的加法中不存在进位的问题,因为3 + 3 = 6,不足8,不会产生进位。

测试

将以上5种方法放在单片机上运行,比对运行效率。单片机为潘多拉STM32L475,主频80MHz,测试方法如下。

#define TEST_NUMBER    0xFFFFFFFF

start_tick = HAL_GetTick();
for(i = 0; i < 100000; i++)
{
    count = count_bits_1(TEST_NUMBER); 
}
end_tick = HAL_GetTick();
printf("Function 1 result:%d,takes %d ms.\r\n",count,end_tick - start_tick);

对5种算法都采用以上方法进行测试,每种算法执行10万次后计算执行所需时间,最终结果如下。

测试结果(-O0优化)

 

方法1

方法2

方法3

方法4

方法5

0x00000000

465ms

22ms

59ms

42ms

45ms

0x00000001

465ms

31ms

59ms

43ms

46ms

0x0000000F465ms57ms59ms42ms46ms
0x0000001F465ms66ms59ms43ms46ms

0x11111111

465ms

92ms

59ms

42ms

55ms

0x33333333

465ms

162ms

59ms

43ms

55ms

0x77777777

465ms

232ms

59ms

43ms

56ms

0xFFFFFFFF

465ms

302ms

59ms

43ms

56ms

测试结果(-O2优化,仅做参考)

 

方法1

方法2

方法3

方法4

方法5

0x00000000

375ms20ms33ms35ms31ms

0x00000001

376ms29ms33ms35ms32ms
0x0000000F380ms55ms33ms35ms32ms
0x0000001F381ms64ms33ms35ms32ms

0x11111111

385ms90ms33ms35ms41ms

0x33333333

395ms160ms32ms35ms41ms

0x77777777

405ms230ms32ms35ms43ms

0xFFFFFFFF

415ms300ms32ms35ms43ms

结论(不考虑编译器优化)

方法1的效率最低,执行时长固定。

方法2的执行时长受计算值影响,计算值的二进制中,含1越多,执行时间越长。

方法3由于是查表法,执行时间固定,但表本身就占用空间,消耗的资源更多。

方法4效率最高,执行时间固定。当置1的bit位个数小于等于4个时,方法2效率高,超过4个时,方法4明显优于方法2。

方法5效率第二,执行时间相对固定,其效率低应该是算法中有取余运算引起的。

综上,在算法选择上优先选择方法4。但这也不是绝对的,适合自己项目才是最好的。例如我的项目中有个触摸按键模块,用bit位来表示当前有多少个键被按下,一般只有1个按键被按下,那么方法2反而是效率最高的。

  • 33
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
### 回答1: 您好,变量a中有多少个1可以通过以下代码来求解: int count_ones(uint32_t a) { int ones = 0; while (a != 0) { ones += (a & 1); a = a >> 1; } return ones; } 其中,uint32_t是无符号32位整型,通过按位与运算符&和右移运算符>>找出二进制表示中的每一个1,然后将其逐个加起来。 ### 回答2: 对于一个32位的变量a,我们可以使用位运算来统计其中1的个数。一个简单的方法是使用一个循环,对a进行32次迭代,每次检查a的最低位是否为1,如果是则计数加一,然后将a右移一位。代码示例如下: ``` int count = 0; // 初始化计数器为0 for (int i=0; i<32; i++) { if ((a & 1) == 1) { count++; } a = a >> 1; } ``` 另一种更高效的方法是使用Brian Kernighan算法,该算法利用了一个性质:将一个数减去1,会将其最右边的1及其右边的0都变成1。因此,我们可以不断将a与a-1相与操作,直到a为0,每次操作都会将a的最右边的1变为0,计数增加1。代码示例如下: ``` int count = 0; while (a != 0) { a = a & (a-1); count++; } ``` 这两种方法都可以统计32位变量a中1的个数,第一种方法逐位检查,时间复杂度为O(32),第二种方法根据二进制中1的个数直接计算,时间复杂度为O(1)。但是在实际应用中,由于编译器对位运算进行了优化,两种方法的效率差别并不大。 ### 回答3: 对于一个32位的变量a,我们可以通过遍历每一位来计算出其中有多少个1。 我们可以用一个循环来遍历32位中的每一位,从最低位到最高位。首先初始化一个变量count为0,表示1的个数。 在每一次循环中,我们可以使用位掩码操作(bit masking)来检查当前位是否为1。我们可以通过将1左移i位,得到一个只有第i位为1的数字mask。然后,我们可以使用按位与操作符&将a和mask进行位运算,如果结果不为0,则表示第i位是1。在这种情况下,我们将count加1。 最后,循环结束后,变量count的值就是a中1的个数。 以下是一个示例代码: ```python count = 0 for i in range(32): mask = 1 << i if a & mask != 0: count += 1 print("变量a中有", count, "个1.") ``` 注意,该示例代码中的变量a需要提前定义并赋值。该方法可以用于任何32位的整数。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dokin丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值