位运算简介及实用技巧(二):进阶篇(1)
icon2 Program Impossible | icon4 2007-07-24 2:42| icon340 Comments | 本文内容遵从CC版权协议 转载请注明出自matrix67.com

=====   真正强的东西来了!   =====

二进制中的1有奇数个还是偶数个
    我们可以用下面的代码来计算一个32位整数的二进制中1的个数的奇偶性,当输入数据的二进制表示里有偶数个数字1时程序输出0,有奇数个则输出1。例如,1314520的二进制101000000111011011000中有9个1,则x=1314520时程序输出1。
var
   i,x,c:longint;
begin
   readln(x);
   c:=0;
   for i:=1 to 32 do
   begin
      c:=c + x and 1;
      x:=x shr 1;
   end;
   writeln( c and 1 );
end.

    但这样的效率并不高,位运算的神奇之处还没有体现出来。
    同样是判断二进制中1的个数的奇偶性,下面这段代码就强了。你能看出这个代码的原理吗?
var
   x:longint;
begin
   readln(x);
   x:=x xor (x shr 1);
   x:=x xor (x shr 2);
   x:=x xor (x shr 4);
   x:=x xor (x shr 8);
   x:=x xor (x shr 16);
   writeln(x and 1);
end.

    为了说明上面这段代码的原理,我们还是拿1314520出来说事。1314520的二进制为101000000111011011000,第一次异或操作的结果如下:

    00000000000101000000111011011000
XOR  0000000000010100000011101101100
---------------------------------------
    00000000000111100000100110110100


    得到的结果是一个新的二进制数,其中右起第i位上的数表示原数中第i和i+1位上有奇数个1还是偶数个1。比如,最右边那个0表示原数末两位有偶数个1,右起第3位上的1就表示原数的这个位置和前一个位置中有奇数个1。对这个数进行第二次异或的结果如下:

    00000000000111100000100110110100
XOR   000000000001111000001001101101
---------------------------------------
    00000000000110011000101111011001


    结果里的每个1表示原数的该位置及其前面三个位置中共有奇数个1,每个0就表示原数对应的四个位置上共偶数个1。一直做到第五次异或结束后,得到的二进制数的最末位就表示整个32位数里有多少个1,这就是我们最终想要的答案。


计算二进制中的1的个数
    同样假设x是一个32位整数。经过下面五次赋值后,x的值就是原数的二进制表示中数字1的个数。比如,初始时x为1314520(网友抓狂:能不能换一个数啊),那么最后x就变成了9,它表示1314520的二进制中有9个1。
x := (x and $55555555) + ((x shr 1) and $55555555);
x := (x and $33333333) + ((x shr 2) and $33333333);
x := (x and $0F0F0F0F) + ((x shr 4) and $0F0F0F0F);
x := (x and $00FF00FF) + ((x shr 8) and $00FF00FF);
x := (x and $0000FFFF) + ((x shr 16) and $0000FFFF);

    为了便于解说,我们下面仅说明这个程序是如何对一个8位整数进行处理的。我们拿数字211(我们班某MM的生日)来开刀。211的二进制为11010011。

+---+---+---+---+---+---+---+---+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <---原数
+---+---+---+---+---+---+---+---+
|  1 0  |  0 1  |  0 0  |  1 0  |   <---第一次运算后
+-------+-------+-------+-------+
|    0 0 1 1    |    0 0 1 0    |   <---第二次运算后
+---------------+---------------+
|        0 0 0 0 0 1 0 1        |   <---第三次运算后,得数为5
+-------------------------------+


    整个程序是一个分治的思想。第一次我们把每相邻的两位加起来,得到每两位里1的个数,比如前两位10就表示原数的前两位有2个1。第二次我们继续两两相加,10+01=11,00+10=10,得到的结果是00110010,它表示原数前4位有3个1,末4位有2个1。最后一次我们把0011和0010加起来,得到的就是整个二进制中1的个数。程序中巧妙地使用取位和右移,比如第二行中$33333333的二进制为00110011001100....,用它和x做and运算就相当于以2为单位间隔取数。shr的作用就是让加法运算的相同数位对齐。


二分查找32位整数的前导0个数
    这里用的C语言,我直接Copy的Hacker's Delight上的代码。这段代码写成C要好看些,写成Pascal的话会出现很多begin和end,搞得代码很难看。程序思想是二分查找,应该很简单,我就不细说了。
int nlz(unsigned x)
{
   int n;

   if (x == 0) return(32);
   n = 1;
   if ((x >> 16) == 0) {n = n +16; x = x <<16;}
   if ((x >> 24) == 0) {n = n + 8; x = x << 8;}
   if ((x >> 28) == 0) {n = n + 4; x = x << 4;}
   if ((x >> 30) == 0) {n = n + 2; x = x << 2;}
   n = n - (x >> 31);
   return n;
}



