数目字之魅(一) 关于“出现数字1”

数字之魅(一) 关于“出现数字1”

节选自编程之美。注意这里部分只给出算法的思想和伪代码。

如果仔细想来,其实编程的过程就是和数字打交道的过程,所以对bit很熟练的操作,应该是程序员需要掌握的一项基本技能。

下面开始第一个问题:

1. 统计一个32位无符号整数中,1出现的个数。

其实这道题是一个比较老的题目了,网上有好多解法,我自己在面试的时候也被问过这个问题。但是当时候给出的算法是o(n)的,注意这里的n是数字的位数。

解法一:算法的思想是:每次测试最后的一位,测试的方法是n和二进制的01进行相与,如果是1那么就加一,然后左移1位。网上称这种方法为普通法。

下面是伪代码。

//o(n)的移位操作对应的伪代码
count = 0;
while n > 0
	if ((n &0x01) == 1)
		count ++;
		n >>=1;
retuen count;

解法二:时间复杂度为 o(k) 注意这里的k是n中1出现的次数,即算法的复杂度只和1出现的次数有关,虽然和o(n)的算法在时间复杂度在本质上相同,但是还是有很大的提高。

网上称这种方法为快速法。这种算法的关键是; 怎么每次找到1,然后再将它去除。

算法的思想是:因为 n = (n-1) + 1; 所以如果每次将n 和 n-1 进行“与”的话,那么就可以去掉n对应二进制最右边的1,然后将"与"值继续与比它小一的数进行重复操作,直至n为0;伪代码如下:

//o(k)的移位操作
count  = -1;
while n > 0
	n &=(n-1);
	count +=1;
return count;

其实上面是我们比较容易想到的方法,尤其是第一种,如果用辗转相除的话也可以实现,但是没有移位快。

这时候我的面试官为我有没有o(1)的算法,当时我就懵了,最后他给我提示用空间替换时间,我最后想到了用hash表,key为每0-2^32-1的每一个值,value为事先算好的对应的“1” 的个数,当然直接用数组来实现了。但是最后想想没敢说,这个空间的代价太大了,因为2^32大约是4G的内存,知道后来我在网上看到类似的但是跟巧妙的算法:

解法三:o(1) 的时间复杂度+ o(256)的空间复杂度;网上称这种方法为建表法;

算法思想是: 提前算出任意8bit整数对应的1出现的次数,然后放到一个表中。然后循环取n的后8位,进行hash然后将结果进行相加,下面是用c写的代码;

int BitCount3(unsigned int n)
{ 
    unsigned int table[256] = 
    { 
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, 
    }; 

    return table[n &0xff] +
        table[(n >>8) &0xff] +
        table[(n >>16) &0xff] +
        table[(n >>24) &0xff] ;
}
暂且不去考虑你是否有条件得到table[256],如果你得不到需要自己建怎么办,那么下面这种方法就是它的延伸,网上称为动态建表法:

解法四:这个算法主要实现了动态的建表,其中建表的思想是,这个很关键,请读者自己体会:

对于n 如果 n是偶数,那么n和n/2中含有1的个数是一样的,因为n是n/2左移1位来的,而左移最后是补0的,例如6 (0110) 和3 (0011) 个数是一样的。而如果n是奇数的话,

那么n = n/2 + 1 ; 所以n中1出现的次数是n/2出现的次数加1.

所以就有;

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 ; 
}
对于这个解法还有一个解释就是,这里用char型地址来分四次存储int型地址,达到每次存取8Bit的目的。

总结:对于上面四中解法,熟练掌握即可。3,4两种用空间换时间不一定比1,2好,但这种思想要学会应用。

2. 如何计算一个整数A和整数B的二进制表示中有多少位数不一样,换一种说法就是整数A如果变为整数B需要变换多少位?

其实,这个题目是1的延伸,如果对1理解的很清楚了,那么借助位运算可以很快的的得出算法:

先将A和B进行异或运算(c语言中是 ^) , 不同位的话异或会得出1,所以对异或的值选择1中的某个算法进行运算就可以了:

代码如下:

int get_diff(unsigned int A, unsigned int B)
{
	unsigned int diff = A ^ B;
	return BitCount3(diff);
}
下面再看一个和“1的数目”有关的题目: