趣题:用位运算生成下一个含有k个1的二进制数(转载)

趣题:用位运算生成下一个含有k1的二进制数(转载)

 

(转载自matrix67的专栏:http://blog.csdn.net/matrix67/archive/2008/09/24/4779921.aspx

 

    在所有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”的个数正好是cb中的“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 |  最终结果

 

    这次,我们只用了7个运算就实现了题目的要求。还能继续优化吗?欢迎大家继续讨论!

    来源:http://realtimecollisiondetection.net/blog/?p=78

 

 

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/matrix67/archive/2008/09/24/4779921.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值