用歸併法統計二進制序列中1的個數

今天遇到一個面試題:

 如何統計一個二進制整數num中1的個數.這裏參考了

https://blog.csdn.net/peiyao456/article/details/51724099
的第4種思路,非常巧妙,這裏寫一下心得筆記

我們以8位整數爲例,

首先輸入num可以看成一個二進制序列

num= a1 b1 a2 b2 a3 b3 a4 b4,

可以認爲他們自動分成8組,每組長度爲1,即

num = {a1},{b1},{a2},{b2},{a3},{b3},{a4},{b4},

每組中的值正好就是本組中1的個數.

然後我們把每2組合併成新的一組(現在每組的長度爲2),可以得到

num= {a1,b1}{a2,b2}{a3,b3}{a4,b4}

我們的目的是希望計算新的每一組的和,那麼具體步驟是

首先構造一個單組掩碼g_mask,長度等於每組的長度,左半部分都爲0,右半部分都爲1,

也就是g_mask=01,(這個掩碼的作用就是每一組的值和它進行與運算之後,只保留第2個元素,左半部分爲0!)

然後把每組的g_mask合併起來得到一個總的掩碼mask,即

mask=01010101=0x55

然後我們計算num&mask,這一步的目的是把每組左半部分的值爲0,

num&mask = {0,b1}{0,b2}{0,b3}{0,b4}

然後我們要得到每一組的左半部分,並且要和右半部分對齊,以便相加,實現這一步的技巧爲

首先計算 num>>s,其中s爲組長的一半,即

num>>s =  {a1,a1}{b1,a2}{b2,a3}{b3,a4}

然後再和mask進行與運算,以只保留右半部分,即

(num>>s)&mask =  {0,a1}{0,a2}{0,a3}{0,a4}

然後我們計算

num&mask+(num>>s)&mask,這就相當於計算

   {0,b1}{0,b2}{0,b3}{0,b4}

+{0,a1}{0,a2}{0,a3}{0,a4}

={a1+b1},{a2+b2},{a3+b3},{a4+b4}

然後對新的數據繼續進行兩兩合併,

每次重新計算

n*=2;  //兩兩合併就意味着每次分組長度擴大一倍,同時分組數目減少一半

s=n/2;    //s始終爲當前分組長度的一半,這樣才能保證每組右移s位之後,左元素和右元素對齊,以便相加

g_mask=1<<s-1; g_mask左邊是s個0,右邊是s個1

mask=n個g_mask連接起來,總長度爲整數位長L

right = num>>mask;    //每組只保留右半部分,左半部分爲0

left = (num>>s)&mask;   //把每組的左半部分移動到右部

num = left+right;   //計算每組的和,現在每組中的值都等於原始數據中對應那一組中的1的個數

直到最後只剩一組爲止,循環結束.最後得到的結果即爲所求

下面是一個具體的計算例子,

輸入數據:
a爲輸入的二進制整數
a=1111 1110 0001 0010 1001 1010 1011 1100

L=整數位數=32
一共18個1,預期輸出18.

計算步驟(n和s每次都乘2)

原始分組

num = {1} {1} {1} {1} {1} {1} {1} {0} {0} {0} {0} {1} {0} {0} {1} {0} {1} {0} {0} {1} {1} {0} {1} {0} {1} {0} {1} {1} {1} {1} {0} {0}

step1 

將num兩兩合併得到

{1,1} {1,1} {1,1} {1,0} {0,0} {0,1} {0,0} {1,0} {1,0} {0,1} {1,0} {1,0} {1,0} {1,1} {1,1} {0,0}

現在分組長度n=2,移位長度爲s=n/2=1,分組數爲16,單組mask爲01,

總mask爲   16個二進制01即0x5555 5555     

right=num&mask的結果爲

{0,1} {0,1} {0,1} {0,0} {0,0} {0,1} {0,0} {0,0} {0,0} {0,1} {0,0} {0,0} {0,0} {0,1} {0,1} {0,0}

num>>s的結果爲
{0,1} {1,1} {1,1} {1,1} {0,0} {0,0} {1,0} {0,1} {0,1} {0,0} {1,1} {0,1} {0,1} {0,1} {1,1} {1,0}

left=(num>>s) &mask的結果爲
{0,1} {0,1} {0,1} {0,1} {0,0} {0,0} {0,0} {0,1} {0,1} {0,0} {0,1} {0,1} {0,1} {0,1} {0,1} {0,0}