只用位运算来取绝对值
    这是一个非常有趣的问题。大家先自己想想吧,Ctrl+A显示答案。
    答案:假设x为32位整数,则x xor (not (x shr 31) + 1) + x shr 31的结果是x的绝对值
    x shr 31是二进制的最高位,它用来表示x的符号。如果它为0(x为正),则not (x shr 31) + 1等于$00000000,异或任何数结果都不变;如果最高位为1(x为负),则not (x shr 31) + 1等于$FFFFFFFF,x异或它相当于所有数位取反,异或完后再加一。



高低位交换
    这个题实际上是我出的,做为学校内部NOIp模拟赛的第一题。题目是这样:

    给出一个小于2^32的正整数。这个数可以用一个32位的二进制数表示(不足32位用0补足)。我们称这个二进制数的前16位为“高位”,后16位为“低位”。将它的高低位交换,我们可以得到一个新的数。试问这个新的数是多少(用十进制表示)。
  例如,数1314520用二进制表示为0000 0000 0001 0100 0000 1110 1101 1000(添加了11个前导0补足为32位),其中前16位为高位,即0000 0000 0001 0100;后16位为低位,即0000 1110 1101 1000。将它的高低位进行交换,我们得到了一个新的二进制数0000 1110 1101 1000 0000 0000 0001 0100。它即是十进制的249036820。


    当时几乎没有人想到用一句位操作来代替冗长的程序。使用位运算的话两句话就完了。
var
   n:dword;
begin
   readln( n );
   writeln( (n shr 16) or (n  shl 16) );
end.

    而事实上,Pascal有一个系统函数swap直接就可以用。


二进制逆序
    下面的程序读入一个32位整数并输出它的二进制倒序后所表示的数。
    输入: 1314520    (二进制为00000000000101000000111011011000)
    输出: 460335104  (二进制为00011011011100000010100000000000)
var
   x:dword;
begin
   readln(x);
   x := (x and $55555555) shl  1 or (x and $AAAAAAAA) shr  1;
   x := (x and $33333333) shl  2 or (x and $CCCCCCCC) shr  2;
   x := (x and $0F0F0F0F) shl  4 or (x and $F0F0F0F0) shr  4;
   x := (x and $00FF00FF) shl  8 or (x and $FF00FF00) shr  8;
   x := (x and $0000FFFF) shl 16 or (x and $FFFF0000) shr 16;
   writeln(x);
end.

    它的原理和刚才求二进制中1的个数那个例题是大致相同的。程序首先交换每相邻两位上的数,以后把互相交换过的数看成一个整体,继续进行以2位为单位、以4位为单位的左右对换操作。我们再次用8位整数211来演示程序执行过程:
+---+---+---+---+---+---+---+---+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |   <---原数
+---+---+---+---+---+---+---+---+
|  1 1  |  1 0  |  0 0  |  1 1  |   <---第一次运算后
+-------+-------+-------+-------+
|    1 0 1 1    |    1 1 0 0    |   <---第二次运算后
+---------------+---------------+
|        1 1 0 0 1 0 1 1        |   <---第三次运算后
+-------------------------------+



Copyright也很强
writeln('Matrix' , 42 XOR 105 , '原创,转贴请注明出处');

