Leetcode基础算法-位运算


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=ab=00011110(2)
b = b ⊕ a = 0000101 0 ( 2 ) b = b \oplus a = 00001010_{(2)} b=ba=00001010(2)
a = a ⊕ b = 0001010 0 ( 2 ) a = a \oplus b = 00010100_{(2)} a=ab=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 改写成 0x & (x - 1)100101000 -> 100100000
去掉右边起第一个 1 的左边x & (x ^ (x - 1)) 或 x & (-x)100101000 -> 1000
去掉最后一位x >> 1101101 -> 10110
取右数第 k 位x >> (k - 1) & 11101101 -> 1, k = 4
取末尾 3 位x & 71101101 -> 101
取末尾 k 位x & 151101101 -> 1101, k = 4
只保留右边连续的 1(x ^ (x + 1)) >> 1100101111 -> 1111
右数第 k 位取反x ^ (1 << (k - 1))101001 -> 101101, k = 3
在最后加一个 0x << 1101101 -> 1011010
在最后加一个 1(x << 1) + 1101101 -> 1011011
把右数第 k 位变成 0x & ~(1 << (k - 1))101101 -> 101001, k = 3
把右数第 k 位变成 1x(1 << (k - 1))
把右边起第一个 0 变成 1x(x + 1)
把右边连续的 0 变成 1x(x - 1)
把右边连续的 1 变成 0x & (x + 1)100101111 -> 100100000
把最后一位变成 0x1 - 1
把最后一位变成 1x1
把末尾 k 位变成 1x(1 << (k - 1))
最后一位取反x ^ 1101101 -> 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]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值