left+right的結果爲

b={1,0} {1,0} {1,0} {0,1} {0,0} {0,1} {0,0} {0,1} {0,1} {0,1} {0,1} {0,1} {0,1}{1,0} {1,0} {0,0}

step2

繼續將b兩兩合併

得到

b={1,0,1,0} {1,0,0,1} {0,0,0,1} {0,0,0,1} {0,1,0,1} {0,1,0,1} {0,1,1,0} {1,0,0,0}

現在分組長度n=4,移位長度爲s=n/2=2,分組數爲8,單組mask爲0011,

總mask爲   8個二進制0011即0x33333333

然後計算 c= b&mask+(b>>s) &mask,得到

c=0100 0011 0001 0001 0010 0010 0011 0010

step3,

現在分組長度n=8,移位長度爲s=n/2=4,分組數爲4,單組mask爲00001111,

總mask爲  0x0f0f0f0f

計算d= c&mask+(c>>s)&mask

得到

d=0000 0111 0000 0010 0000 0100 0000 0101

step4.

現在分組長度n=16,移位長度爲s=n/2=8,分組數爲2,單組mask爲0x00ff

總mask爲  0x00ff00ff

計算e= d&mask+(d>>s)&mask

得到

e=0000 0000 0000 1001 0000 0000 0000 1001

step 5

現在分組長度n=32,移位長度爲s=n/2=16,分組數爲1,單組mask爲0x0000ffff

總mask爲  0x0000ffff

計算f= e&mask+(e>>s)&mask

得到

f=0000 0000 0000 0000 0000 0000 0001 0010

即10進制的18,現在分組數目爲1,算法結束,f即爲所求結果

下面是上面計算過程的C程序,供讀者參考,並自行修改讓其適應64位的情況

#include <stdio.h> 

void print_bin(int value) {
	const int L = sizeof(int) * 8;   //得到整數的位數
	unsigned int mask =1<<(L-1);
	for (int i = 0;i < L;++i, mask >>= 1) {
		if (i % 4 == 0 && i != 0)
			printf(" ");
		printf("%d", value&mask ? 1 : 0);
	}
	printf("\n");
}

void print_bin(int value,int offset) {
	const int L = sizeof(int) * 8;   //得到整數的位數
	unsigned int mask = 1 << (L - 1);
	printf("{");
	for (int i = 0;i < L;++i, mask >>= 1) {
		if (i%offset==0 && i)
		{
			printf("} {");
		}
		if (i % offset )
		printf(",");
		printf("%d", value&mask ? 1 : 0);
	}
	printf("}");
	printf("\n");
}


int count_ones(unsigned int num)
{
	unsigned int m_2  = 0x55555555;
	unsigned int m_4  = 0x33333333;
	unsigned int m_8  = 0x0f0f0f0f;
	unsigned int m_16 = 0x00ff00ff;
	unsigned int m_32 = 0x0000ffff;
	
	
	unsigned int b = (m_2&num) + ((num >> 1)&m_2);
	printf("b=");
	print_bin(b);

	unsigned int c = (m_4&b) + ((b >> 2)&m_4);

	printf("c=");
	print_bin(c);
	

	unsigned int d = (m_8&c) + ((c >> 4)&m_8);
	printf("d=");
	print_bin(d);

	unsigned int e = (m_16&d) + ((d >> 8)&m_16);
	printf("e=");
	print_bin(e);

	printf("f=");
	unsigned int f = (m_32& e) + ((e>>16)&m_32);
	print_bin(f);

	return f;
}

int main()
{
	int a = 0xfe129abc;
	printf("a=");
	print_bin(a);
	int n = count_ones(a);
	printf("0x%x has %d ones \n", a,n);
	return 0;
}

程序運行結果爲

a=1111 1110 0001 0010 1001 1010 1011 1100
b=1010 1001 0001 0001 0101 0101 0110 1000
c=0100 0011 0001 0001 0010 0010 0011 0010
d=0000 0111 0000 0010 0000 0100 0000 0101
e=0000 0000 0000 1001 0000 0000 0000 1001
f=0000 0000 0000 0000 0000 0000 0001 0010
0xfe129abc has 18 ones

顯然本算法的複雜度爲log(2,L),L爲機器的整數位長.

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章