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


  14641人阅读  评论(35)  收藏  举报
编程 读书 算法 面试 编译器 活动

   《编程之美——微软技术面试心得》读书笔记

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

by ZelluX 

 

《编程之美》读书笔记——“求二进制数中1的个数”_第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的个数就行了。

 

MIT HAKMEM算法分析

分类: 学习笔记 算法   2357人阅读  评论(4)  收藏  举报
算法 优化 n2 c 工作

今天学习了一种很有趣的BitCount算法——MIT HAKMEM算法。

 

本文中^表示乘方

 

问题需求:计算32位整型数中的'1'的个数

 

思路分析:

 

1.整型数 i 的数值,实际上就是各位乘以权重——也就是一个以2为底的多项式:

 

i = A0*2^0+A1*2^1+A2*2^2+...

 

因此,要求1的位数,实际上只要将各位消权:

 

i = A0+A1+A2+...

 

所得的系数和就是'1'的个数。

 

2.对任何自然数n的N次幂,用n-1取模得数为1,证明如下:

 

若 n^(k-1) % (n-1) = 1 成立

 

则 n^k % (n-1) = ((n-1)*n^(k-1) + n^(k-1)) % (n-1) = 0 + n^(k-1) % (n-1)  = 1 也成立

 

又有 n^(1-1) % (n-1) = 1

 

故对任意非负整数N, n^N %(n-1)=1

 

3.因此,对一个系数为{Ai}的以n为底的多项式P(N), P(N)%(n-1) = (sum({Ai})) % (n-1) ;

 

如果能保证sum({Ai}) < (n-1),则 P(N)%(n-1) = (sum({Ai}))  ,也就是说,此时只要用n-1对多项式取模,就可以完成消权,得到系数和。

 

于是,问题转化为,将以2为底的多项式转化为以n为底的多项式,其中n要足够大,使得n-1 > sum({Ai})恒成立。

 

32位整型数中Ai=0或1,sum({Ai})<=32。n-1 > 32 ,n需要大于33。

 

因此取n=2^6=64>33作为新多项式的底。

 

4.将32位二进制数的每6位作为一个单位,看作以64为底的多项式:

 

i = t0*64^0 + t1*64^1 + t2*64^2 + t3*64^3 + ...

 

各项的系数ti就是每6位2进制数的值。

 

这样,只要通过运算,将各个单位中的6位数变为这6位中含有的'1'的个数,再用63取模,就可以得到所求的总的'1'的个数。

 

5.取其中任意一项的6位数ti进行考虑,最简单的方法显然是对每次对1位进行mask然后相加,即

 

(ti>>5)&(000001) + (ti&>>4)(000001) + (ti>>3)&(000001) + (ti>>2)&(000001) + (ti>>1)&(000001) + ti&(000001)

 

其中000001位2进制数

 

由于ti最多含有6个1,因此上式最大值为000110,绝不会产生溢出,所以上式中的操作完全可以直接对整型数 i 进行套用,操作过程中,t0~t6将并行地完成上式的运算。

 

注意:不能将&运算提取出来先+后&,想想为什么。

 

因此,bit count的实现代码如下:

 

 

但MIT HAKMEM最终的算法要比上面的代码更加简单一些。

 

为什么说上面的式子中不能先把(ti>>k)都先提取出来相加,然后再进行&运算呢?

因为用&(000001)进行MASK后,产生的有效位只有1位,只要6位数中的'1'个数超过1位,那么在"先加"的过程中,得数就会从最低位中向上溢出。

 

但是我们注意到,6位数中最多只有6个'1',也就是000110,只需要3位有效位。上面的式子实际上是以1位为单位提取出'1'的个数再相加求和求出6位中'1'的总个数的,所以用的是&(000001)。如果以3位为单位算出'1'的个数再进行相加的话,那么就完全可以先加后MASK。算法如下:

 

tmp = (ti>>2)&(001001) + (ti>>1)&(001001) + ti&(001001)

(tmp + tmp>>3)&(000111)

 

C代码:

 

注:代码中是使用8进制数进行MASK的,11位8进制数为33位2进制数,多出一位,因此第一位八进制数会把最高位舍去(7->3)以免超出int长度。

 

从第一个版本到第二个实际上是一个“提取公因式”的过程。用1组+, >>, &运算代替了3组。并且已经提取了"最大公因式"。然而这仍然不是最终的MIT HAKMEM算法,不过已经非常接近了,看看代码吧。

 

