位运算技巧

位运算

这里讨论一些位操作技巧,如果使用得当会有助于提高代码运行效率。

这里假设你已经知道整型数据二进制 补码 表示方式。

位运算符

运算符 名称 例子 结果
& And(按位与) a & b 将把 a 和 b 中都为 1 的位设为 1,否则设为 0。
| Or(按位或) a | b 将把 a 和 b 中任何一个为 1 的位设为 1。
^ Xor(按位异或) a ^ b 将把 a 和 b 中一个为 1 另一个为 0 的位设为 1
(相同的为0,不同的为1)
~ Not(按位取反) ~ a 将 a 中为 0 的位设为 1,反之亦然。
<< Shift left(左移) a << b 将 a 中的位向左移动 b 次(每一次移动都表示“乘以 2”)。
>> Shift right(右移) a >> b 将 a 中的位向右移动 b 次(每一次移动都表示“除以 2”)。

异或性质

  • 任何数异或自己都为 0; 即 a ^ a = 0

  • 任何数「异或」0 都为本身;即 a ^ 0 = a

  • 「异或」支持交换律和结合律 (a ^ b) ^ c = a ^ (b ^ c)

运算符优先级

下表按照 PHP 优先级从高到低列出了运算符。同一行中的运算符具有相同优先级,此时它们的结合方向决定求值顺序。

PHP运算符优先级

操作技巧

判断奇偶 a & 1

  • a & 1 等于 0,a 为偶数
  • a & 1 等于 1,a 为奇数

交换两个数

function swap(&$a, &$b) 
{
        if ($a != $b) {
        $a ^= $b;
        $b ^= $a;
        $a ^= $b;
    }   
}

计算过程:

记住「异或 ^ 」的性质

  • 任何数异或自己都为 0; 即 a ^ a = 0

  • 任何数「异或」0 都为本身;即 a ^ 0 = a

  • 进行「异或」的数可以无序交换 (a ^ b) ^ c = a ^ (b ^ c)

a=a^b
b=b^a=b^(a^b)=(b^b)^a=a  --> b=a
a=a^b=(a^b)^b^(a^b)=(a^a)^(b^b)^b=b   --> a=b

变换符号

function signReversal(int $a) {
  return ~a + 1;
}

原理:

变换符号就是正数变成负数,负数变成正数。

如对于-11和11,可以通过下面的变换方法将-11变成11

1111 0101(二进制) –取反-> 0000 1010(二进制) –加1-> 0000 1011(二进制)

同样可以这样的将11变成-11

0000 1011(二进制) –取反-> 0000 0100(二进制) –加1-> 1111 0101(二进制)

因此变换符号只需要取反后加1即可。

求绝对值

function abs($a) {
    return $a ^ ($a >> 31) - ($a >> 31);
}

原理:

我们知道在我们对一个数进行位运算的时候,是在这个数的补码上进行的

对于补码我们知道,正数的补码是原码,负数的补码为原码除了最高位的符号位,取反,然后加1。把补码转换成原码的时候,正数还是原码,

负数时把补码除了符号位取反然后加 1 (我们可以发现如果这时候连符号位也求反,然后加1,与以前不同的只是少了一个符号位,现在实际上就是这个数的绝对值)。

所以我们可以得到对一个 **负数求绝对值 **的表达式为

function signReversal(int $a) {
  return ~a + 1;
}

那么由这些知识我们可以很快地得到求一个数的绝对值的表达式:

先移位来取符号位,int i = a >> 31;要注意如果 a 为正数,i 等于 0,为负数,i 等于 -1。然后对 i 进行判断——如果 i 等于0,直接返回。否之,返回~a+1。完整代码如下:

function my_abs(int $a) {
    $i = $a >> 31;
    return $i == 0 ? $a : (~$a + 1);
}

现在再分析下。对于任何数,与 0 「异或」都会保持不变,与 -1 即 0xFFFFFFFF 「异或」就相当于取反。因此,a 与 i 「异或」后再减 i(因为 i 为 0 或 -1,所以减 i 即是要么加 0 要么加 1)也可以得到绝对值。所以可以对上面代码优化下:

function my_abs(int $a) {
    $i = $a >> 31;
    return ($a ^ $i) - $i;
}

// 简化后为
function abs($a) {
    return $a ^ ($a >> 31) - ($a >> 31);
}

找出没有重复的那个数

给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。

如:1,2,3,4,5,1,2,3,4

function find($arr) {
    $tmp = $arr[0];
    foreach($arr as $value) {
        $tmp ^= $value;
    }
    return $tmp;
}

两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下:

由于异或支持交换律和结合律,所以:

1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。

也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。

避免溢出求平均值

function average($a, $b) {
        return ($a & $b) + (($a ^ $b) >> 1); 
}

a & b 取出 a 和 b二进制都为 1 的所有位

a ^ b a 和 b 中有一个为 1 的所有位

>>1 除以 2

想象一下a和b按照位整齐排序,当 a 和 b 对应为上全为 1 的时候相加会使此位为 0,并且会向前进一位,所以当出现对应位全为1的时候,直接在此位保留一个 1 就算对这两个对应位求平均值了

然后剩下就是对应位不全为1的时候,分为a的某一个位为1,对应的b的那个位为0,或者倒过来,或者两个都为0,这个时候就要把他们除以2(右移1位)了

然后把结果加起来就可以了。

参考资料

位操作基础篇之位操作全面总结

机器数和真值 原码,反码,补码及位运算

用位运算求一个数的绝对值

数据结构与算法系列之——位运算全解,弄懂位运算

posted @ 2019-10-23 18:48  Martini  阅读(333)  评论(0编辑  收藏  举报