位运算与二进制表示集合
位运算与二进制表示集合
位运算
运算符
运算 | 运算符 | 数学符号表示 | 解释 |
---|---|---|---|
与 | \(\&\) | \(\&、and\) | 只有两个对应位都为 \(1\) 时才为 \(1\) |
或 | \(\mid\) | \(\mid、or\) | 只要两个对应位有一个 \(1\) 时就为 \(1\) |
异或 | $\ \hat{\ }\ $ | \(\oplus\)、\(xor\) | 只有两个对应位不同时才为 \(1\) |
取反 | \(\sim\) | 无 | 二进制位均 全部取反(\(0\) 变为 \(1\),\(1\) 变为 \(0\)) |
左移 | \(<<\) | 无 | \(num << i\) 表示将 \(num\) 的二进制位向左移动 \(i\) 位所得的值 |
带符号右移 | \(>>\) | 无 | 正数右移后,高位补 \(0\),负数右移后,高位补 \(1\) |
无符号右移(\(java\)等部分语言) | \(>>>\) | 无 | 无论正负,高位均补 \(0\) |
原码、反码、补码
最高位是符号位,以下以 \(8\) 位的二进制举例
编码方式 | 解释 | 举例 |
---|---|---|
原码 | 原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值 | \(+1_{原}=(0000\ 0001)_{原}\),\(-1_{原}=(1000\ 0001)_{原}\) |
反码 | 正数的反码是其本身。负数的反码是在其原码的基础上,符号位不变,其余各个位取反. | \(+1_{反}=(0000\ 0001)_{原}=(0000\ 0001)_{反}\) \(-1_{反}=(1000\ 0001)_{原}=(1111\ 1110)_{反}\) |
补码 | 正数的补码就是其本身。负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(即在反码的基础上+1) | \(+1_{补}=(0000\ 0001)_{原}=(0000\ 0001)_{反}=(0000\ 0001)_{补}\) \(-1_{补}=(1000\ 0001)_{原}=(1111\ 1110)_{反}=(1111\ 1111)_{补}\) |
计算机采用补码的编码方式
应用
一个数乘除 \(2\) 的非负整数次幂
计算 \(n\times 2^m\):\(n << m\)
计算 \(n\div 2^m\):\(n >> m\)
操作一个数的二进制位
操作 | 实现 | 举例 |
---|---|---|
获取 \(a\) 的二进制倒数 \(b\) 位(编号从 \(0\) 开始) | \(a >> b\ \&\ 1\) | \(4\ >>\ 2\ \&\ 1\) 等于 \(1\) |
将 \(a\) 的二进制倒数 \(b\) 位设置为 \(0\) | \(a\ \&\ \sim(1<<b)\) | \(5\ \& \sim(1 << 2)\) 等于 \(1\) |
将 \(a\) 的二进制倒数 \(b\) 位设置为 \(1\) | \(a \mid (1 << b)\) | \(1 \mid (1 << 2)\) 等于 \(5\) |
将 \(a\) 的二进制倒数 \(b\) 位取反 | \(a\ \hat{}\ (1 << b)\) | \(1\ \hat{}\ (1 << 2)\) 等于 \(5\) |
获取 \(a\) 的二进制最后一个 \(1\) 的值 | \(a\ \&\ -a\) 或 \(a\ \&\ (\sim a+1)\) | \(6\ \& -6\) 等于 \(2\) |
将 \(a\) 的二进制位最后一个 \(1\) 置为 \(0\) | \(a\ \&\ (a-1)\) | \(6\ \& (6 - 1)\) 等于 \(4\) |
将 \(a\) 的二进制位最后一个 \(0\) 置为 \(1\) | \(a \mid (a + 1)\) | \(7 \mid (7 + 1)\) 等于 \(8\) |
将 \(a\) 的二进制位最低位开始的连续 \(1\) 置为 \(0\) | \(a\ \& (a + 1)\) | \(7\ \& (7 + 1)\) 等于 \(0\) |
对于 \(+1-1\) 的各种二进制方法记不住也没关系,因为 \(+1-1\) 对应二进制变换无非就是 向前进位、原地加、向前借位、原地减
这四种情况,现场挑几个数字(如 \((0110)_2\) 和 \((0111)_2\) )推导即可
一些自带的二进制方法
大多通过二分查找的方式实现
C++(头文件stdlib.h 中) |
Java(均可通过使用Long 类代替Integer 类) |
|
---|---|---|
十进制转换其他进制(\(2\sim36\) 范围内) | itoa(数字, char[] ans, 进制) ,ltoa(数字, char[] ans, 进制) 同时返回值均为 char[] 类型的答案 |
Integer.toString(数字, 进制) |
任意进制转换十进制(\(2\sim36\) 范围内) | strtol(原进制字符数组, 接受剩余非法字符, 进制) 返回十进制数strtol 返回long 类型,strtoll 返回long long 类型strtoul 返回unsigned long 类型。还有double 等类型函数 |
Integer.parseInt(字符串String, 进制) |
二进制中 \(1\) 的个数 | int __builtin_popcount(unsigned int x) int __builtin_popcountll(unsigned long long x) 等等 |
Integer.bitCount(数字) |
二进制末尾连续 \(0\) 的个数 | int __builtin_ctz(unsigned int x) ,long 等其他类型同上 当 \(x\) 等于 \(0\) 时,行为未定义 |
Integer.numberOfTrailingZeros(数字) 当 \(x\) 等于 \(0\) 时,返回 \(32\) |
二进制前导零的个数(可用来求最高位的 \(1\) 的位置) | int __builtin_clz(unsigned int x) long 等其他类型同上当 \(x\) 等于 \(0\) 时,行为未定义 |
Integer.numberOfLeadingZeros(数字) 当 \(x\) 等于 \(0\) 时,返回 \(32\) |
获取符号 | 暂未了解 | Integer.signum(数字) 当 数字大于 \(0\) 时返回 \(1\) 当 数字等于 \(0\) 时返回 \(0\) 当 数字小于 \(0\) 时返回 \(-1\)。 |
获取二进制最后一个 \(1\) 的值 | 暂未了解 | Integer.lowestOneBit(数字) 通过 x & -x 实现 |
获取二进制第一个 \(1\) 的值(也就是小于等于该数的最大的 \(2\) 的幂的值) | 暂未了解 | Integer.highestOneBit(数字) 通过调用numberOfLeadingZeros(数字) 实现 |
二进制循环左移(低位缺少的位通过高位消去的位补充) | 暂未了解 | Integer.rotateLeft(数字, 移动位数) |
二进制循环右移(高位缺少的位通过低位消去的位补充) | 暂未了解 | Integer.rotateRight(数字, 移动位数) |
二进制按位反转 | 暂未了解 | Integer.reverse(数字) 返回二进制按位反转后的十进制值 |
更多位数
Int
类型的二进制位只有 \(32\) 位,Long
类型的二进制位也只有 \(64\) 位,如果需要更多的二进制位,就需要使用位图这个类了
下述表格待补全...
c++bitset (头文件bitset 中) |
JavaBitSet |
|
---|---|---|
例题
class Solution {
public boolean isPowerOfTwo(int n) {
if (n <= 0) return false;
return (n & (n - 1)) == 0;
}
}
class Solution {
public boolean isPowerOfFour(int n) {
if (n <= 0) return false;
if ((n & (n - 1)) != 0) return false;
return (0b10101010101010101010101010101010 & n) == 0;
}
}
class Solution {
public int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
}
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
return Integer.bitCount(n);
}
}
public class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
// 或者直接调用
//return Integer.reverse(n);
int ans = 0;
for (int i = 0; i < 16; ++i) {
ans |= (n >> i & 1) << (31 - i);
ans |= (n >> (31 - i) & 1) << i;
}
return ans;
}
}
每四位二进制数对应一位十六进制数
class Solution {
char[] ch = "0123456789abcdef".toCharArray();
final int mask = 0b1111;
public String toHex(int num) {
if (num == 0) return "0";
StringBuilder ans = new StringBuilder();
while (num != 0) {
ans.append(ch[num & mask]);
num >>>= 4; // 注意要无符号右移
}
return ans.reverse().toString();
}
}
数列横着看和竖着看是两种方式,有时另一种会十分简便
class Solution {
public int totalHammingDistance(int[] nums) {
int n = nums.length, ans = 0;
for (int i = 0; i < 32; ++i) {
int sum = 0;
// 计算第i位1的个数
for (int v : nums) {
sum += v >> i & 1;
}
ans += sum * (n - sum);
}
return ans;
}
}
class Solution {
public boolean hasAlternatingBits(int n) {
n ^= n >> 1;
return n != 0 && (n & (n + 1)) == 0;
}
}
class Solution {
// 预处理小时位和分钟位的二进制1的个数
static LinkedList<String>[] hours = new LinkedList[4];
static LinkedList<String>[] minutes = new LinkedList[9];
static {
for (int i = 0; i < 4; ++i) hours[i] = new LinkedList<String>();
for (int i = 0; i < 9; ++i) minutes[i] = new LinkedList<String>();
for (int i = 0; i < 12; ++i) {
hours[Integer.bitCount(i)].add(Integer.toString(i));
}
for (int i = 0; i < 10; ++i) {
minutes[Integer.bitCount(i)].add(":0" + i);
}
for (int i = 10; i < 60; ++i) {
minutes[Integer.bitCount(i)].add(":" + i);
}
}
public List<String> readBinaryWatch(int turnedOn) {
List<String> ans = new LinkedList<String>();
if (turnedOn >= 9) return ans;
// 枚举小时位灯的个数
for (int i = Math.min(3, turnedOn); i >= 0; --i) {
for (String hour : hours[i]) {
for (String minute : minutes[turnedOn - i]) {
ans.add(hour + minute);
}
}
}
return ans;
}
}
二进制表示集合
集合操作
操作 | 集合表示 | 位运算符 |
---|---|---|
交集 | \(a\cap b\) | \(a\ \&\ b\) |
并集 | \(a\cup b\) | \(a \mid b\) |
补集 | \(\bar{a}\) | \(\sim a\)(全集为二进制位均为 \(1\)) |
差集 | \(a\setminus b\) | \(a\ \&\ (\sim b)\) |
对称差 | \(a\bigtriangleup b\) | \(a\ \hat{\ }\ b\) |
遍历子集
若遍历的是二进制表示除前导 \(0\) 外均为 \(1\) 的集合(如 111111
),则可以通过下述方式遍历
int n = 1;
int S = (1 << n) - 1;
for (int i = 1; i <= S; ++i) {
for (int j = 0; j < n; ++j) {//遍历二进制每一位
if ((i >> j & 1) == 1) {//判断第j位是否存在
// do something;
}
}
}
但如果要屏蔽某一位置的遍历(如111110011
),若仍选择通过上述方式遍历,就需要一些判断,更推荐如下做法(逆序遍历)
/*
// 这种写法不会遍历空集
int n = 1;
int S = (1 << n) - 1;
for (int i = S; i != 0; i = (i - 1) & S) {
for (int j = 0; j < n; ++j) { // 遍历二进制每一位
if ((i >> j & 1) == 1) { // 判断第j位是否存在
//do something;
}
}
}
*/
int n = 1;
int S = (1 << n) - 1;
int i = S;
do {
for (int j = 0; j < n; ++j) { // 遍历二进制每一位
if ((i >> j & 1) == 1) { // 判断第j位是否存在
//do something;
}
}
i = (i - 1) & S;
} while (i != S);
原理:
- 减 \(1\) 是为了遍历所有比 \(S\) 小的数,减 \(1\) 的实质就是去掉二进制数的最后一个 \(1\),并在其后面的位上补上 \(1\),如\((10100)_2-1=(10011)_2\)
- & 操作是让原来 \(S\) 二进制上是 \(0\) 的位均保持 \(0\)
- 当 \(i\) 变为空集 \(0\) 时,继续减一会变成 \(-1\),而 \(-1=(111\dots111)_2\),他与 \(S\) 做 & 运算就会重新变为 \(S\),此时循环终止
例题
class Solution {
public List<String> letterCasePermutation(String s) {
int len = s.length();
char[] t = s.toCharArray();
List<String> ans = new LinkedList<>();
ans.add(s);
int S = 0;
for (int i = 0; i < len; ++i) {
if (Character.isDigit(t[i])) continue;
S |= 1 << i;
}
for (int i = S; i != 0; i = (i - 1) & S) {
t = s.toCharArray();
for (int j = 0; j < len; ++j) {
if ((i >> j & 1) == 1) t[j] ^= 32; // 英文字母异或32代表大小写转换
}
ans.add(new String(t));
}
return ans;
}
}
class Solution {
public List<List<Integer>> subsets(int[] nums) {
int len = nums.length;
List<List<Integer>> ans = new ArrayList<>(1 << len);
for (int i = 0, S = 1 << len; i < S; ++i) {
List<Integer> t = new LinkedList<>();
for (int j = 0; j < len; ++j) {
if ((i >> j & 1) == 1) {
t.add(nums[j]);
}
}
ans.add(t);
}
return ans;
}
}
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
int len = nums.length;
List<List<Integer>> ans = new ArrayList<>(1 << len);
for (int i = 0, S = 1 << len; i < S; ++i) {
List<Integer> t = new LinkedList<>();
boolean mark = true;
for (int j = 0; j < len; ++j) {
if ((i >> j & 1) == 1) {
if (j > 0 && nums[j] == nums[j - 1] && (i >> (j - 1) & 1) == 0) {
mark = false;
break;
}
t.add(nums[j]);
}
}
if (mark) ans.add(t);
}
return ans;
}
}
class Solution {
public List<Integer> findNumOfValidWords(String[] words, String[] puzzles) {
HashMap<Integer, Integer> map = new HashMap<>();
for (String s : words) {
// 二进制映射
int mask = 0;
for (int i = 0; i < s.length(); ++i) {
mask |= 1 << (s.charAt(i) - 'a');
}
// 题目保证puzzle字符串长度为 7
// 只加入个数小于等于 7 的减少空间消耗
if (Integer.bitCount(mask) <= 7) {
map.put(mask, map.getOrDefault(mask, 0) + 1);
}
}
List<Integer> ans = new ArrayList<>(puzzles.length);
for (String s : puzzles) {
// 二进制映射
int mask = 0;
// 跳过首字母,之后处理集合的时候单独加上,保证首字母存在
for (int i = 1; i < s.length(); ++i) {
mask |= 1 << (s.charAt(i) - 'a');
}
int cnt = 0;
int begin = s.charAt(0) - 'a';
for (int i = mask; i != 0; i = (i - 1) & mask) {
// 保证首字母存在
cnt += map.getOrDefault(i | (1 << begin), 0);
}
// 处理空集(只有首字母的情况)
cnt += map.getOrDefault(1 << begin, 0);
ans.add(cnt);
}
return ans;
}
}