MIT HAKMEM算法:

 

又减少了一组+, >>, &运算。被优化的是3位2进制数“组”内的计算。再回到多项式,一个3位2进制数是4a+2b+c,我们想要求的是a
+b+c,n>>1的结果是2a+b,n>>2的结果是a。

 

于是: (4a+2b+c) - (2a+b) - (a) = a + b + c

 

中间的MASK是为了屏蔽"组间""串扰",即屏蔽掉从左边组的低位移动过来的数。

 

总结

 

第一次自己写算法分析,感觉收获还是蛮大的。

 

看算法的时候都是看这个算法是如何工作的,写的过程则是去模拟发现者的思路,看这个算法是如何被推导出来的,把思路反过来又重新梳理了一遍。

 

最大的感慨是数学对于算法的重要性。这个算法最开始的思路就是多项式消权,并且贯穿了整个算法推导和优化的过程。而第二步的必要条件则是对取模和幂运算关系的了解。优化的第一步用到了“提取公因式”思想,第二步则回归到了多项式的基本运算。

 

其中除了取模和幂运算的知识不算太基础以外,其他无一例外都是初中以内的数学知识与思想。特别是对2进制数本质的理解,所有人都清楚如何求得一个二进制整型数的数值,但是却很少有人对其多项式本质始终保持着清晰的认识。

History and usage[edit]

The Hamming weight is named after Richard Hamming although he did not originate the notion.[2] Irving S. Reed introduced a concept, equivalent to Hamming weight in the binary case, in 1954.[3]

Hamming weight is used in several disciplines including information theory, coding theory, and cryptography. Examples of applications of the Hamming weight include:

  • In modular exponentiation by squaring, the number of modular multiplications required for an exponent e is log2 e + weight(e). This is the reason that the public key value e used in RSA is typically chosen to be a number of low Hamming weight.
  • The Hamming weight determines path lengths between nodes in Chord distributed hash tables.[4]
  • IrisCode lookups in biometric databases are typically implemented by calculating the Hamming distance to each stored record.
  • In computer chess programs using a bitboard representation, the Hamming weight of a bitboard gives the number of pieces of a given type remaining in the game, or the number of squares of the board controlled by one player's pieces, and is therefore an important contributing term to the value of a position.
  • Hamming weight can be used to efficiently compute find first set using the identity ffs(x) = pop(x ^ (~(-x))). This is useful on platforms such as SPARC that have hardware Hamming weight instructions but no hardware find first set instruction.[5]

Efficient implementation[edit]

The population count of a bitstring is often needed in cryptography and other applications. The Hamming distance of two words A and B can be calculated as the Hamming weight of A xor B.

The problem of how to implement it efficiently has been widely studied. Some processors have a single command to calculate it (see below), and some have parallel operations on bit vectors. For processors lacking those features, the best solutions known are based on adding counts in a tree pattern. For example, to count the number of 1 bits in the 16-bit binary number A=0110110010111010, these operations can be done:

Expression Binary Decimal Comment
A 01 10 11 00 10 11 10 10   The original number
B = A & 01 01 01 01 01 01 01 01 01 00 01 00 00 01 00 00 1,0,1,0,0,1,0,0 every other bit from A
C = (A >> 1) & 01 01 01 01 01 01 01 01 00 01 01 00 01 01 01 01 0,1,1,0,1,1,1,1 the remaining bits from A
D = B + C 01 01 10 00 01 10 01 01 1,1,2,0,1,2,1,1 list giving # of 1s in each 2-bit piece of A
E = D & 0011 0011 0011 0011 0001 0000 0010 0001 1,0,2,1 every other count from D
F = (D >> 2) & 0011 0011 0011 0011 0001 0010 0001 0001 1,2,1,1 the remaining counts from D
G = E + F 0010 0010 0011 0010 2,2,3,2 list giving # of 1s in each 4-bit piece of A
H = G & 00001111 00001111 00000010 00000010 2,2 every other count from G
I = (G >> 4) & 00001111 00001111 00000010 00000011 2,3 the remaining counts from G
J = H + I 00000100 00000101 4,5 list giving # of 1s in each 8-bit piece of A
K = J & 0000000011111111 0000000000000101 5 every other count from J
L = (J >> 8) & 0000000011111111 0000000000000100 4 the remaining counts from J
M = K + L 0000000000001001 9 the final answer