40 条回复

  • 楼层: 沙发 | | caesius 说:

    灌……

  • 楼层: 板凳 | | caesius 说:

    水……

  • 楼层: 地毯 | | dd 说:

    n = n + 2; x = x << 2;
    这是C程序员写出的代码吗?

    回复:还好只是copy的别人的,没丢人
    不过我也不明白为什么书上要这么写

  • 楼层: 地板 | | swj 说:

    不管用什么语言 原来是学Pascal的本质很容易暴露哦

  • 楼层: 地下室 | | yiyi 说:

    感觉传统的语言中,似乎只有COBOL比Pascal更像英文了.
    某位大师说过,程序是数学写成的诗...

    估计这也是,我一直用P的原因之一吧~

  • 楼层: 地基 | | Ai.Freedom 说:

    dd说的那个怎么了? C程序员是不会那么写... 但干嘛在这里说?

  • 楼层: 地壳 | | 逆铭 说:

    n = n + 2; x = x << 2;
    这是C程序员写出的代码吗?

    DD牛的意思应该是
    n+=2;x<<=2;
    这样写吧...

    其实中间可以用逗号连接,这样连括号都省了...

  • 楼层: 地幔 | | zouxun 说:

    为什么对于负数所有位取反,再加一就是它的绝对值?

    回复:看(一)的最后面

  • 楼层: 地核 | | Ai.Freedom 说:

    引用来自 dd
    n = n + 2; x = x << 2;
    这是C程序员写出的代码吗?
    DD牛的意思应该是
    n+=2;x<<=2;
    这样写吧...

    其实中间可以用逗号连接,这样连括号都省了...

    这个我理解, 但这个的出处是哪里?

  • 楼层: 10楼 | | Ai.Freedom 说:

    引用来自 逆铭
    引用来自 dd
    n = n + 2; x = x << 2;
    这是C程序员写出的代码吗?
    DD牛的意思应该是
    n+=2;x<<=2;
    这样写吧...

    其实中间可以用逗号连接,这样连括号都省了...

    这个我理解, 但这个的出处是哪里?

    理解了, 原来是我习惯性地看东西跳过src带来的严重后果..

  • 楼层: 11楼 | | axgle 说:

    例如,1314520的二进制101000000111011011000中有9个1,则x=1314520时程序输出1
    ---------------"效率"最高的写法(只需一行代码)---------------
    puts 1314520.to_s(2).count('1')%2
    -----------------当然这里的效率是指程序员(人)的效率---------
    Ruby-lang
    [lol]

    回复:我越来越想学ruby了

  • 楼层: 12楼 | | axgle 说:

    二进制逆序
        下面的程序读入一个32位整数并输出它的二进制倒序后所表示的数。
        输入: 1314520    (二进制为00000000000101000000111011011000)
        输出: 460335104  (二进制为00011011011100000010100000000000)
    --------ruby语言,一行代码解决问题:
    puts sprintf("%032b","1314520").split(//).reverse.join.to_i(2)

  • 楼层: 12a楼 | | axgle 说:

    午餐时候想到的,更简捷的写法:
    puts sprintf("%032b",1314520).reverse.to_i(2)

  • 楼层: 14楼 | | gromy 说:

    哈哈!你太有个性了!211就211吧!还非要突出是你们班某MM的生日!哈哈

    回复:突出那个MM在我心中的地位,随时忘不了提到她啊……

  • 楼层: 15楼 | | axgle 说:

    writeln('Matrix' , 42 XOR 105 , '原创,转贴请注明出处');
    42 XOR 105 = 67 ?
    [lol]

    回复:嗯

  • 楼层: 16楼 | | bar 说:

    只用位运算来取绝对值
        这是一个非常有趣的问题。大家先自己想想吧,Ctrl+A显示答案。
        答案:假设x为32位整数,则x xor (not (x shr 31) + 1) + x shr 31的结果是x的绝对值
        x shr 31是二进制的最高位,它用来表示x的符号。如果它为0(x为正),则not (x shr 31) + 1等于$00000000,异或任何数结果都不变;如果最高位为1(x为负),则not (x shr 31) + 1等于$FFFFFFFF,x异或它相当于所有数位取反,异或完后再加一。
    ___________________________________________________________________________
    这个实现跟平台相关吧,有符号数左移,右移是未定义的行为吧。比如你这里的 x shr 31,如果x为负数的话,那么
    x shr 31就为0xffffffff, not (x shr 31) + 1 等于0x00000000了。

  • 楼层: 17楼 | | Etfl 说:

    为什么取绝对值时用
    x:=x and not(1 shl 31);
    当x为负数时会出201错误?(x是longint)

  • 楼层: 18楼 | | jiqing 说:

    我怎么觉得二分查找32位整数的前导0个数的那个程序不对头也X=100时返回的是25, 我觉得应该是29呢?

  • 楼层: 19楼 | | 激情 说:

    二分查找32位整数的前导0个数的那个程序中20的前导0个数是27,而40是28, 40的前导0个数比20多??

    回复:不会吧,我运行出来的不是这个结果

  • 楼层: 20楼 | | XeCycle 说:

    绝对值如果这样:x<>=1;
    何如?

  • 楼层: 21楼 | | XeCycle 说:

    What's that?????

  • 楼层: 22楼 | | hfcxb 说:

    您的程序的可读性实在是......

    "求二进制中 1 的个数" 这样写如何?
    var num, x: integer;
    begin
    readln(x);
    num:=0;
    while x0 do
    begin
    x:=x and (x-1);
    num:=num+1;
    end;
    writeln(num);
    end.

  • 楼层: 23楼 | | zhzhzoo 说:

    那么要知道二进制中最后一个1在第几位呢

  • 楼层: 24楼 | | sai90 说:

    有些系統的左移運算是循環左移,這個問題不可忽略!

  • 楼层: 25楼 | | 阿僵/老卡 说:

    答案:假设x为32位整数,则x xor (not (x shr 31) + 1) + x shr 31的结果是x的绝对值

    只用位运算,可是有加号啊:-)

  • 楼层: 26楼 | | 位运算简介及实用技巧(二):进阶篇(1) | Planet of CODE 说:

    [...] http://www.matrix67.com/blog/archives/264 « 位运算简介及实用技巧(一):基础篇 [...]

  • 楼层: 27楼 | | Matrix67: My Blog » Blog Archive » 趣题:理想模型下的排序算法(上) 说:

    [...]     我非常喜欢这个题目。月初的时候我就提交了一个正确的算法。我们将用左移和加法运算把整数序列编码成一个超大整数,然后利用排序网络进行并行排序。这个算法比较复杂,你可以按照下面的思路一步一步得到这个算法。 1. 如何用位运算来取绝对值? [...]

  • 楼层: 28楼 | | ytj 说:

    计算二进制中的1的个数还有一种叫做“HAKMEM算法”的方法,看上去似乎是某种诡异的三分法。

    int Count(unsigned x){
    unsigned n;
    n = (x >> 1) & 033333333333;
    x = x - n;
    n = (n >> 1) & 033333333333;
    x = x - n;
    x = (x + (x >> 3)) & 030707070707;
    x = x % 63;
    return x;
    }

  • 楼层: 29楼 | | ytj 说:

    计算二进制中的1的个数,wikipedia上的方法更诡异,可以做64位整数。
    int popcount(unsigned long long x) {
    typedef unsigned long long qword;
    const qword m2 = ((qword)0x33333333<> 1) & (((qword)0x55555555<> 2) & m2);
    x = (x + (x >> 4)) & (((qword)0x0f0f0f0f<<32)+0x0f0f0f0f);
    return (x * (((qword)0x01010101<>56;
    }

  • 楼层: 30楼 | | St.fleagle 说:

    位运算 任意位取反 (把a 在b的1处取反 )
    a-(a and b) (反 a 中要反的 1)
    a+((not(a xor b)) and b) (反 a 中要反的 0)
    请教强人 是不是有点麻烦了

  • 楼层: 31楼 | | cyclone77 说:

    我是个菜鸟:您帮我看看那错了;
    program finding;
    var
    i,m,n:longint;
    procedure nlz(x:word);
    var
    n:integer;
    begin
    if x=0 then exit(32);
    n:=1;
    if ((x shr 16)=0) then begin n:=n+16; x:=x shl 16; end;
    if ((x shr 24)=0) then begin n:=n+8; x:=x shl 8; end;
    if ((x shr 28)=0) then begin n:=n+4; x:=x shl 4; end;
    if ((x shr 30)=0) then begin n:=n+2; x:=x shl 2; end;
    n:=n-(x shr 31);
    exit(n);
    end;
    begin
    assign(input,'input.txt');
    reset(input);
    assign(output,'output.txt');
    rewrite(output);
    readln(n);
    for i:=1 to n do
    read(a[i]);
    readln;
    readln(m);
    for i:=1 to n do
    nlz(m);
    end.

  • 楼层: 32楼 | | cyclone77 说:

    program finding;
    var
    i,m,n,k:longint;
    a:array[1..1000000]of longint;
    function nlz(x:word):integer;
    var
    n:integer;
    begin
    if x=0 then exit(32);
    n:=1;
    if ((x shr 16)=0) then begin n:=n+16; x:=x shl 16; end;
    if ((x shr 24)=0) then begin n:=n+8; x:=x shl 8; end;
    if ((x shr 28)=0) then begin n:=n+4; x:=x shl 4; end;
    if ((x shr 30)=0) then begin n:=n+2; x:=x shl 2; end;
    n:=n-(x shr 31);
    exit(n);
    end;
    begin
    assign(input,'input.txt');
    reset(input);
    assign(output,'output.txt');
    rewrite(output);
    readln(n);
    for i:=1 to n do
    read(a[i]);
    readln;
    readln(m);
    k:=nlz(m);
    writeln(k);
    close(input);
    close(output);
    end.
    不好意思上次的那个发错了这个才是。。。

  • 楼层: 33楼 | | yeah 说:

    This Is My Life, RatedLife:
    8.3Mind:
    7.9Body:
    9Spirit:
    9.1Friends/Family:
    6.3Love:
    5.7Finance:
    6.5Take the Rate My Life Quiz

  • 楼层: 34楼 | | power 说:

    怎么计算二进制中的0的个数?

  • 楼层: 35楼 | | 笨笨 说:

    回34楼
    0的个数用32减去1的个数就得了呗……

  • 楼层: 36楼 | | OIer:liyue 说:

    Orz Martrix67 神牛………
    明天就NOIP2009了

  • 楼层: 37楼 | | 蜗牛 说:

    膜拜楼主!!!

  • 楼层: 38楼 | | Pzjay 说:

    第一个判1个数的奇偶性循环下就清爽了
    for(y=1;y<=16;y<>y);

  • 楼层: 39楼 | | 晓而不羽 说:

    x xor (-(x shr 31))+(x shr 31) 也可以,好像是一样的

  • 楼层: 40楼 | | ShakeMyHeart 说:

    不错,不错

您也随便说几句吧:

您可以在 Gravatar 设置您的头像。