在所有8-bit的整数中,含有k个数字“1”的二进制数一共有C(8,k)个。给出其中的一个二进制数,你如何利用位运算快速找到下一个恰有k个“1”的数?例如,如果给你二进制数01011100,那么下一个(含4个“1”的)数就是01100011。在继续阅读下去之前,建议你仔细思考一下。你或许会想看看我很早以前写的一篇介绍位运算的文章。这是一道很好的题目,很多位运算技巧在这里都有体现。
在草稿纸上随便举几个例子,规律很容易看出来。由于“1”的个数是固定的,为了让这个二进制数更大,我们必须把第一个出现在“1”左边的“0”改成“1”;同时,为了让这个二进制数尽可能小,我们必须把它右边那些“1”重新排到最低位去。
更具体地说,下一个二进制数可以通过以下步骤得到:找到右起第一个单个的或连续的数字“1”,把它们全改成“0”,同时把它们左边的那个“0”改为“1”。此时,“1”的个数可能减少了,我们只需把还差的“1”摆在最右边就行了。举个例子,01011100的右起第一个“1”在第三位,把它和左边紧挨着的“1”一并变为“0”,并把再左边那个“0”变为“1”,于是我们得到01100000。我们还差两个“1”,把这两个“1”补在最低位得到01100011即可。现在我们的任务是,想出一个用位运算来实现这些步骤的办法。
我们已经熟知,用x & -x可以提取最右边的那个“1”。当意识到可以利用加法来消除连续的“1”时,我们很快得到了第一步操作的位运算实现:把x & -x加到x上,利用二进制加法的进位把“..01111..”变成“..10000..”。现在,我们需要计算出刚才的操作中一共“跳过”了多少个“1”,换句话说现在的x的右起第一个“1”和原来的x的右起第一个“1”差了多少位。关键就在这里!我们可以用除法来完成这一步,例如100000除以100就相当于把被除数右移2位,得到的结果即可以表示两个数中的“1”差了多少位。在最低位产生指定数量的“1”需要用到另一个技巧:减1操作可以把右边连续的“0”都变成“1”,即把...10000变成...01111。我们得到了该问题的第一个算法:
b = x & -x;
t = x + b;
c = t & -t;
m = (c/b >> 1) - 1;
r = t | m; //最终结果
我们对上述算法做一个简单的说明:
操作 | 样例 | 说明
------------------+----------+----------------------------
x | 01011100 | 原数
b = x & -x | 00000100 | 提取x的右起第一个“1”
t = x + b | 01100000 | 把x的右起第一个位于某个“1”左边的“0”变成“1”,并把它右边的那些“1”都变为“0”
c = t & -t | 00100000 | 提取t的右起第一个“1”
c / b | 00001000 | 右移c中的那个“1”,其结果中最低位连续的“0”的个数正好是c和b中的“1”相差的距离
m = (c/b >> 1) - 1| 00000011 | 在最低位产生数字“1”,其个数比上述的“距离”少1
r = t | m | 01100011 | 最终结果
除去赋值,我们一共用了9个运算符。有可能用更少的运算么?
回想x^(x-1)的作用:保留右起第一个“1”,同时把右起连续的“0”也都变为“1”;直观地说,“...10000”异或“...01111”得到“11111”。巧妙就巧妙在,我可以用c=t^(t-1)同时完成定位右起第一个“1”和产生足够多的数字“1”两个步骤。我们的新算法省下了一个运算,只需要8个运算符:
b = x & -x;
t = x + b;
c = t ^ (t-1);
m = (c >> 2) / b;
r = t | m; //最终结果
同样地,我们做一个简单的说明:
操作 | 样例 | 说明
------------------+----------+----------------------------
x | 01011100 | 原数
b = x & -x | 00000100 | 提取x的右起第一个“1”
t = x + b | 01100000 | 把x的右起第一个位于某个“1”左边的“0”变成“1”,并把它右边的那些“1”都变为“0”
c = t ^ (t-1) | 00111111 | 提取t的右起第一个“1”,同时把后面的“0”也全变为“1”
m = (c >> 2) / b | 00000011 | 把c右移两位,再右移b所表示的位数
r = t | m | 01100011 | 最终结果
现在,我们只用了8个运算符便完成了最初提到的一系列操作。这个数目还能再少吗?其实,这里面还有一个非常隐蔽的可改进之处:计算c时我们根本不需要用t来异或t-1,直接异或x就行了,它所产生的“1”的个数显然足够多。这样,我们又可以节省一个运算符了:
b = x & -x;
t = x + b;
c = t ^ x;
m = (c >> 2) / b;
r = t | m; //最终结果
操作 | 样例 | 说明
------------------+----------+----------------------------
x | 01011100 | 原数
b = x & -x | 00000100 | 提取x的右起第一个“1”
t = x + b | 01100000 | 把x的右起第一个位于某个“1”左边的“0”变成“1”,并把它右边的那些“1”都变为“0”
c = t ^ x | 00111100 | 提取t的右起第一个“1”,同时把后面足够多的“0”也全变为“1”
m = (c >> 2) / b | 00000011 | 把c右移两位,再右移b所表示的位数
r = t | m | 01100011 | 最终结果