Here, the operations are as in C, so X >> Y means to shift X right by Y bits, X & Y means the bitwise AND of X and Y, and + is ordinary addition. The best algorithms known for this problem are based on the concept illustrated above and are given here:

//types and constants used in the functions below
 
const uint64_t m1  = 0x5555555555555555; //binary: 0101...
const uint64_t m2  = 0x3333333333333333; //binary: 00110011..
const uint64_t m4  = 0x0f0f0f0f0f0f0f0f; //binary:  4 zeros,  4 ones ...
const uint64_t m8  = 0x00ff00ff00ff00ff; //binary:  8 zeros,  8 ones ...
const uint64_t m16 = 0x0000ffff0000ffff; //binary: 16 zeros, 16 ones ...
const uint64_t m32 = 0x00000000ffffffff; //binary: 32 zeros, 32 ones
const uint64_t hff = 0xffffffffffffffff; //binary: all ones
const uint64_t h01 = 0x0101010101010101; //the sum of 256 to the power of 0,1,2,3...
 
//This is a naive implementation, shown for comparison,
//and to help in understanding the better functions.
//It uses 24 arithmetic operations (shift, add, and).
int popcount_1(uint64_t x) {
    x = (x & m1 ) + ((x >>  1) & m1 ); //put count of each  2 bits into those  2 bits 
    x = (x & m2 ) + ((x >>  2) & m2 ); //put count of each  4 bits into those  4 bits 
    x = (x & m4 ) + ((x >>  4) & m4 ); //put count of each  8 bits into those  8 bits 
    x = (x & m8 ) + ((x >>  8) & m8 ); //put count of each 16 bits into those 16 bits 
    x = (x & m16) + ((x >> 16) & m16); //put count of each 32 bits into those 32 bits 
    x = (x & m32) + ((x >> 32) & m32); //put count of each 64 bits into those 64 bits 
    return x;
}
 
//This uses fewer arithmetic operations than any other known  
//implementation on machines with slow multiplication.
//It uses 17 arithmetic operations.
int popcount_2(uint64_t x) {
    x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
    x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits 
    x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits 
    x += x >>  8;  //put count of each 16 bits into their lowest 8 bits
    x += x >> 16;  //put count of each 32 bits into their lowest 8 bits
    x += x >> 32;  //put count of each 64 bits into their lowest 8 bits
    return x & 0x7f;
}
 
//This uses fewer arithmetic operations than any other known  
//implementation on machines with fast multiplication.
//It uses 12 arithmetic operations, one of which is a multiply.
int popcount_3(uint64_t x) {
    x -= (x >> 1) & m1;             //put count of each 2 bits into those 2 bits
    x = (x & m2) + ((x >> 2) & m2); //put count of each 4 bits into those 4 bits 
    x = (x + (x >> 4)) & m4;        //put count of each 8 bits into those 8 bits 
    return (x * h01)>>56;  //returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... 
}

The above implementations have the best worst-case behavior of any known algorithm. However, when a value is expected to have few nonzero bits, it may instead be more efficient to use algorithms that count these bits one at a time. As Wegner (1960) described,[6] the bitwise and of x with x − 1 differs from x only in zeroing out the least significant nonzero bit: subtracting 1 changes the rightmost string of 0s to 1s, and changes the rightmost 1 to a 0. If x originally had n bits that were 1, then after only n iterations of this operation, x will be reduced to zero. The following implementation is based on this principle.

//This is better when most bits in x are 0
//It uses 3 arithmetic operations and one comparison/branch per "1" bit in x.
int popcount_4(uint64_t x) {
    int count;
    for (count=0; x; count++)
        x &= x-1;
    return count;
}

If we are allowed greater memory usage, we can calculate the Hamming weight faster than the above methods. With unlimited memory, we could simply create a large lookup table of the Hamming weight of every 64 bit integer. If we can store a lookup table of the hamming function of every 16 bit integer, we can do the following to compute the Hamming weight of every 32 bit integer.

static uint8_t wordbits[65536] = { /* bitcounts of integers 0 through 65535, inclusive */ };
static int popcount(uint32_t i)
{
    return (wordbits[i&0xFFFF] + wordbits[i>>16]);
}

你可能感兴趣的:(算法,学习笔记)