CS-Notes/docs/notes/Leetcode 题解 - 位运算.md

406 lines
12 KiB
Markdown
Raw Normal View History

2019-03-27 20:46:47 +08:00
# 原理
## 1. 基本原理
0s 表示一串 01s 表示一串 1。
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
x ^ 0s = x      x & 0s = 0      x | 0s = x
x ^ 1s = ~x     x & 1s = x      x | 1s = 1s
x ^ x = 0       x & x = x       x | x = x
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
- 利用 x ^ 1s = ~x 的特点可以将位级表示翻转利用 x ^ x = 0 的特点可以将三个数中重复的两个数去除只留下另一个数。
- 利用 x & 0s = 0  x & 1s = x 的特点可以实现掩码操作。一个数 num  mask00111100 进行位与操作只保留 num 中与 mask  1 部分相对应的位。
- 利用 x | 0s = x  x | 1s = 1s 的特点可以实现设值操作。一个数 num  mask00111100 进行位或操作 num 中与 mask  1 部分相对应的位都设置为 1。
2019-03-08 20:31:07 +08:00
位与运算技巧:
2019-03-27 20:46:47 +08:00
- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110100减去 1 得到 10110011这两个数相与得到 10110000。
- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1对于二进制表示 10110100-n 得到 01001100相与得到 00000100。
- n-n&(~n+1) 去除 n 的位级表示中最高的那一位。
2019-03-08 20:31:07 +08:00
移位运算:
2019-03-27 20:46:47 +08:00
- \>\> n 为算术右移相当于除以 2<sup>n</sup>
- \>\>\> n 为无符号右移左边会补上 0。
- &lt;&lt; n 为算术左移相当于乘以 2<sup>n</sup>
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
## 2. mask 计算
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
要获取 111111111 0 取反即可~0。
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
要得到只有第 i 位为 1  mask 1 向左移动 i-1 位即可1&lt;&lt;(i-1) 。例如 1&lt;&lt;4 得到只有第 5 位为 1  mask 00010000。
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
要得到 1  i 位为 1  mask1&lt;&lt;(i+1)-1 即可例如将 1&lt;&lt;(4+1)-1 = 00010000-1 = 00001111。
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
要得到 1  i 位为 0  mask只需将 1  i 位为 1  mask 取反 ~(1&lt;&lt;(i+1)-1)。
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
## 3. Java 中的位操作
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
static int Integer.bitCount();           // 统计 1 的数量
static int Integer.highestOneBit();      // 获得最高位
static String toBinaryString(int i);     // 转换为二进制表示的字符串
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
# 例题
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
## 统计两个数的二进制表示有多少位不同
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[461. Hamming Distance (Easy)](https://leetcode.com/problems/hamming-distance/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Input: x = 1, y = 4
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
Output: 2
2019-03-08 20:31:07 +08:00
Explanation:
2019-03-27 20:46:47 +08:00
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
The above arrows point to positions where the corresponding bits are different.
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
对两个数进行异或操作位级表示不同的那一位为 1统计有多少个 1 即可。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int hammingDistance(int x, int y) {
    int z = x ^ y;
    int cnt = 0;
    while(z != 0) {
        if ((z & 1) == 1) cnt++;
        z = z >> 1;
    }
    return cnt;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
使用 z&(z-1) 去除 z 位级表示最低的那一位。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int hammingDistance(int x, int y) {
    int z = x ^ y;
    int cnt = 0;
    while (z != 0) {
        z &= (z - 1);
        cnt++;
    }
    return cnt;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
可以使用 Integer.bitcount() 来统计 1 个的个数。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int hammingDistance(int x, int y) {
    return Integer.bitCount(x ^ y);
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 数组中唯一一个不重复的元素
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[136. Single Number (Easy)](https://leetcode.com/problems/single-number/description/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Input: [4,1,2,1,2]
Output: 4
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
两个相同的数异或的结果为 0对所有数进行异或操作最后的结果就是单独出现的那个数。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int singleNumber(int[] nums) {
    int ret = 0;
    for (int n : nums) ret = ret ^ n;
    return ret;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 找出数组中缺失的那个数
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[268. Missing Number (Easy)](https://leetcode.com/problems/missing-number/description/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Input: [3,0,1]
Output: 2
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
题目描述数组元素在 0-n 之间但是有一个数是缺失的要求找到这个缺失的数。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int missingNumber(int[] nums) {
    int ret = 0;
    for (int i = 0; i < nums.length; i++) {
        ret = ret ^ i ^ nums[i];
    }
    return ret ^ nums.length;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 数组中不重复的两个元素
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[260. Single Number III (Medium)](https://leetcode.com/problems/single-number-iii/description/)
2019-03-08 20:31:07 +08:00
两个不相等的元素在位级表示上必定会有一位存在不同。
将数组的所有元素异或得到的结果为不存在重复的两个元素异或的结果。
2019-03-27 20:46:47 +08:00
diff &= -diff 得到出 diff 最右侧不为 0 的位也就是不存在重复的两个元素在位级表示上最右侧不同的那一位利用这一位就可以将两个元素区分开来。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int[] singleNumber(int[] nums) {
    int diff = 0;
    for (int num : nums) diff ^= num;
    diff &= -diff;  // 得到最右一位
    int[] ret = new int[2];
    for (int num : nums) {
        if ((num & diff) == 0) ret[0] ^= num;
        else ret[1] ^= num;
    }
    return ret;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 翻转一个数的比特位
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[190. Reverse Bits (Easy)](https://leetcode.com/problems/reverse-bits/description/)
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int reverseBits(int n) {
    int ret = 0;
    for (int i = 0; i < 32; i++) {
        ret <<= 1;
        ret |= (n & 1);
        n >>>= 1;
    }
    return ret;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
如果该函数需要被调用很多次可以将 int 拆成 4  byte然后缓存 byte 对应的比特位翻转最后再拼接起来。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
private static Map<Byte, Integer> cache = new HashMap<>();
public int reverseBits(int n) {
    int ret = 0;
    for (int i = 0; i < 4; i++) {
        ret <<= 8;
        ret |= reverseByte((byte) (n & 0b11111111));
        n >>= 8;
    }
    return ret;
2019-03-08 20:31:07 +08:00
}
2019-03-27 20:46:47 +08:00
private int reverseByte(byte b) {
    if (cache.containsKey(b)) return cache.get(b);
    int ret = 0;
    byte t = b;
    for (int i = 0; i < 8; i++) {
        ret <<= 1;
        ret |= t & 1;
        t >>= 1;
    }
    cache.put(b, ret);
    return ret;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 不用额外变量交换两个整数
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[程序员代码面试指南 P317](#)
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
a = a ^ b;
b = a ^ b;
a = a ^ b;
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
## 判断一个数是不是 2  n 次方
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[231. Power of Two (Easy)](https://leetcode.com/problems/power-of-two/description/)
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
二进制表示只有一个 1 存在。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public boolean isPowerOfTwo(int n) {
    return n > 0 && Integer.bitCount(n) == 1;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
利用 1000 & 0111 == 0 这种性质得到以下解法
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public boolean isPowerOfTwo(int n) {
    return n > 0 && (n & (n - 1)) == 0;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 判断一个数是不是 4  n 次方
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[342. Power of Four (Easy)](https://leetcode.com/problems/power-of-four/)
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
这种数在二进制表示中有且只有一个奇数位为 1例如 1610000
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public boolean isPowerOfFour(int num) {
    return num > 0 && (num & (num - 1)) == 0 && (num & 0b01010101010101010101010101010101) != 0;
2019-03-08 20:31:07 +08:00
}
```
也可以使用正则表达式进行匹配。
```java
2019-03-27 20:46:47 +08:00
public boolean isPowerOfFour(int num) {
    return Integer.toString(num, 4).matches("10*");
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 判断一个数的位级表示是否不会出现连续的 0  1
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[693. Binary Number with Alternating Bits (Easy)](https://leetcode.com/problems/binary-number-with-alternating-bits/description/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Input: 10
Output: True
2019-03-08 20:31:07 +08:00
Explanation:
2019-03-27 20:46:47 +08:00
The binary representation of 10 is: 1010.
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
Input: 11
Output: False
2019-03-08 20:31:07 +08:00
Explanation:
2019-03-27 20:46:47 +08:00
The binary representation of 11 is: 1011.
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
对于 1010 这种位级表示的数把它向右移动 1 位得到 101这两个数每个位都不同因此异或得到的结果为 1111。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public boolean hasAlternatingBits(int n) {
    int a = (n ^ (n >> 1));
    return (a & (a + 1)) == 0;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 求一个数的补码
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[476. Number Complement (Easy)](https://leetcode.com/problems/number-complement/description/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Input: 5
Output: 2
Explanation: The binary representation of 5 is 101 (no leading zero bits), and its complement is 010. So you need to output 2.
2019-03-08 20:31:07 +08:00
```
2019-03-27 20:46:47 +08:00
题目描述不考虑二进制表示中的首 0 部分。
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
对于 00000101要求补码可以将它与 00000111 进行异或操作。那么问题就转换为求掩码 00000111。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int findComplement(int num) {
    if (num == 0) return 1;
    int mask = 1 << 30;
    while ((num & mask) == 0) mask >>= 1;
    mask = (mask << 1) - 1;
    return num ^ mask;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
可以利用 Java  Integer.highestOneBit() 方法来获得含有首 1 的数。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int findComplement(int num) {
    if (num == 0) return 1;
    int mask = Integer.highestOneBit(num);
    mask = (mask << 1) - 1;
    return num ^ mask;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
对于 10000000 这样的数要扩展成 11111111可以利用以下方法
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
mask |= mask >> 1    11000000
mask |= mask >> 2    11110000
mask |= mask >> 4    11111111
2019-03-08 20:31:07 +08:00
```
```java
2019-03-27 20:46:47 +08:00
public int findComplement(int num) {
    int mask = num;
    mask |= mask >> 1;
    mask |= mask >> 2;
    mask |= mask >> 4;
    mask |= mask >> 8;
    mask |= mask >> 16;
    return (mask ^ num);
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 实现整数的加法
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[371. Sum of Two Integers (Easy)](https://leetcode.com/problems/sum-of-two-integers/description/)
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
a ^ b 表示没有考虑进位的情况下两数的和(a & b) << 1 就是进位
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
递归会终止的原因是 (a & b) << 1 最右边会多一个 0那么继续递归进位最右边的 0 会慢慢增多最后进位会变为 0递归终止
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int getSum(int a, int b) {
    return b == 0 ? a : getSum((a ^ b), (a & b) << 1);
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 字符串数组最大乘积
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[318. Maximum Product of Word Lengths (Medium)](https://leetcode.com/problems/maximum-product-of-word-lengths/description/)
2019-03-08 20:31:07 +08:00
```html
2019-03-27 20:46:47 +08:00
Given ["abcw", "baz", "foo", "bar", "xtfn", "abcdef"]
Return 16
The two words can be "abcw", "xtfn".
2019-03-08 20:31:07 +08:00
```
题目描述:字符串数组的字符串只含有小写字符。求解字符串数组中两个字符串长度的最大乘积,要求这两个字符串不能含有相同字符。
2019-03-27 20:46:47 +08:00
本题主要问题是判断两个字符串是否含相同字符由于字符串只含有小写字符总共 26 因此可以用一个 32 位的整数来存储每个字符是否出现过。
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int maxProduct(String[] words) {
    int n = words.length;
    int[] val = new int[n];
    for (int i = 0; i < n; i++) {
        for (char c : words[i].toCharArray()) {
            val[i] |= 1 << (c - 'a');
        }
    }
    int ret = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            if ((val[i] & val[j]) == 0) {
                ret = Math.max(ret, words[i].length() * words[j].length());
            }
        }
    }
    return ret;
2019-03-08 20:31:07 +08:00
}
```
2019-03-27 20:46:47 +08:00
## 统计从 0 ~ n 每个数的二进制表示中 1 的个数
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/)
2019-03-08 20:31:07 +08:00
2019-03-27 20:46:47 +08:00
对于数字 6(110)它可以看成是 4(100) 再加一个 2(10)因此 dp[i] = dp[i&(i-1)] + 1;
2019-03-08 20:31:07 +08:00
```java
2019-03-27 20:46:47 +08:00
public int[] countBits(int num) {
    int[] ret = new int[num + 1];
    for(int i = 1; i <= num; i++){
        ret[i] = ret[i&(i-1)] + 1;
    }
    return ret;
2019-03-08 20:31:07 +08:00
}
```