文章目录
1. 位运算简介
1.1 位运算与二进制简介
位运算(Bit Operation): 在计算机内部, 数是以「二进制(Binary)」的形式存储. 位运算是直接对数的二进制进行操作, 使用位运算可显著提高程序性能.
二进制数
二进制数(Binary): 由 0 和 1 两个数码表示的数. 二进制数中的每个 0 或 1 称为一个「位(Bit)」.
在二进制中, 仅有 0 和 1, 进位规则为「逢二进一」. 例如:
1
(
2
)
+
0
(
2
)
=
1
(
2
)
1_{(2)} + 0_{(2)} = 1_{(2)}
1(2)+0(2)=1(2)
1
(
2
)
+
1
(
2
)
=
1
0
(
2
)
1_{(2)} + 1_{(2)} = 10_{(2)}
1(2)+1(2)=10(2)
1
0
(
2
)
+
1
(
2
)
=
1
1
(
2
)
10_{(2)} + 1_{(2)} = 11_{(2)}
10(2)+1(2)=11(2)
1.2 二进制数的转换
在十进制数中, 数字
274
9
(
10
)
2749_{(10)}
2749(10)可以表示为:
2
×
1000
+
7
×
100
+
4
×
10
+
9
×
1
=
274
9
(
10
)
2 \times 1000 + 7 \times 100 + 4 \times 10 + 9 \times 1 = 2749_{(10)}
2×1000+7×100+4×10+9×1=2749(10)
同理, 二进制数
0110101
0
(
2
)
01101010_{(2)}
01101010(2) 可表示为:
(
0
×
2
7
)
+
(
1
×
2
6
)
+
(
1
×
2
5
)
+
(
0
×
2
4
)
+
(
1
×
2
3
)
+
(
0
×
2
2
)
+
(
1
×
2
1
)
+
(
0
×
2
0
)
=
10
6
(
10
)
(0 \times 2^7) + (1 \times 2^6) + (1 \times 2^5) + (0 \times 2^4) + (1 \times 2^3) + (0 \times 2^2) + (1 \times 2^1) + (0 \times 2^0) = 106_{(10)}
(0×27)+(1×26)+(1×25)+(0×24)+(1×23)+(0×22)+(1×21)+(0×20)=106(10)
1.2.2 十进制转二进制数
十进制数转二进制的基本方法是: 除二取余, 逆序排列.
例如, 10 6 ( 10 ) 106_{(10)} 106(10) 的转换过程为:
106
÷
2
=
53
(余 0)
53
÷
2
=
26
(余 1)
26
÷
2
=
13
(余 0)
13
÷
2
=
6
(余 1)
6
÷
2
=
3
(余 0)
3
÷
2
=
1
(余 1)
1
÷
2
=
0
(余 1)
\begin{aligned}106 \div 2 &= 53 & \text{(余 0)} \\53 \div 2 &= 26 & \text{(余 1)} \\26 \div 2 &= 13 & \text{(余 0)} \\13 \div 2 &= 6 & \text{(余 1)} \\6 \div 2 &= 3 & \text{(余 0)} \\3 \div 2 &= 1 & \text{(余 1)} \\1 \div 2 &= 0 & \text{(余 1)}\end{aligned}
106÷253÷226÷213÷26÷23÷21÷2=53=26=13=6=3=1=0(余 0)(余 1)(余 0)(余 1)(余 0)(余 1)(余 1)
反向遍历余数, 得到
0110101
0
(
2
)
01101010_{(2)}
01101010(2).
2. 位运算基础操作
在二进制基础上, 位运算包括六种基本操作: 「按位与」、「按位或」、「按位异或」、「取反」、「左移」、「右移」. 其中, 前五个是双目运算, 取反为单目运算.
运算符总结
运算符 | 描述 | 规则 |
---|---|---|
| | 按位或 | 只要对应的两个二进位有一个为 1, 结果位为 1 |
& | 按位与 | 只有对应的两个二进位都为 1, 结果位为 1 |
<< | 左移 | 向左移动若干位, 高位丢弃, 低位补 0 |
>> | 右移 | 向右移动若干位, 低位丢弃, 高位补 0 |
^ | 按位异或 | 对应的两个二进位相异时, 结果位为 1 |
~ | 取反 | 每个二进位取反, 1 变 0, 0 变 1 |
2.1 按位与运算
按位与运算符为 &
, 仅当对应的两个二进位均为 1 时, 结果位为 1.
举个栗子
01111100
& 00111110
-----------
00111100
2.2 按位或运算
按位或运算符为 |
, 只要对应的两个二进位有一个为 1, 结果位为 1.
01001010
| 01011011
-----------
01011011
2.3 按位异或运算
按位异或运算符为^
, 对应的两个二进位相异时, 结果位为 1.
01001010
^ 01000101
-----------
00001111
2.4 取反运算
取反运算符为 ~
, 使每个二进位取反.
~01101010
-----------
10010101
2.5 左移与右移运算
左移运算符为 <<
, 右移运算符为 >>
. 左移时高位丢弃, 低位补 0;右移时低位丢弃, 高位补 0.
左移:
01101010 << 1 = 11010100
右移:
01101010 >> 1 = 00110101
3. 位运算的应用
3.1 位运算的常用操作
3.1.1 判断整数奇偶
通过与 1 进行按位与运算, 判断数是奇数还是偶数.
- ((x & 1) == 0) 为偶数. 因为1的末尾是二进制1,如果是偶数,末尾一定是二进制0,二者取
&
是0 - ((x & 1) == 1) 为奇数.
3.1.2 二进制数选取指定位
使用另一个二进制数 Y, 通过按位与运算提取 X 中的特定位.
X = 01101010
Y = 00001111
-----------------
X & Y = 00001010
3.1.3 将指定位设置为 1
通过按位或运算设置特定位.
X = 01101010
Y = 00001111
-----------------
X | Y = 01101111
3.1.4 反转指定位
通过按位异或运算反转特定位.
X = 01101010
Y = 00001111
-----------------
X ^ Y = 01100101
3.1.5 交换两个数
通过按位异或运算实现两个数的交换.
a, b = 10, 20
a ^= b
b ^= a
a ^= b
print(a, b) # 输出: 20, 10
1
0
(
10
)
=
0000101
0
(
2
)
10_{(10)} = \ \ \ \ \ \ 00001010_{(2)}
10(10)= 00001010(2)
2
0
(
10
)
=
0001010
0
(
2
)
20_{(10)} = \ \ \ \ \ \ 00010100_{(2)}
20(10)= 00010100(2)
a
=
a
⊕
b
=
0001111
0
(
2
)
a = a \oplus b = 00011110_{(2)}
a=a⊕b=00011110(2)
b
=
b
⊕
a
=
0000101
0
(
2
)
b = b \oplus a = 00001010_{(2)}
b=b⊕a=00001010(2)
a
=
a
⊕
b
=
0001010
0
(
2
)
a = a \oplus b = 00010100_{(2)}
a=a⊕b=00010100(2)
通过三次异或实现了ab交换
3.1.6 将最右侧为 1 的二进位改为 0
通过 \(X & (X - 1)\)
实现
X = 01101100
X-1 = 01101011
X & (X - 1) = 01101000
3.1.7 计算二进位为 1 的个数
使用 \(X & (X - 1)\)
统计二进位为 1 的数量.
当x不为0的时候,不停地与x-1做并操作,能够把最后一位非1的数字变为0,每次操作都会把最后一个1变成0,不停地操作
示例代码:
class Solution:
def hammingWeight(self, n: int) -> int:
cnt = 0
while n:
n = n & (n - 1)
cnt += 1
return cnt
3.1.8 判断某数是否为 2 的幂次方
通过判断 (X & (X - 1) == 0) 来验证.
3.2 位运算的常用操作总结
功 能 | 位运算 | 示例 |
---|---|---|
从右边开始, 把最后一个 1 改写成 0 | x & (x - 1) | 100101000 -> 100100000 |
去掉右边起第一个 1 的左边 | x & (x ^ (x - 1)) 或 x & (-x) | 100101000 -> 1000 |
去掉最后一位 | x >> 1 | 101101 -> 10110 |
取右数第 k 位 | x >> (k - 1) & 1 | 1101101 -> 1, k = 4 |
取末尾 3 位 | x & 7 | 1101101 -> 101 |
取末尾 k 位 | x & 15 | 1101101 -> 1101, k = 4 |
只保留右边连续的 1 | (x ^ (x + 1)) >> 1 | 100101111 -> 1111 |
右数第 k 位取反 | x ^ (1 << (k - 1)) | 101001 -> 101101, k = 3 |
在最后加一个 0 | x << 1 | 101101 -> 1011010 |
在最后加一个 1 | (x << 1) + 1 | 101101 -> 1011011 |
把右数第 k 位变成 0 | x & ~(1 << (k - 1)) | 101101 -> 101001, k = 3 |
把右数第 k 位变成 1 | x | (1 << (k - 1)) |
把右边起第一个 0 变成 1 | x | (x + 1) |
把右边连续的 0 变成 1 | x | (x - 1) |
把右边连续的 1 变成 0 | x & (x + 1) | 100101111 -> 100100000 |
把最后一位变成 0 | x | 1 - 1 |
把最后一位变成 1 | x | 1 |
把末尾 k 位变成 1 | x | (1 << (k - 1)) |
最后一位取反 | x ^ 1 | 101101 -> 101100 |
末尾 k 位取反 | x ^ (1 << (k - 1)) | 101001 -> 100110, k = 4 |
3.3 二进制枚举子集
通过二进制的 1 和 0 来表示集合的元素选取状态.
例如, 集合 ({a, b, c}) 的所有子集为:
- 空集:
000
- 只包含 a 的子集:
100
- 只包含 b 的子集:
010
- 只包含 c 的子集:
001
- 包含 a 和 b 的子集:
110
- 包含 b 和 c 的子集:
011
- 包含 a 和 c 的子集:
101
- 包含 a, b 和 c 的子集:
111
3.4 位运算的常见场景
- 高效判断奇偶性.
- 提取、设置特定位.
- 交换变量值而不需要临时变量.
- 统计二进制数中 1 的个数.
260. 只出现一次的数字 III
题要求在一个整数数组中找出恰好只出现一次的两个元素, 且其余所有元素都出现两次.
个人思路
如果我们对整个数组中的所有元素进行异或运算, 最终的结果会是两个只出现一次的数字的异或值, 因为所有出现两次的数字都会抵消为0.
假设异或结果为 xor
, 这个值包含了两个只出现一次的数字的某些位. 为了找到这两个数字, 我们需要找到 xor
中任意一个为1的位. 可以用 xor & -xor
来获得最低位的1.
使用找到的位, 将数组中的元素分为两个组: 一组在该位为1, 另一组在该位为0. 由于两个只出现一次的数字一定会落在不同的组中, 所以这样可以将其分开.
分别对这两个组中的元素进行异或运算, 得到的结果就是我们要找的两个数字.
class Solution:
def singleNumber(self, nums: List[int]) -> List[int]:
# 第一步:获取两个唯一数字的异或结果
xor = 0
for num in nums:
xor ^= num
# 第二步:获取最右边的设置位(区分位)
rightmost_set_bit = xor & -xor
# 第三步:将数字分为两组
num1, num2 = 0, 0
for num in nums:
if num & rightmost_set_bit:
num1 ^= num # 分组:区分位被设置的组
else:
num2 ^= num # 分组:区分位未被设置的组
return [num1, num2]