天天看点

《编程之美》读书笔记——“求二进制数中1的个数”

“求二进制数中1的个数”

by ZelluX 

​​

《编程之美》读书笔记——“求二进制数中1的个数”

​​

由电子工业出版社博文视点和w3china.org社区联合举办了“看样章, 写书评, 赢取《​​编程之美——微软技术面试心得​​​》的​​活动​​​”,详情请见:​​http://bbs.w3china.org/dispbbs.asp?boardID=42&ID=61162​​

大家踊跃参加,活动非常热烈。下面文章来自读者ZelluX:

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

先来看看样章上给出的几个算法:

解法一,每次除二,看是否为奇数,是的话就累计加一,最后这个结果就是二进制表示中1的个数。

解法二,同样用到一个循环,只是里面的操作用位移操作简化了。

   1:  int Count(int v)   

   2:  {   

   3:      int num = 0;

   4:      while (v) {   

   5:          num += v & 0x01;   

   6:          v >>= 1;   

   7:      }   

   8:      return num;   

   9:  } 

解法三,用到一个巧妙的与操作,v & (v -1 )每次能消去二进制表示中最后一位1,利用这个技巧可以减少一定的循环次数。

解法四,查表法,因为只有数据8bit,直接建一张表,包含各个数中1的个数,然后查表就行。复杂度O(1)。

   1:  int countTable[256] = { 0, 1, 1, 2, 1, ..., 7, 7, 8 };   

   2:      

   3:  int Count(int v) {   

   4:      return countTable[v];   

   5:  }

好了,这就是样章上给出的四种方案,下面谈谈我的看法。

首先是对算法的衡量上,复杂度真的是唯一的标准吗?尤其对于这种数据规模给定,而且很小的情况下,复杂度其实是个比较次要的因素。

查表法的复杂度为O(1),我用解法一,循环八次固定,复杂度也是O(1)。至于数据规模变大,变成32位整型,那查表法自然也不合适了。

其次,我觉得既然是这样一个很小的操作,衡量的尺度也必然要小,CPU时钟周期可以作为一个参考。

解法一里有若干次整数加法,若干次整数除法(一般的编译器都能把它优化成位移),还有几个循环分支判断,几个奇偶性判断(这个比较耗时间,根据CSAPP上的数据,一般一个branch penalty得耗掉14个左右的cycle),加起来大概几十个cycle吧。

再看解法四,查表法看似一次地址计算就能解决,但实际上这里用到一个访存操作,而且第一次访存的时候很有可能那个数组不在cache里,这样一个cache miss导致的后果可能就是耗去几十甚至上百个cycle(因为要访问内存)。所以对于这种“小操作”,这个算法的性能其实是很差的。

这里我再推荐几个解决这个问题的算法,以32位无符号整型为例。

   1:  int Count(unsigned x) {   

   2:     x = x - ((x >> 1) & 0x55555555);    

   3:     x = (x & 0x33333333) + ((x >> 2) & 0x33333333);    

   4:     x = (x + (x >> 4)) & 0x0F0F0F0F;    

   5:     x = x + (x >> 8);    

   6:     x = x + (x >> 16);    

   7:     return x & 0x0000003F;    

   8:  } 

这里用的是二分法,两两一组相加,之后四个四个一组相加,接着八个八个,最后就得到各位之和了。

还有一个更巧妙的HAKMEM算法

   1:  int Count(unsigned x) {

   2:     unsigned n;    

   3:      

   4:     n = (x >> 1) & 033333333333;    

   5:     x = x - n;   

   6:     n = (n >> 1) & 033333333333;   

   7:     x = x - n;    

   8:     x = (x + (x >> 3)) & 030707070707;   

   9:     x = modu(x, 63);  

   10:     return x;   

   11:  } 

首先是将二进制各位三个一组,求出每组中1的个数,然后相邻两组归并,得到六个一组的1的个数,最后很巧妙的用除63取余得到了结果。

因为2^6 = 64,也就是说 x_0 + x_1 * 64 + x_2 * 64 * 64 = x_0 + x_1 + x_2 (mod 63),这里的等号表示同余。

这个程序只需要十条左右指令,而且不访存,速度很快。

由此可见,衡量一个算法实际效果不单要看复杂度,还要结合其他情况具体分析。

关于后面的两道扩展问题,问题一是问32位整型如何处理,这个上面已经讲了。

问题二是给定两个整数A和B,问A和B有多少位是不同的。

这个问题其实就是数1问题多了一个步骤,只要先算出A和B的异或结果,然后求这个值中1的个数就行了。

