• 读书笔记一:求二进制数中1的个数


    问题描述:

            对于一个字节(8bit)的无符号整型变量,求其二进制表示中“1”的个数,要求算法的执行效率尽可能高效。

            在编写程序的过程中,根据实际应用的不同,对存储空间或效率的要求也不一样。比如在PC上与在嵌入式设备上的程序编写就有很大的差别。

    分析与解法:

    解法一:

       举一个八位的二进制例子来分析。对于二进制操作,我们知道,除以一个2,原来的数字将会减少一个0.如果除的过程中有余,那么就表示当前位置有一个1.

    以10100010为例:

    第一次除以2时,商为1010001,余为0.

    第二次除以2时,商为101000,余为1.

    因此,可以考虑利用整型数据除法的特点,通过相除和判断余数的值来分析。

     1 int BitCount(unsigned int n)
     2 {
     3     unsigned int c =0 ; // 计数器
     4     while (n)
     5     {
     6         if(n % 2 ==1) // 当前位是1
     7             c++ ; // 计数器加1
     8         n = n / 2;
     9     }
    10     return c ;
    11 }

    解法二:使用位操作

     进行向右移位操作

    1 int Count(unsigned int v){
    2     int num = 0;
    3     while(v){
    4         num += v & 0x01;
    5         v >>= 1;
    6     }
    7     return num;
    8 }

    解法三:

    位操作比除、余操作的效率高了很多。但是,即使采用位操作,时间复杂度仍为O(log2V),log2V为二进制数的位数。

    考虑:01000000

    如何判断给定的二进制数里面有且仅有一个1呢?可以通过判断这个数是否是2的整数次幂来实现。另外,如果只和这个“1”进行判断,如何设计操作呢?我们知道,如果进行这个操作,结果为0或1,就可以得到结论。

    n & (n - 1)能清除最右边的1。因为从二进制的角度讲,n相当于在n - 1的最低位加上1。举个例子,8(1000)= 7(0111)+ 1(0001),所以8 & 7 = (1000)&(0111)= 0(0000),清除了8最右边的1(其实就是最高位的1,因为8的二进制中只有一个1)。再比如7(0111)= 6(0110)+ 1(0001),所以7 & 6 = (0111)&(0110)= 6(0110),清除了7的二进制表示中最右边的1(也就是最低位的1)。

    所以有了如下代码:

    1 int Count(unsigned int v){
    2     int num = 0;
    3     while(v){
    4         v &= (n - 1);
    5         num++;
    6     }
    7     return num;
    8 }

    时间复杂度为O(M),其中M为1的个数。

    解法四:

    既然只有8位数据,索性直接把0-255的情况罗列出来,并使用分支操作,就可以得到答案。

     1 int Count(unsigned int v){
     2     int num = 0;
     3     switch(v){
     4         case 0x0: num = 0; break;
     5         case 0x1:
     6         case 0x2:
     7         case 0x4:
     8         case 0x8: 
     9         case 0x10:
    10         case 0x20:
    11         case 0x40:
    12         case 0x80: num = 1; break;
    13         //...
    14 
    15     }
    16     return num;
    17 }

    解法四看似很直接,但实际执行效率可能低于解法二和解法三,因为分支语句的执行情况要看具体字节的值,如果a = 255,则要在最后一个case才得到答案,即在进行了255次比较操作之后。

    解法四提供了一个思路,就是采用空间换时间的方法。

    解法五:查表法。

     1 int countTable[256];
     2 int CountTable(int *countTable){
     3     for(int i = 0; i < 256; i++){
     4         countTable[i] = (i & 1) + countTable[i / 2];
     5     }
     6 }
     7 int Count(unsigned int v){
     8     //check parameter
     9     return CountTable[v];
    10 }

    这是一个典型的空间换时间的算法,把0-255中“1”的个数直接存储在数组中,v作为数组的下标,算法的时间复杂度为O(1)。

    在一个需要频繁使用这个算法的应用中,通过“空间换时间”来获取高的时间效率是一个常用的方法。

    扩展问题:

       1.如果变量是32位的DWORD,你会使用上述的哪一个算法,或者改进哪一个算法?

    前面三种都是可以用的。如果用查表法:(参考了http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html)

    动态建表法:

    由于表示在程序运行时动态创建的,所以速度上肯定会慢一些,把这个版本放在这里,有两个原因

    1. 介绍填表的方法,因为这个方法的确很巧妙。

    2. 类型转换,这里不能使用传统的强制转换,而是先取地址再转换成对应的指针类型。也是常用的类型转换方法。

     1 int countTable[256];
     2 int CountTable(int *countTable){
     3     for(int i = 0; i < 256; i++){
     4         countTable[i] = (i & 1) + countTable[i / 2];
     5     }
     6 }
     7 int Count(unsigned int v){
     8     //这里进行类型转换
     9     unsigned char *p = (unsigned char*) &v;
    10     //check parameter
    11     int c;
    12     c = countTable[p[0]] + countTable[p[1]]
    13         + countTable[p[2]] + countTable[p[3]];
    14     return c;
    15 }

    对于任意一个32位无符号整数,将其分割为4部分,每部分8bit,对于这四个部分分别求出1的个数,再累加起来即可。而8bit对应2^8 = 256种01组合方式,这也是为什么表的大小为256的原因。

    注意类型转换的时候,先取到n的地址,然后转换为unsigned char*,这样一个unsigned int(4 bytes)对应四个unsigned char(1 bytes),分别取出来计算即可。举个例子吧,以87654321(十六进制)为例,先写成二进制形式-8bit一组,共四组,以不同颜色区分,这四组中1的个数分别为4,4,3,2,所以一共是13个1,如下面所示。

    10000111 01100101 01000011 00100001 = 4 + 4 + 3 + 2 = 13

    静态表-8bit:

    首先构造一个包含256个元素的表table,table[i]即i中1的个数,这里的i是[0-255]之间任意一个值。然后对于任意一个32bit无符号整数n,我们将其拆分成四个8bit,然后分别求出每个8bit中1的个数,再累加求和即可,这里用移位的方法,每次右移8位,并与0xff相与,取得最低位的8bit,累加后继续移位,如此往复,直到n为0。所以对于任意一个32位整数,需要查表4次。以十进制数2882400018为例,其对应的二进制数为10101011110011011110111100010010,对应的四次查表过程如下:红色表示当前8bit,绿色表示右移后高位补零。

    第一次(n & 0xff)             10101011110011011110111100010010

    第二次((n >> 8) & 0xff)  00000000101010111100110111101111

    第三次((n >> 16) & 0xff)00000000000000001010101111001101

    第四次((n >> 24) & 0xff)00000000000000000000000010101011

     1 int countTable[256];
     2 int CountTable(int *countTable){
     3     for(int i = 0; i < 256; i++){
     4         countTable[i] = (i & 1) + countTable[i / 2];
     5     }
     6 }
     7 int Count(unsigned int v){
     8     return countTable[v & 0xff] 
     9            + countTable[(v >> 8) & 0xff] 
    10            + countTable[(v >> 16) & 0xff] 
    11            + countTable[(n >> 24) & 0xff];
    12 }

    同理静态表-4bit:

     1 int BitCount4(unsigned int n)
     2 {
     3     unsigned int table[16] = 
     4     {
     5         0, 1, 1, 2, 
     6         1, 2, 2, 3, 
     7         1, 2, 2, 3, 
     8         2, 3, 3, 4
     9     } ;
    10 
    11     unsigned int count =0 ;
    12     while (n)
    13     {
    14         count += table[n &0xf] ;
    15         n >>=4 ;
    16     }
    17     return count ;
    18 }

    平行算法

     1 int BitCount4(unsigned int n) 
     2 { 
     3     n = (n &0x55555555) + ((n >>1) &0x55555555) ; 
     4     n = (n &0x33333333) + ((n >>2) &0x33333333) ; 
     5     n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ; 
     6     n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ; 
     7     n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ; 
     8 
     9     return n ; 
    10 }

    速度不一定最快,但是想法绝对巧妙。 说一下其中奥妙,其实很简单,先将n写成二进制形式,然后相邻位相加,重复这个过程,直到只剩下一位。

    以217(11011001)为例,有图有真相,下面的图足以说明一切了。217的二进制表示中有5个1



    完美法(还不太懂,以后再看)

    int BitCount5(unsigned int n) 
    {
        unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);
        return ((tmp + (tmp >>3)) &030707070707) %63;
    }

    最喜欢这个,代码太简洁啦,只是有个取模运算,可能速度上慢一些。区区两行代码,就能计算出1的个数,到底有何奥妙呢?为了解释的清楚一点,我尽量多说几句。

    第一行代码的作用

    先说明一点,以0开头的是8进制数,以0x开头的是十六进制数,上面代码中使用了三个8进制数。

    将n的二进制表示写出来,然后每3bit分成一组,求出每一组中1的个数,再表示成二进制的形式。比如n = 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,不会产生进位。

    tmp + (tmp >> 3)-这句就是是相邻组相加,注意会产生重复相加的部分,比如tmp = 659 = 001 010 010 011时,tmp >> 3 = 000 001 010 010,相加得

    001 010 010 011

    000 001 010 010

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

    001 011 100 101

    011 + 101 = 3 + 5 = 8。(感谢网友Di哈指正。)注意,659只是个中间变量,这个结果不代表659这个数的二进制形式中有8个1。

    注意我们想要的只是第二组和最后一组(绿色部分),而第一组和第三组(红色部分)属于重复相加的部分,要消除掉,这就是&030707070707所完成的任务(每隔三位删除三位),最后为什么还要%63呢?因为上面相当于每次计算相连的6bit中1的个数,最多是111111 = 77(八进制)= 63(十进制),所以最后要对63取模。

    指令法

    使用微软提供的指令,首先要确保你的CPU支持SSE4指令,用Everest和CPU-Z可以查看是否支持。

    unsigned int n =127 ;
    unsigned int bitCount = _mm_popcnt_u32(n) ;

    2.另一个相关的问题,给定两个正整数(二进制形式表示)A和B,问把A变为B需要改变多少位(bit)?也就是说,整数A和B的二进制表示中有多少位是不同的?

    分析:这个问题很简单,举个例子:

    01100100

    10101010

    很明显,如果利用“&”运算是不行的,相同的0&0 = 0, 1&1 = 1.很容易想到用异或运算“||”

    相同的0 ^ 0 = 0, 1 ^1 = 0.

    不同的0 ^ 1 = 1, 1 ^ 0 = 1.

     1 #include <stdio.h>
     2 int Count(int a, int b){
     3     int num = 0;
     4     while(a && b){
     5         num += ((a & 1) ^ (b & 1));
     6         a >>= 1;
     7         b >>= 1;
     8     }
     9     while(a){
    10         num++;
    11         a >>= 1;
    12     }
    13     while(b){
    14         num++;
    15         b >>= 1;
    16     }
    17     return num;
    18 }
    19 int main(){
    20     int a, b;
    21     scanf("%d %d", &a, &b);
    22     printf("%d
    ", Count(a, b));
    23     return 0;
    24 }

     

  • 相关阅读:
    python操作Excel读写--使用xlrd
    python 使用pymssql连接sql server数据库
    python pdb调试
    sqlser生成guid与复制造数
    sqlser游标与for循环
    bat写循环
    Jenkins配置多任务
    git命令行与Jenkins
    Jenkins执行python脚本
    Windows环境Jenkins配置免密登录Linux
  • 原文地址:https://www.cnblogs.com/qinduanyinghua/p/5864151.html
Copyright © 2020-2023  润新知