写下这个题目,我的脸皮红得在发烧。我知道我很懒,但是远远低于Blog更新的平均周期仍然使我感到羞愧。然后是我在文章统计里看到了这个:
文章标题 | 页面/聚合访问 |
英文版WindowsXP下中文名Chm文件无法打开的解决方法 | 1662 /15 |
呵呵,发现了一个破解NTFS加密文件夹的另类方法 | 4273 /14 |
两篇奇技淫巧文章占去了我一半的访问量,让我十分怀疑自己是否退化到了高中时的水平(那个时候最为痴迷的杂志是《电脑爱好者》)。因此,我想我应该写点符合一名CS的学生身份的东西。最近看了些面试题,有些东西还值得记录下来,如果我没有再次半途而废,这个系列会继续下去~~
--
先贴两段代码:
{
n = ((n >> 1) & 0x55555555) | ((n << 1) & 0xaaaaaaaa);
n = ((n >> 2) & 0x33333333) | ((n << 2) & 0xcccccccc);
n = ((n >> 4) & 0x0f0f0f0f) | ((n << 4) & 0xf0f0f0f0);
n = ((n >> 8) & 0x00ff00ff) | ((n << 8) & 0xff00ff00);
n = ((n >> 16) & 0x0000ffff) | ((n << 16) & 0xffff0000);
return n;
}
int count_ones( int n)
{
n = (n & 0x55555555) + ((n & 0xaaaaaaaa) >> 1);
n = (n & 0x33333333) + ((n & 0xcccccccc) >> 2);
n = (n & 0x0f0f0f0f) + ((n & 0xf0f0f0f0) >> 4);
n = (n & 0x00ff00ff) + ((n & 0xff00ff00) >> 8);
n = (n & 0x0000ffff) + ((n & 0xffff0000) >> 16);
return n;
}
第一次看到是在C PUZZLES, Some interesting C problems,完全不懂它是在做什么,从函数名称上推断,bit_reverse是将一个unsigned int的二进制位反序排列,count_ones是计算一个unsigned int中二进制位1出现的次数。运行一下,的确结果是对的!今天在这里看到一贴回复,又演算了一下,方才明白其中的奥妙。
首先看bit_reverse的一个使用8bit的位串的简化示例,假定从左向右为每个位编号为1 2 3 4 5 6 7 8,
第一步:
((n >> 1) & 0x55) 即 (n >> 1) & 0101,0101),将位串上奇数位右移到偶数位上,得到0 1 0 3 0 5 0 7
((n << 1) & 0xaa) 即 ((n << 1) & 1010,1010),将位串上偶数位左移到奇数位上,得到2 0 4 0 6 0 8 0
两者相与,结果即得到奇数位和偶数位相互交换的位串:1 2 3 4 5 6 7 8 => 2 1 4 3 6 5 8 7
第二步:
分别将相邻的4位编为一组,左侧与右侧相互交换:2 1 4 3 6 5 8 7 => 4 3 2 1 8 7 6 5
第三步:
分别将相邻的8位编为一组,左侧与右侧相互交换:4 3 2 1 8 7 6 5 => 8 7 6 5 4 3 2 1. ~End~
这段代码像是杂耍一样就完成工作了,呵呵。事实上,在Programming Pearls里Chapter 2里提到的字符串旋转的算法与此有异曲同工之妙。效率O(logn),比起阳春的O(n)算法要高得多。
再来看count_ones,依然使用8bit的位串的简化示例,这次假定位串值位1001,0110
第一步,将相邻的2位编为一组:
(n & 0x55) 即 (n & 0101,0101) = 0001,0100:计算出每一组中左侧位上1的个数,并保存在结果中该组对应的位置上;
((n & 0xaa) >> 1) 即 (n & 1010,1010) = 0100,0001:计算出每一组中右侧位上1的个数,并保存在结果中该组对应的位置上;
两者相加,得到该组中1的个数,并保存在结果中该组对应的位置上;
第二步,将相邻的4位编为一组,根据第一步中的算法,计算每组中1的个数,并保存在结果中该组对应的位置上:
(n & 0x33) + ((n & 0xcc) >> 2) 即 0001,0001 + 0001,0001 即 0010,0010;
第三步,将相邻的8位编为一组,根据第一步中的算法,计算每组中1的个数,并保存在结果中该组对应的位置上:
(n & 0x0f) + ((n & 0xf0) >> 4) 即 0000,0010 + 0000,0010 即 0000,0100(二进制4). ~End~
如果上个算法像是杂耍,那这个算法就像是魔术了:-) (当然还有更加惊为天人鬼斧神工的,看Quake3中的快速平方根算法)。对于一般情况,这个算法的复杂度也是O(logn),相对于阳春的O(n)算法当然要高。但是在某些情况下,如果只有很少的位为1(考虑常见的标志位),那么还有下面的代码:
{
int count=0;
while(x)
{
count++;
x = x&(x-1);
}
return count;
}
How does it work? Try to figure it out:-)
这只是关于位运算一点小trick,茶余饭后娱乐一下,搞嵌入式的大牛们就不必费心看过来了。有自虐倾向者请继续看向Algorithms for Programmers里的Chapter 1.