位运算,虽然说是一种十分精妙的算法技巧,但是除了花里胡哨好像并不能大幅提升时间复杂度。虽说如此,但不得不说,位运算的诸多技巧还是非常有趣的!
初识位运算
初次见到位运算的应用,那是在一个夏天的下午,有同学在班群问:“如何写交换函数?”不久,有人贴了这么段代码:
1 | void swap(int* a, int* b) { |
我一脸懵,这都行?这是怎么交换的?异或还能这么使?记得那天我在草稿纸上花了半天,才搞懂了个大概:
我们以 3
和 6
为例:
1 | 初始状态时: a = 3 = 011 b = 6 = 110 |
确实交换过来了,但为啥呢?但其实,我们可以将代码简单地变一下:
1 | b = (a ^ b) ^ b; |
使用交换律,也就是 b = a ^ (b ^ b)
,而这时,便可以清晰地看出来:因为一个数异或本身等于 0
,所以代码的前两句也便是 b = a
。那 a
又是怎么变过来的呢?
1 | a = (a ^ b) ^ ((a ^ b) ^ b) |
同样,进行变换 a = ((a ^ b) ^ (a ^ b)) ^ b
,也就是 a = b
,至此便完成了交换的操作。
常见的位运算技巧
位运算说是技巧,但实质上也是利用了逻辑运算的基本技巧:
- 一个数异或自己本身为
0
- 与或非的基本定义
- ……
只出现一次的数字 (LeetCode 136
)
题目说明:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
题目解答:这道题便可以利用异或的性质,将数组元素挨个异或,便可以将所有成对的数据消去,最终剩下的,便是只出现了一次时数字。
代码如下:
1 | class Solution { |
位1的个数(LeetCode 191
)
题目说明: 编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
题目解答: 这题便是相当明显的需要利用位操作的题目,因为需要一位一位地判断。所以一边右移一边判断最右边那位是0还是1即可!
1 | public class Solution { |
说明: 数字和1与,可以判断该数的奇偶!因为 1 的二进制是 00...1
,所以与完的结果便只有最后一位,1
为奇数,0
为偶数。
虽是如此,官方还给出来一个更为特殊的解法:
1 | public class Solution { |
说明:在二进制表示中,数字 n
中最低位的 1
总是对应 n - 1
中的 0
。因此,将 n
和 n - 1
与运算总是能把 n
中最低位的 1
变成 0
,并保持其他位不变。
所以,优化之后,只运算 1
的个数次即可!
2的幂(LeetCode 231
)
题目说明: 给定一个整数,编写一个函数来判断它是否是 2 的幂次方。
题目解答: 这题如果能想到 位运算
这仨字,那么便可以比较容易地想出算法,因为毕竟 2 的幂次在二进制中来看是“整”的,相当于十进制中的 10...100...1000
等等!
比如: 8的二进制表示为1000
,8-1
也就是 7
的二进制为 0111
。如果把这俩数相与,不刚好是 0
吗?但是由于负数的补码问题,所以首先还需要判断是否正数。
所以可以写出代码:
1 | bool isPowerOfTwo(int n) { |
4的幂(LeetCode 342
)
题目说明: 给定一个整数 (32
位有符号整数),请编写一个函数来判断它是否是 4
的幂次方。
题目解答: 这算是上题的进阶,如果熟悉二进制,便知道4的幂实质上用二进制来表示实质上为 10
间隔,4 -> 10
16 -> 10000
64 -> 1000000
。利用此特点,便可以发现这样一个数:01010101010101010101...
,也就是十六进制的 0x55555555
,其和4的幂相与,不会改变 0x55555555
的值。
综上写出代码:
1 | class Solution { |
说明: “0x55555555
,其和4的幂相与,不会改变 0x55555555
的值”是一个充分条件,并非必要条件。故可能还有别的数和 0x55555555
,其与4的幂相与,不会改变 0x55555555
的值,因此需要进行限制!即将非 2
的幂次和小于 0
的数除外。