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

来源:互联网 发布:杭州河坊街怎么样知乎 编辑:程序博客网 时间:2024/04/29 19:45

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

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

下面开始第一个问题:

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

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

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

下面是伪代码。

//o(n)的移位操作对应的伪代码count = 0;while n > 0if ((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 > 0n &=(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的数目”有关的题目,这是一道智力题,但是智力题的背后考察你对进制额的理解程度。

说:一个商人去鉴定一批41g以下的珠宝,不包括41g,那么如果它只能带4个砝码,请问他应该带哪4个?

My god, 如果你第一反应和我一样认为这是一道智力题,然后认为智商开始受虐,那么我也认为没什么。其实我是感觉这种题目如果之前没有碰到过类似的,那么估计得想一阵子。但是,如果我们能从信息科学的角度去思考这个问题,那么我认为还是可以解决的。

我们知道,对于二进制,n位可以表示任意的0-2^n-1,那么如果我们“剥离这道题天生骄傲的外衣”(引用下王自如喷锤子手机的话,哈哈) 其实,这道题的本质就是在我,如何用4bit表示1-40之间任意的数。这里很直观,2进制是不行了,而3进制正好可用。下面给出一个精彩的讲解这个问题的连接,大家可以移步。谢谢。

http://segmentfault.com/a/1190000000473432

0 0
原创粉丝点击