总体看来这本书还是很不错的,比较喜欢里面针对一个问题提出不同算法并不断改进的风格。这里提出一点个人的理解,望大家指正 ;-)

发现oj上有几道类似的题,做一个总结:

整数中的1(二)​​链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=281&rec=sim​​

时间限制:1000 ms  |  内存限制:65535

难度:5

描述

给出两个非负32位整型范围内的数a,b,请输出闭区间[a,b]内所有数二进制中各个位的1的总个数。

输入

有多组测试数据

每组测试数据只有一行,是两个整型数a,b(0<=a<=b<=150000000),空格分隔。

当a,b都是0时表示输入结束,该组输入不用计算。

测试数据的组数不超过1000组

输出

对于每组输入,输出区间[a,b]内所有数二进制中各个位的1的总个数。

样例输入

1 2
100 200
0 0      

样例输出

2
419      

求一个数化成二进制表示1的个数方法:

int found1(int a)
{
  int num = 0;
  while(a)
  {
    if(1 == a % 2)//如果是奇数,二进制的末尾一定是1
    {
      num++;
    }
    a /= 2;
  }
  return num;
}
int found2(int a)
{
  int num = 0;
  while(a)
  {
    num += a & 0x01;//和1相与,这个可比取模之后和1相比快多了哦
    a >>= 1;//右移一位,相当于除以2
  }
  return num;
}
int found3(int a)
{
  int num = 0;
  while(a)
  {
    a &= (a-1);//这个是复杂度最低的,为log2(x);x为a中1的个数。怎么解释呢……有点难度啊。自己模拟一下就明白了。
    num++;
  }
  return num;
}      

​​参考代码:​​

#include<cmath>
#include<queue>
#include<stack>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
using namespace std;
#define Max(a,b) a>b?a:b
#define Min(a,b) a>b?b:a
#define mem(a,b) memset(a,b,sizeof(a))
int dir[4][2]= {{0,-1},{-1,0},{0,1},{1,0}};
int fun(int n)
{
    int m=n,t=2,ans=0;
    while(m)
    {
        ans+=(n/t)*t/2;
        if(n%t>=t/2)
        {
            ans+=n%t-t/2+1;
        }
        t=t*2;
        m=m/2;
    }
    return ans;
}
int main()
{
    //freopen("1.txt","r",stdin);
    //freopen("2.txt","w",stdout);
    int n,m;
    while(~scanf("%d %d",&n,&m),n+m)
    {
        if(n>m)
            swap(n,m);
        //printf("%d %d\n",fun(n),fun(m));
        printf("%d\n",fun(m)-fun(n-1));
    }
    return 0;
}      

​​标程:​​

难得想到,呵呵~我是没有想到,先贴出代码:暂时不懂,只先贴下代码,类似归并的做法。对于相邻的两位,先计算这两位的1的个数(最大是2),比如对于32位的数来说,分成 16组,每组计算的是相邻两位的1的个数和,并且将这个和用新得到的数的两位表示(2位可以最大表示4,所以可以存得下这个和,和此时最大为2);然后对相邻四位进行操作,计算每两位两位的和(这样操作后其实是计算了原来32位数的相邻四位的1的个数);这样依次类推,对于32位的数来说,只要操作到将其相邻16位的1的个数相加就可以得到其包含的1的个数了。​

#include<cstdio>
unsigned f[]={0,1,4,12,32,80,192,448,1024,2304,5120,11264,24576,53248,114688,245760,524288,1114112,2359296,4980736,10485760,22020096,46137344,96468992,201326592,419430400,872415232,1811939328};
int getpos(int n)
{
    int cnt=0;
    while(n)
    {
        cnt++;
        n>>=1;
    }
    return cnt;
}
int count1(int num)
{
    int i=0;
    while(num)
    {
     num&=(num-1);
     i++;
    }
    return i;
}
int getsub(int s)
{
    s|=s>>1;
    s|=s>>2;
    s|=s>>4;
    s|=s>>8;
    s|=s>>16;
    return s=~s;
}
int g(int m)
{
    if(!(m&(m-1))) 
    {
        int n=getpos(m);
        return f[n-1];
    }
    return g((m&(-m)))+g((m&(m-1)))+count1((getsub(m&(-m))&(m&(m-1))))*(m&(-m));
}
int main()
{
    int a,b;
    while(~scanf("%d%d",&a,&b) && (a||b))
    printf("%d\n",g(b+1)-g(a));
}