目录
整数的二进制表示有3种:原码、反码、补码。正整数的原码、反码、补码相同。负整数的反码是原码符号位不变,其他位按位取反,补码是反码+1。整数在内存中存储的是补码。
+7:
原码:00000000000000000000000000000111
反码:00000000000000000000000000000111
补码:00000000000000000000000000000111
-7:
原码:10000000000000000000000000000111
反码:11111111111111111111111111111000
补码:11111111111111111111111111111001
操作符的优先级:
() = [] = 结构成员访问操作符 > 单目操作符 > 算术操作符 > 移位操作符 > 关系操作符 > 位操作符 > 逻辑操作符 > ?: > 赋值操作符 > ,(逗号)
如果记不住,写表达式的时候,能加括号就加括号。
01. 按位取反操作符 ~
~是一个单目操作符,表示对一个数的二进制的每一位按位取反,即0变成1,1变成0。
int a = 7;
int b = ~a;
// a的补码:00000000000000000000000000000111
// b的补码:11111111111111111111111111111000
int a = -7;
int b = ~a;
// a的补码:11111111111111111111111111111001
// b的补码:00000000000000000000000000000110
02. 移位操作符
02.1 左移操作符 <<
移位规则:左边抛弃,右边补0
int a = 7;
int b = a << 1;
0[00000000000000000000000000001110]
int a = -7;
int b = a << 1;
1[11111111111111111111111111110010]
1 << n = 2的n次幂
int a = 1;
int b = a << 1;
0[00000000000000000000000000000010] = 2的1次幂
int b = a << 2;
00[00000000000000000000000000000100] = 2的2次幂
int b = a << 3;
000[00000000000000000000000000001000] = 2的3次幂
02.2 右移操作符 >>
移位规则:
- 逻辑移位:左边补0,右边抛弃
- 算术移位:左边补原符号位,右边抛弃(常见编译器都是算术移位)
算术移位:
int a = 7;
int b = a >> 1;
[00000000000000000000000000000011]1
int a = -7;
int b = a >> 1;
[11111111111111111111111111111100]1
n >> 1 和 n / 2 非负数和负偶数时二者等价
n为非负数时,n >> 1 等价于 n / 2。
10 >> 1 = 5 10 / 2 = 5 0 >> 1 = 0 0 / 2 = 0
n为负偶数时,n >> 1 等价于 n / 2。
-10 >> 1 = -5 -10 / 2 = -5
n为负奇数时,n >> 1 = n / 2 - 1(n >> 1是n除以2向下取整,n / 2直接丢弃小数部分)。
-5 >> 1 = -3 -5 / 2 = -2
03. 位操作符
&按位与,|按位或,^按位异或
位操作符通过逐位比较两个运算对象,生成一个新值。对于每个位:
- &:两个操作数相应的位都为1,结果为1(有0则0,记忆法:&看起来像0)
- | :两个操作数相应的位至少有一个为1,结果为1(有1则1,记忆法:|看起来像1)
- ^:两个操作数相应的位相同为0,相异为1(无进位加法)
03.1 按位与操作符 &
int a = 3;
int b = -5;
int c = a & b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:00000000000000000000000000000011
n >> i & 1 获取二进制的i位
一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要获取i位,就要在i位上& 1。
以75(000000000000000000000001001011)为例,
要想获取0位,就要在0位上& 1:
000000000000000000000001001011
&000000000000000000000000000001
=000000000000000000000000000001
=1
要想获取1位,就要在1位上& 1,如果直接& 10(二进制),得到的结果是10(二进制),这个结果并不是我们想要的,我们想要的结果是1。所以把要把0位去掉,再& 1。
先>> 1,再& 1:
75 >> 1 = [000000000000000000000000100101]1
000000000000000000000000100101
&000000000000000000000000000001
=000000000000000000000000000001
=1
同理,要想获取2位,就要先>> 2,再& 1:
75 >> 2 = [000000000000000000000000010010]11
000000000000000000000000010010
&000000000000000000000000000001
=000000000000000000000000000000
=0
n & 1 和 n % 2 非负数时二者等价
显然,n & 1表示获取n的二进制的最后一位,奇数的二进制的最后一位一定是1,偶数的二进制的最后一位一定是0。所以,当n为非负数时,n & 1和n % 2等价;当n为负数时,由于取模运算保留符号,n % 2 = -1,而n & 1 = 1。
n为非负数时,n & 1等价于n % 2
- n为奇数:n & 1 = 1 n % 2 = 1
- n为偶数:n & 1 = 0 n % 2 = 0
n为负数时,
- n为奇数:n & 1 = 1 n % 2 = -1
- n为偶数:n & 1 = 0 n % 2 = 0
n & ~ (1 << i) 将二进制的i位变为0
一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要将i位修改成0,就要在i位上& 0,其他位都& 1。
以75(000000000000000000000001001011)为例,
要将3位修改成0,就要在3位上& 0,其他位都& 1:
000000000000000000000001001011
&111111111111111111111111110111
=000000000000000000000001000011
n & n - 1 将二进制最右边的1变为0
n - 1表示将n的二进制最右边的1的右侧区域(包括1)全部按位取反。
n & (n - 1)表示将n的二进制最右边的1变为0,其余位不变。
24: 00000000000000000000000000011000
23: 00000000000000000000000000010111
24 & 23:00000000000000000000000000010000
如果n & (n - 1)为0,则n是2的幂。
2: 00000000000000000000000000000010
1: 00000000000000000000000000000001
4: 00000000000000000000000000000100
3: 00000000000000000000000000000011
2 & 1:00000000000000000000000000000000
4 & 3:00000000000000000000000000000000
n & -n 提取二进制最右边的1
-n表示将n的二进制最右边的1的左侧区域(不包括1)全部按位取反。
+20: 00000000000000000000000000010100
-20: 11111111111111111111111111101100
20 & -20:00000000000000000000000000000100
03.2 按位或操作符 |
int a = 3;
int b = -5;
int c = a | b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:11111111111111111111111111111011
n | 1 << i 将二进制的i位变为1
一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要将i位修改成1,就要在i位上| 1,其他位都| 0。
以75(000000000000000000000001001011)为例,
要将2位修改成1,就要在2位上| 1,其他位都| 0:
000000000000000000000001001011
|000000000000000000000000000100
=000000000000000000000001001111
03.3 按位异或操作符 ^
int a = 3;
int b = -5;
int c = a ^ b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:11111111111111111111111111111000
异或运算支持交换律和结合律,即:
a ^ b = b ^ a (a ^ b) ^ c = a ^ (b ^ c)
a ^ a = 0 0 ^ a = a
a ^ a = 0:
如,3 ^ 3 = 00000011 ^ 00000011 = 00000000
0 ^ a = a:
如,0 ^ 3 = 00000000 ^ 00000011 = 00000011
不创建临时变量实现两个数的交换:
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a ^ b; // 10^20
b = a ^ b; // 10^20^20=10^0=10
a = a ^ b; // 10^20^10=0^20=20
printf("a = %d b = %d\n", a, b);
return 0;
}
04. 位图
数据是否存在是两种状态,可以用一个比特位表示这种状态,1表示存在,0表示不存在。
位图就是哈希表直接定址法的变形。
用位图表示{ 1,3,7,4,12,27,30,13,22,18 }中的元素是否存在:
可以创建一个int类型的变量作为位图,
基础位运算:
1. 比特位计数(简单)
方法一:
class Solution {
public:
vector<int> countBits(int n) {
vector<int> ans(n + 1, 0);
// 0的二进制中没有1,现在ans[0]已经为0,所以可以从1开始计算
for (int i = 1; i <= n; i++)
{
int n = i;
while (n)
{
// 每进行一次n&n-1操作,就能将最右边的1变为0,去掉所有1时(n为0时)结束循环
// 二进制1的个数=循环的次数
ans[i]++;
n &= n - 1;
}
}
return ans;
}
};
如果一个整数共有k位,对于每个整数,while循环最多循环k次,每次循环的时间复杂度为O(1),因此上述代码的时间复杂度为O(kn)。
方法二:
class Solution {
public:
vector<int> countBits(int n) {
vector<int> ans(n + 1, 0);
// 0的二进制中没有1,现在ans[0]已经为0,所以可以从1开始计算
for (int i = 1; i <= n; i++)
{
// i的二进制1的个数比i&i-1的二进制1的个数多1个
ans[i] = ans[i & i - 1] + 1;
}
return ans;
}
};
动态规划的思想。上述代码的时间复杂度为O(n)。
方法三:
如果正整数i是偶数,那么i相当于将i / 2左移一位的结果,因此偶数i和i / 2的二进制形式中1的个数是相同的。
如果正整数i是奇数,那么i相当于将i / 2左移一位后,再将最右边一位设为1的结果,因此奇数i的二进制形式中1的个数比i / 2的二进制形式中1的个数多1个。
例如,
3:00000000000000000000000000000011
6:00000000000000000000000000000110
7:00000000000000000000000000000111
class Solution {
public:
vector<int> countBits(int n) {
vector<int> ans(n + 1, 0);
// 0的二进制形式中没有1,现在ret[0]的值已经为0,所以可以从1开始计算
for (int i = 1; i <= n; i++)
{
// ret[i] = ret[i / 2] + (i % 2);
// 当i为非负数时,i/2=i>>1,i%2=i&1,且位运算比除法运算和取余运算效率高
// 代码优化如下:
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
};
上述代码的时间复杂度为O(n)。
2. 丢失的数字(简单)
class Solution {
public:
int missingNumber(vector<int>& nums) {
// 0^(3^0^1)^(0^1^2^3)=2
int n = nums.size();
int ans = 0;
for (auto& e : nums)
{
ans ^= e;
}
for (int i = 0; i <= n; i++)
{
ans ^= i;
}
return ans;
}
};
3. 只出现一次的数字(简单)
class Solution {
public:
int singleNumber(vector<int>& nums) {
// 0^(2^2^1)=1
int ans = 0;
for (auto& e : nums)
{
ans ^= e;
}
return ans;
}
};
4. 只出现一次的数字 II(中等)
将数组中只出现一次的元素剔除,剩下的元素都出现了三次,将这些元素的各个数位的值相加,各个数位的和一定是3的倍数。
将数组中所有元素的各个数位的值相加:
- 如果某位的和能被3整除,说明只出现1次的元素该位是0
- 如果某位的和被3除余1,说明只出现1次的元素该位是1
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0; // 只出现一次的数
// 依次修改ans的每一位
for (int i = 0; i < 32; i++)
{
int sum = 0;
// 计算所有元素的二进制i位的和
for (auto& e : nums)
{
if ((e >> i & 1) == 1)
{
sum++;
}
}
// 如果i位的和被3除余1,说明ans的i位是1
if (sum % 3 == 1)
{
ans |= 1 << i;
}
}
return ans;
}
};
5. 只出现一次的数字 III(中等)
假设只出现一次的元素是a和b,将数组所有元素异或在一起,结果是a ^ b,显然结果的二进制一定有1。假设结果的二进制的i位为1,根据这个条件将数组所有元素分为两个组:一组元素的二进制的i位为1,另一组元素的二进制的i位为0,那么a和b一定会分到不同的组中。分别将每个组的元素都异或在一起,会分别得到a和b。
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
// 将数组所有元素异或在一起
int tmp= 0;
for (auto& e : nums)
{
tmp ^= e;
}
// 提取tmp的二进制的最右边的1,假设是i位
// 如果tmp为INT_MIN,那么-tmp会溢出,所以要把它转化为无符号整型
int lowbit = tmp & -(unsigned int)tmp;
// 根据i位的不同,将元素分成两组
int a = 0;
int b = 0;
for (auto& e : nums)
{
if (e & lowbit) // 这组i位为1
{
a ^= e;
}
else // 这组i位为0
{
b ^= e;
}
}
return { a,b };
}
};
6. 消失的两个数字(困难)
给定一个数组,包含从1到N所有的整数,但其中缺了两个数字。如果把1到N所有的整数再算进去,问题就转化成:给定一个数组,包含从1到N所有的整数,其中恰好有两个元素只出现一次,其余所有元素均出现两次。和上一题“只出现一次的数字 III”类似。
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
// 将数组所有元素和1~N所有整数都异或在一起
int tmp= 0;
for (auto& e : nums)
{
tmp ^= e;
}
int N = nums.size() + 2;
for (int i = 1; i <= N; i++)
{
tmp ^= i;
}
// 提取tmp的二进制的最右边的1,假设是x位
int lowbit = tmp & -tmp;
// 根据x位的不同,将数组所有元素和1~N所有整数都分成两组
int a = 0;
int b = 0;
for (auto& e : nums)
{
if (e & lowbit) // 这组x位为1
{
a ^= e;
}
else // 这组x位为0
{
b ^= e;
}
}
for (int i = 1; i <= N; i++)
{
if (i & lowbit) // 这组x位为1
{
a ^= i;
}
else // 这组x位为0
{
b ^= i;
}
}
return { a,b };
}
};
7. 两整数之和(中等)
^:两个操作数相应的位相同为0,相异为1,可以理解为无进位加法。
&:两个操作数相应的位都为1,结果为1,可以理解为获得进位。
a + b = (a ^ b) + (a & b) << 1
显然,可以用递归:
class Solution {
public:
int getSum(int a, int b) {
if (b == 0)
return a;
return getSum(a ^ b, (a & b) << 1);
}
};
也可以用迭代:
class Solution {
public:
int getSum(int a, int b) {
while (b)
{
int carry = (a & b) << 1; // 进位结果
a = a ^ b; // 无进位加法结果
b = carry;
}
return a;
}
};
位图:
1. 判断字符是否唯一(简单)
可以创建一个int类型的变量作为位图,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……如果出现小写字母'a',就把0位变为1,如果出现小写字母'b',就把1位变为1……
class Solution {
public:
bool isUnique(string astr) {
// 鸽巢原理
if (astr.size() > 26)
return false;
int bitset = 0; // 位图
for (auto& ch : astr)
{
int i = ch - 'a'; // 字母在位图中对应的位置
if ((bitset >> i & 1) == 1) // 如果字母已经出现过
return false;
bitset |= 1 << i; // 将i位变为1
}
return true;
}
};
2. 最大单词长度乘积(中等)
创建元素为int类型的bitsets数组,每个元素都是位图。bitsets[i]记录words[i]出现的字符,如果flags[i] & flags[j] == 0,那么words[i]和words[j]没有相同字符,此时可以计算它们长度的乘积,然后和之前计算过的最大值比较,取较大的。
class Solution {
public:
int maxProduct(vector<string>& words) {
int n = words.size();
vector<int> bitsets(n, 0); // bitsets[i]记录words[i]出现的字符
for (int i = 0; i < n; i++)
{
for (int j = 0; j < words[i].size(); j++)
{
int x = words[i][j] - 'a'; // 字母在位图中对应的位置
bitsets[i] |= 1 << x; // 将bitsets[i]的x位变为1
}
}
// 如果bitsets[i]&bitsets[j]==0,那么words[i]和words[j]没有相同字符
int ans = 0;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if ((bitsets[i] & bitsets[j]) == 0)
{
int prod = words[i].size() * words[j].size();
ans = max(ans, prod);
}
}
}
return ans;
}
};