“求二進制數中1的個數”
by ZelluX

由電子工業出版社博文視點和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));
}