位运算学习(一)

问题来源

我是由一个算法题目想到位运算的。题目是《算法问题实战策略》中的204页的题目,题目如下:
在没有电话的时代,摩尔斯电码是无线电传输领域中的一个常用代码。电码以短信号和长信号的不同组合表示各种文字,其中短信号是o,长信号是-。例如,o—表示英文字母J,而–表示英文字母M。
假设有一本以n个长点和m个短点组成的。包含所有信号的字典。例如,n=m=2,就会包含如下信号。
–oo
-o-o
-oo-
o–o
o-o-
oo–
所以,如果给定一个n和一个m,再给定一个k值,就能够唯一确定一个信号,即如果k是4,那么就是上述信号中的o–o。

这就让我想起了位运算中的一种算法,算是一种奇技淫巧吧,问题如下:
给定一个数a,a转化成二进制以后的位数为n,所有比a大的且比(1 << n) - 1小的数构成一个集合,集合中有一类数,这种数字转化为二进制以后和a中1的个数相同,求这一类数中的最小者,设为b。

问题解决

首先得说,这个问题在《算法心得》这本书上面有了详细的解答,即用短短的5行C语言代码就可以解决问题,但是,我在思考这个问题的时候想出了前三行代码和第五行代码,后面,也是我卡壳的地方,即第四行代码,我是没有想出来,最后看了书。我这么说的目的没有其他的,只是说我想把我对于这个算法的见解讲出来,毕竟,原创总比转载的好。
回到正题,为了方便,我就把书上面的代码粘贴过来,例子也是书上面的。

unsigned sonnb(unsigned x)
{
    unsigned smallest, ripple, ones;
                                     // x = xxx0 1111 0000
    smallest = x & -x;               // x = xxx0 0001 0000
    ripple = x + smallest;           // x = xxx1 0000 0000
    ones = x ^ ripple;               // x = xxx1 1111 0000
    ones = (ones >> 2) / smallest;   // x = xxx0 0000 0111
    return ripple | ones;            // x = xxx1 0000 0111
}

我先说说我的大致的思路吧,首先有一串二进制数(左边是高位,右边是低位),从第i位到第j位为最后一连串的1,那么就需要使第i位到第j位全部置0,然后i+1位置1(就是上面代码是中的ripple变量),可以发现从第i位到第0位一定有足够的位置来防止剩下的j-i个1(就是上述代码中的倒数第二行的ones变量).

首先是如何产生ripple变量,要产生ripple变量,第一步的工作就是要找到一个数,这个数具有如下性质:
除了第j位以外,其他的位都是0.
有了这样性质的数以后,加上原来的数,就可以得到ripple了。这里我们可以使用x和x的相反数求与的操作来实现,要是想问这到底是怎么来的,说实话,这是《算法心得》中第一章就介绍了的,所以直接拿来用就可以了。

然后是要将生鲜的i-j个数放在最末尾,这个操作就不是书上原本就有的操作了,分析如下:
首先可以观察,有了ripple和原本的x,如果我们将其或操作的话,那这个数(也就是代码中的ones)中的1的个数就比i-j多两个,不止如此,其实任何数只要进行了前三部的代码操作,ones中1的个数都比i-j多两个。
然后我们的目的就是要将这多出来的2个1去掉,同时要把这i-j个1移动到最低位,我们可以通过代码中的第四行进行实现,最后将ripple和ones进行或操作就可以了。

可能这里有个疑问,就是问什么要把ones进行右移两位而不是把x进行右移一位在除以smallest,说实话,这个问题但是还迷惑了我一下,原因就是,ones中1的位置是从第i+1到第j位,而x中的1的个数除了有第i到第j位之外,还有其他位。

猜你喜欢

转载自blog.csdn.net/u014713475/article/details/51590756