zoukankan      html  css  js  c++  java
  • 对“算法-求二进制数中1的个数” 中一些要点的补充

    今天在听 吴军的谷歌方法论的时候,吴军讲解了关于 "求二进制数中1的个数" 的一些内容。

    于是搜索了下文章,发现有蛮多的方式,以下内容为对我看到的一篇文章的一些难点的补充(因为原文写的比较简洁,有些我一时也没能看懂): 

    算法-求二进制数中1的个数: http://www.cnblogs.com/graphics/archive/2010/06/21/1752421.html

    以下的代码也都为引用该文章

    1、普通法

    int BitCount(unsigned int n)
    {
        unsigned int c =0 ; // 计数器
        while (n >0)
        {
            if((n &1) ==1) // 当前位是1
                ++c ; // 计数器加1
            n >>=1 ; // 移位
        }
        return c ;
    }

    普通法就不要说明了,估计是针对该问题头脑中蹦出的第一个算法。

    2、快速法

    int BitCount2(unsigned int n)
    {
        unsigned int c =0 ;
        for (c =0; n; ++c)
        {
            n &= (n -1) ; // 清除最低位的1
        }
        return c ;
    }

    快速法的特点是使用了一个清除最右侧1的技巧,该算法运算次数与n中一个个数直接相关,也就是一算一个准。这里主要描述“清除最右侧1”的原理。

    假设:数字a= 0b10000(n) 和 数字 b= 0b01111 ,

    这里 a是一个2^n的数,也就是1个1 后面接 X个0(X取值 自然数 0,1,2,3...)

    这里  b = a-1  ,也就是X个1

    a&b = 0b00000 ,也就是我们需要用到的 a&(a-1)=0 ,也就是 a的唯一的一个1被消除掉了。

    那么任意的一个数字 n (n不等于0)我们都可以取出最后的那位1 和后面的所以0 作为a ,则a不为0,那么n-1的时候a其中的1左侧的值都不会变,只会有a变成a-1,

    那么n&n-1的时候,也就是a中的1消除掉的时候。

    假设 n=10101011101000= 10101011100000 + 1000

    则   n-1 =10101011100000 + 1000 - 1 =10101011100000 + (1000 - 1)

    所以n&(n-1)=10101011100000 & 10101011100000 + 1000&(111)=10101011100000 消除掉了最右侧的一个1。同理,当所以1消除完用的次数就是1的个数了。

    3、查表法

    int BitCount3(unsigned int n) 
    { 
        // 建表
        unsigned char BitsSetTable256[256] = {0} ; 
    
        // 初始化表 
        for (int i =0; i <256; i++) 
        { 
            BitsSetTable256[i] = (i &1) + BitsSetTable256[i /2]; 
        } 
    
        unsigned int c =0 ; 
    
        // 查表
        unsigned char* p = (unsigned char*) &n ; 
    
        c = BitsSetTable256[p[0]] + 
            BitsSetTable256[p[1]] + 
            BitsSetTable256[p[2]] + 
            BitsSetTable256[p[3]]; 
    
        return c ; 
    }

    查表法是一种比较简单的方式,方便理解。而且在当前存储越来越便宜的情况下,是越来越适合使用。

    上面例子中主要有两个点:

       一个是初始化数据的适合,以为是从小到大开始初始化,合理利用了 n/2的结果来计算n的结果

       另一个是因为初始化为255个结果即8bit的结果,所以把数据都拆成8bit来计算,然后再加起来。同理原文中还有一个是4bit计算的,我们甚至可以考虑16位初始化的,只要效益足够(这里吴军老师在后面分析关于缓存机制上是不太可行的,把一二级缓存的增益给抹掉了)。

    4、平行算法

    int BitCount4(unsigned int 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 ; 
    }

    这个算法也是比较有意思,核心思想如上图所示。

    这里 

    0x55555555 = 0b01010101010101010101010101010101
    0x33333333 = 0b00110011001100110011001100110011
    0x0f0f0f0f = 0b00001111000011110000111100001111
    0x00ff00ff = 0b00000000111111110000000011111111
    0x0000ffff = 0b00000000000000001111111111111111
    我们看到上面16进制数对应的2进制数,就比较了解了。
    第一步的效果是通过移位和与操作,把每2位加起来
    第二步是把第一步的结果每2个数加起来,为4位的结果
    ......

    5、完美法(原文作者认为的完美)
    int BitCount5(unsigned int n) 
    {
        unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);
        return ((tmp + (tmp >>3)) &030707070707) %63;
    }

    这个算法主要做2步骤,第一步每3位计算1的个数,第二步,把第一步的结果加一起。

    第一步的每三位加统计1个方式如下,假设三位分布位a、b、c (0,1) 

    则,对应位置的值大小为s=a*4 + b *2 + c ,

    而 ((n >>1) &033333333333)  的左右为先右移一位再屏蔽最高wei 相当与上面 s1=a*2 + b

    而((n >>2) &011111111111)  相当于 s2=a

    所以  s-s1-s2=a*4 + b*2 + c   -  (a*2+b) - a =a + b + c;

    也就是达到统计3位的1的个数的结果。

    上面的计算看不出来算法是怎么来的,难道是巧合?

    其实我们换个写法就看的出来了:

    s-s1-s2=a*4 + b*2 + c   -  (a*2+b) - a =(a*4 - a*2 - a) +(b*2 - b) +c;

    同理,假设我们要的是4个位的1的个数,我们可以构造为:

    s-s1-s2-s3 =(a*8 - a*4 - a*2 - a) +(b*4- b*2 -b)+(c*2-c) + d;

    所以它的关键点还是在于右移动上,把一个 数n (n为2^k)一直减去它所有的右移结果,最后为1  ,如:0b10000-0b1000-0b100-0b10-0b1=1 

    第二步的技巧有两个,

      第一个原文说明很详细还有配图,可参考原文,(tmp + (tmp >>3)) 达到的效果是每两个3位结果相加。

      第二个为取模63,这个达到的效果是把上一个的结果(每6位的结果存一个数)累加起来,得到最后的结果。

      原理是:  每6位的偏移为2^6倍,也就是我们可以把前面的结果写成 n1 +  2^6 * n2 + 2^6 * 2^6 *n3 .....=n1 + 64 *n2 + 64*64*n3

    我们对上面的数字取模 
    n1 + 64 *n2 + 64*64*n3... (mod 63)
    = n1 + (63+1)*n2 +(63+1)(63+1)n3...(mod 63)
    = n1 + 63*n2 + n2 + 63 *(63+1)n3 +(63+1)n3 ...(mod 63)
    = n1 + n2 + (63+1)n3...(mod 63)
    = n1 + n2 + 63*n3 + n3 ...(mod 63)
    = n1 + n2 + n3...(mod 63)
    这里的关键点还在于,我们输入的数字长度是有限的32位,所以再取模不会有什么影响,最后达到了把每6bit的结果累加的目的。


    这其中用到和比较多的2进制上的一些技巧,值得我们好好学习。
  • 相关阅读:
    1.python简介
    JSP标准标签库:JSTL
    冒泡排序算法
    中英文金额大写转换器
    递归与斐波那契数列
    web.xml配置文件详解
    Servlet及相关类和接口
    Servlet初始化及处理HTTP请求
    [转]jqGrid 属性、事件全集
    java web 过滤器跟拦截器的区别和使用
  • 原文地址:https://www.cnblogs.com/chianquan/p/9440439.html
Copyright © 2011-2022 走看看