位运算就是和加减乘除等算术运算一样基础的逻辑运算,包括与或非,异或和移位。虽然非常基础,不过在日常的变成中使用的场景却不多,主要是技巧性太强,而且可读性差。即使在汇编编程中,位运算使用场景比较广的也是接口编程等比较专业的地方。但是,在底层编程,标准库等场景中,位运算还是有着不小的应用场景。另外在状态压缩,大数处理,甚至面试中,也少不了它的声音。故位运算还是值得我们去学习的,这篇文章就是我学习位运算以及刷题的一些记录。
浮点数不能位运算
基本操作原则
等价:
a=b^c
b=a^c
c=a^b
0^0=0
0^1=1
1^0=1
1^1=0
x^y=y^x
(x^y)^z=x^(y^z)
0^x=x
1^x=~x
x^x=0获取某位
// 获取 x 的第 pos 位
int getBit(int x,int pos) {
return (x>>pos)&1;
}
// 获取从 pos 开始的 cnt 位
// 原理也是先右移 pos 位,找到起始点,然后取 cnt 位,就是把和一个数相与,即 cnt 个 1,
// 32-cnt 个数的0,这个数用 (1<<cnt)-1 得出,即先左移 cnt 位,如 1000000,再减一就是
// 0111111
int getBits(int x,int pos,int cnt) {
return (x>>pos)&((1<<cnt) -1));
}可以看出来,与的作用主要就是清零,然后取某些位的作用。而与的数就是 mask 掩码。
某位置 1
// 将 x 的第 pos 位设置为 1
int setBit(int x,int pox) {
return x | (1<<pos);
}设置 1 也很简单,就是对应位与 1 相与即可。可以看出来,或运算的主要功能就是置 1.
某位清零
// 将 x 的第 pos 位设置为 0
int unsetBit(int x,int pos) {
return x & ~(1<<pos);
}设置 0 和设置 1相反,置1用或运算,而清零则用与运算,并且掩码也不一样。置1是 000100 这样只有一个1,而清零则是 11110111 这样只有一个 0.
掩码直接移位取反即可。
某位取反
// 将 x 的第 pox 位取反
int flapBit(int x,int pox) {
return x ^ (1 << pox);
}取反特定位则与清零和置一不同,用到的是异或.由于 x^0=x,x^1=~x。故我们只要掩码设为 00001000,和置 1 类似,只是变成相异或,就能设置 1 的对应位取反。
找到最低位的 1
x&(-x)=x&(~x+1). 1会和最低的 1 开始一直进位,知道为 0.最后结果就是最低位 1 的位置。而其他则会被清零。
int lowbit(int x){
return x&(-x);
}消除最后一个 1
和找到最低位 1 类似
int clear(int x){
return x&(x-1);
}判断两数是否异号
即判断两个是是否都是正数或是负数,或者一负一正。原理很简单,最高位异或,然后看最高位是不是 1,即结果是不是小于 0 即可。
bool isOppositeSign(int x,int y){
return (x^y)<0;
}判断奇偶
最后一位是 0 还是 1,因此就是取某一位判断。
bool isEven(int x){
return (x&1)==0;
}交换两数
异或的基本原理,面试的时候比较常见,不过真正使用频率并不高。
void swap(int x,int y){
x^=y;
y^=x;
x^=y;
}自增、自减
原理就是 - 的底层操作,- 的原理就是取反加一,故 -(~a) 就是 ~(~a)+1=a+1. (-a)=(~a+1)=a-1.
int inc(int x){
return -(~x);
}
int dec(int x){
return ~(-y);
}判断是否是 2 的指数
如果是 2 的指数,如 2,4,8 ,那么就必然只有一个 1.如 0001,0010,0100.那么我们清空最低位 1,再判断是否为 0 即可。
bool isTowExponent(int n) {
return (n&(n-1))==0;
}求相反数
求补,取反+1
int reverseSign(int n) {
return (~n)+1;
}求绝对值
判断正负
int abs(int n) {
int s=(n>>31);
return (s==0)?n:(~n+1);
}
// 优化
int abs(int n) {
int s=(n>>31);
return (n^s)-s;
}循环移位
c++ 没有循环移位功能,不过可以使用移位和或运算模拟,如向右循环移位 n 位
int ror(int x,int n) {
x=(x>>n)|(x<<(32-n));
}
int rol(int x,int n) {
x=(x<<n)|(x>>(32-n));
}高低位交换
int 32 位,高低 16 位交换位置,就是左移 16 位,右移 16 位,然后两者相与
int reverseLH(int n) {
return (n << 16) | (n >> 16);
}二进制逆转
取 a 的奇数位并用 0 进行填充可以表示为:a & 0xAAAA 取 a 的偶数为并用 0 进行填充可以表示为:a & 0x5555 因此,上面的第一步可以表示为:a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1)
int reverseInt(int n) {
n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1);
n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2);
n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4);
n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8);
return n;
}模拟集合操作
每一位表示一个数字,1 表示在集合中,0 表示不在集合中。

大小写转换
根据字符 ASCII 码之间的关系
// 转换为大写
char toUpper(char c){
return c&'_';
}
// 转换为小写
char toLower(char c){
return c|' ';
}
// 大小写相互转换
char exchage(char c){
return c^'c';
}求 1 的个数
求 1 的个数就是求各位的和
int bitCount_3(u32 x) {
x-= ((x & 0xAAAAAAAAu) >> 1);
x = ((x & 0xCCCCCCCCu) >> 2) + (x & 0x33333333u);
x = ((x >> 4) + x) & 0x0F0F0F0Fu;
x = (x * 0x01010101u) >> 24;
return x;
}求前缀、后缀 1 的个数
int countLeadingZeros_1(u32 x) {
int ans = 0;
if (x >> 16) x >>= 16; else ans |= 16;
if (x >> 8) x >>= 8; else ans |= 8;
if (x >> 4) x >>= 4; else ans |= 4;
if (x >> 2) x >>= 2; else ans |= 2;
if (x >> 1) x >>= 1; else ans |= 1;
return ans + !x;;
}
int countTrailingZeros_1(u32 x) {
int ans = 0;
if (!(x & 65535u)) x >>= 16, ans |= 16;
if (!(x & 255u )) x >>= 8, ans |= 8;
if (!(x & 15u )) x >>= 4, ans |= 4;
if (!(x & 3u )) x >>= 2, ans |= 2;
if (!(x & 1u )) x >>= 1, ans |= 1;
return ans + !x;
}实现三目表达式
我们可以利用表达式 y ^ ((x ^ y) & -cond) 来实现选择的功能:
若 cond = 1,其结果为 x
若 cond = 0,其结果为 y
int branch(bool cond,int x,int y){
return y ^ ((x ^ y) & -cond);
}0 和 1 转换
0 即 00000000,1 即 000000001,
0^1=000000000^000000001=000000001=1
1^1=000000001^000000001=000000000=0
int exchage(int n){
return n^1;
}构造低位全 1 的掩码
如 5 即 0101 → 000000ffh
int getMask(int x){
x|=x>>1;
x|=x>>2;
x|=x>>4;
x|=x>>8;
x|=x>>16;
return x;
}编码性质是指使用位运算处理一些编码上的问题,比如数的表示,循环码,字符编码等等。
list
题意
将数低位取反
分析
最简单的自然是从左边遍历,找到第一个 1,然后开始取反,就是个简单的获取设置位操作。
优化
要取反的话,其实和 1 异或就是,0^1=1,1^1=0.故我们只要构造掩码,即低位全 1,然后异或即可。
效率
时间复杂度:,掩码是
空间复杂度:
代码
遍历模拟
class Solution {
public:
int findComplement(int num) {
int cnt=0;
bool flag=false;
for (int i=31;i>-1;i--){
int b=getBit(num,i);
if (b==0 && !flag) {
continue;
}
if (b==1 && !flag){
flag=true;
}
num=flapBit(num,i);
}
return num;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
int getBit(int x,int pos) {
return (x>>pos)&1;
}
int flapBit(int x,int pos){
return x^(1<<pos);
}
};掩码异或(取反)
class Solution {
public:
int findComplement(int num) {
return getMask(num)^num;
}
int getMask(int x){
x|=x>>1;
x|=x>>2;
x|=x>>4;
x|=x>>8;
x|=x>>16;
return x;
}
};同 476.
题意
打印补码的最值(范围)
分析
这是 csapp 的一个 lab,有助于我们理解补码的性质。
10000000 00000000 00000000 00000000 → INT_MIN 即
00000000 00000000 00000000 00000000 → 0
01111111 11111111 11111111 11111111 → INT_MAX 即
void show range() {
cout << (~(1<<31))<<endl;
out << 0 <<endl;
cout << (1<<31)<<endl;
}
void showAll() {
cout << bitset<32> (INT_MIN) << endl;
cout << bitset<32> (-1) << endl;
cout << bitset<32> (0) << endl;
out << bitset<32> (1) << endl;
cout << bitset<32> (INT_MAX) << endl;
}
10000000000000000000000000000000
11111111111111111111111111111111
00000000000000000000000000000000
00000000000000000000000000000001
01111111111111111111111111111111题意
生成格雷编码
分析
使用数电中生成格雷编码的算法,n 位到 n+1 的过程是:
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int unsetBit(int x, int pos) {
return x & (~(1 << pos));
}
int setBit(int x, int pos) {
return x | (1 << pos);
}
int flipBit(int x, int pos) {
return x ^ (1 << pos);
}
vector<int> grayCode(int n) {
if (n == 1) {
return {0, 1};
}
vector<int> nums = grayCode(n - 1);
int m = nums.size();
int t;
for (int i = 0; i < m; i++) {
t = nums[m - i - 1];
t = setBit(t, n - 1);
nums.push_back(t);
}
return nums;
}
};题意
生成格雷码再反转
分析
生成格雷码,低位镜像对称,高位全置 1 即可。然后找到 start 位置,进行局部反转后整体反转即可。
生成格雷码的过程可参考 89 题。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int unsetBit(int x, int pos) {
return x & (~(1 << pos));
}
int setBit(int x, int pos) {
return x | (1 << pos);
}
int flipBit(int x, int pos) {
return x ^ (1 << pos);
}
vector<int> getGayCode(int n) {
if (n == 1) {
return {0, 1};
}
vector<int> nums = getGayCode(n - 1);
int m = nums.size();
int t;
for (int i = 0; i < m; i++) {
t = nums[m - i - 1];
t = setBit(t, n - 1);
nums.push_back(t);
}
return nums;
}
vector<int> circularPermutation(int n, int start) {
vector<int> nums = getGayCode(n);
int index;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == start) {
index = i;
}
}
reverse(nums.begin(), nums.begin() + index);
reverse(nums.begin() + index, nums.end());
reverse(nums.begin(), nums.end());
return nums;
}
};题意
给定的是一连串 byte 数组,验证是不是 UTF-8 编码的字符集(可能有多个字符)
分析
其实这题是一个典型的 DFA 题型,每个字节输入有不同的状态,即首字节和非首字节,非首字节又有第几字节几种情况。
不过这题状态较少,直接模拟也能写出来,就是 DFA 的跳转表写法。
由于输入字节主要有两个状态,故我们需要一个计数器表示当前输入字节是哪个状态:
如果输入的不是第一个字节,直接判断当前字节是不是 10 开头的即可。并且 cnt - -
而如果是第一个字节,则又要分多种情况。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool validUtf8(vector<int>& nums) {
int cnt=0;
for (int i=0;i<nums.size();i++){
if(cnt==0){
if((nums[i]&0b11111000)==0b11111000){
return false;
} else if((nums[i]&0b11110000)==0b11110000){
cnt=3;
} else if((nums[i]&0b11100000)==0b11100000){
cnt=2;
} else if((nums[i]&0b11000000)==0b11000000){
cnt=1;
} else if((nums[i]&0b10000000)==0){
cnt=0;
} else {
return false;
}
} else {
if((nums[i]&0b10000000)!=0b10000000){
return false;
}
cnt--;
}
}
return cnt==0;
}
};list
题意
交换两数
临时变量
最简单的就是引入临时变量,没什么好说的
class Solution {
public:
vector<int> swapNumbers(vector<int>& nums) {
int t=nums[0];
nums[0]=nums[1];
nums[1]=t;
return nums;
}
};异或
第二种就是使用位运算异或,交换变量也是基本用法之一了。
基本运算:x^x=0,x^0=x.
class Solution {
public:
vector<int> swapNumbers(vector<int>& nums) {
// nums[0]:nums[0]^nums[1], nums[1]:nums[1]
nums[0]^=nums[1];
// nums[0]:nums[0]^nums[1], nums[1]:nums[1]^nums[0]^nums[1]=nums[0]
nums[1]^=nums[0];
// nums[0]:nums[0]^nums[1]^nums[0]=nums[1], nums[1]:nums[0]
nums[0]^=nums[1];
return nums;
}
};加减法
加减法是另一个技巧,就是可能会溢出
class Solution {
public:
vector<int> swapNumbers(vector<int>& nums) {
// nums[0]:nums[0]-nums[1], nums[1]:nums[1]
nums[0]=nums[0]-nums[1];
// nums[0]:nums[0]-nums[1], nums[1]:nums[1]+nums[0]-nums[1]=nums[0]
nums[1]=nums[1]+nums[0];
// nums[0]:nums[0]-(nums[0]-nums[1])=nums[1], nums[1]:nums[0]
nums[0]=nums[1]-nums[0];
return nums;
}
};效率
三个方法复杂度都是一样的
时间复杂度:
空间复杂度:
题意
检查数字二进制的低位是否 0、1 交替,高位全零不管
分析
直接每次取最右边的位数,然后和当前应该是什么数比较即可,注意 flag 在 0 和 1 直接交替,可以用 x^1 实现。
优化
实际上,二进制的题很多都可以找规律解决掉。这题也是一样的。由于 01 交替,比如
000000101010101,那么我们将其移位
000000010101010,低位必然是一个 0 一个 1,所以我们可以异或
000000111111111,再加上个 1,就必然变成
000001000000000 这种只有一个 1 的数,所以我们可以用 x&(x-1) 清楚掉 1,结果必然等于 0.
效率
时间复杂度:模拟 ,优化后
空间复杂度:
代码
模拟
class Solution {
public:
bool hasAlternatingBits(int n) {
int flag=n&1;
while(n!=0){
n>>=1;
if ((n&1)==flag){
return false;
}
flag^=1;
}
return true;
}
};找规律
class Solution {
public:
bool hasAlternatingBits(int n) {
int m=((n>>1)^n);
if (m==INT_MAX){
m=0;
}else{
m++;
}
return (m&(m-1))==0;
}
};题意
求 [left, right] 所有数相与的结果
分析
最直接的自然是遍历一遍了,但是会超时。所以我们得找规律。由于相与只有 1 与 1 相与为 1,其它为 0,所以我们要找位数相同的部分。
假设 left=5,right=7,则
5→0101
7→0111
要相与的话,显然 01x1 都相等,但是还有中间的数呢,即 6
5→0101
6→0110
7→0111
相同的部分为 01xx,所以结果为 0100,没看出来?继续
假设 left=2,right=12,则
2 → 00000010
12→00001100
相同部分为 0000xxx1,猜测一下,中间那些相与起来会是什么?显然我们只需考虑最低位和高 4 位就行了,因为其它位已经为 0 了。
高 4 位会是什么?最低位又会是什么?
2→0000xxx0
3→0000xxx1
4→0000xxx0
5→0000xxx1
6→0000xxx0
7→0000xxx1
8→0000xxx0
9→0000xxx1
10→0000xxx0
11→0000xxx1
12→0000xxx0
我们发现高 4 位和左右边界一样都是 0000,而最低位则一直在 0,1 间交替,所以结果相同的部分应该是 0000xxxx,最后结果即为 0.
根据上面的分析,其实我们可以得出结论了:
所以:数相与起来,就是 left,right 的相同前缀构成的数。 因为高位不变,低位交替相与就是 0。比如 5 和 7 的公共前缀就是 01xx,2 和 12 的公共前缀就是 0000xxxx。
那么问题就变成:怎么寻找两个数的公共前缀。
最简单的自然是模拟了,从左至右扫描一遍即可。
优化
我们也可以依次消除 right 最右边的 1,知道刚刚小于 left,这时的 rigth 就是公共前缀。
效率
时间复杂度:
空间复杂度:
代码
暴力
class Solution {
public:
int rangeBitwiseAnd(int left, int right) {
int res=0xffffffff;
for (int i=left;i<=right;i++){
res&=i;
}
return res;
}
};模拟
class Solution {
public:
int rangeBitwiseAnd(int left, int right) {
int res=0;
int a,b;
for (int i=31;i>-1;i--){
a=getBit(left,i);
b=getBit(right,i);
if (a!=b){
return res;
}
if (a==1){
res=setBit(res,i);
} else {
res=unsetBit(res,i);
}
}
return res;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
};消除 1
class Solution {
public:
int rangeBitwiseAnd(int left, int right) {
while(left<right){
right&=(right-1);
}
return right;
}
};题意
检查哪些字符串是由给定字符组成的
分析
要检查字符是否符合要求,就是个存在性问题,所以使用哈希表。而这题只检测字符,范围只有 24 个,所以我们可以用一个 int 变量来模拟哈希表,这样更简单一点。
实际上,一些问题上经常可以可以这样简化哈希表的作用,比如统计字符个数,数字个数等,就可以用一个 24 或 9 长度的数组来代替,或者像这题一样,只是检测有限字符的存在性问题,也可以用 bitmap 来代替,而这题则比 bitmap 还要简化一点,即一个 int 变量就行了。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int countConsistentStrings(string allowed, vector<string>& words) {
int cnt=0;
int res=0;
for (int i=0;i<allowed.size();i++){
cnt=setBit(cnt,allowed[i]-'a');
}
for (int i=0;i<words.size();i++){
res++;
for (int j=0;j<words[i].size();j++){
if (!isExist(cnt,words[i][j])) {
res--;
break;
}
}
}
return res;
}
bool isExist(int x,char c){
return getBit(x,c-'a')==1;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
};题意
求所有连续子序列或之后结果的个数
分析
最简单的自然是直接双重循环生成所有连续子数组,然后在生成的过程中求得相或的结果。但是要得到的是不同结果的个数,即需要有一个去重的过程,故直接用 set 来暂存即可。
优化
本题数量级在 10^5,故暴力 显然会超时,故我们需要考虑优化。而最简单的优化就是进行一些剪枝操作,而不用更改基本思路。
我们是怎么生成子数组的?在前一个子数组的基础上增加元素。而我们要求的是什么?或运算。
这里我们就需要利用到或运算的性质了:一个数与另一个数或的结果,结果必然单调不减。
其实也很好理解,毕竟或运算,原来的 1 还是 1,而 0 要么变 1,要么不变,总的来说数自然是在递增的了。
所以说,我们固定左边界,右边界增加元素的过程中,所得到到子集或结果,也是递增的。
而可以剪枝的地方就在这里:由于结果递增,那么如果这个结果已经达到了最大值,后面的所有子集或结果仍然是最大值,而我们只需要知道结果的个数,所以,后面子集不运算也不会影响结果。
故,我们可以进行如下剪枝:
那么现在问题就转变为:怎么求或的最大值。
我们前面说了,增加元素的或结果是递增的,那么元素最多时,自然结果必然就是最大值。
所以,我们直接求得所有元素的或结果,就是最大值。
效率
时间复杂度:
空间复杂度:
代码
暴力
class Solution {
public:
int subarrayBitwiseORs(vector<int>& nums) {
unordered_set<int> s;
int t=0;
for (int i=0;i<nums.size();i++){
t=nums[i];
s.insert(t);
for (int j=i+1;j<nums.size();j++){
t|=nums[j];
s.insert(t);
}
}
return s.size();
}
};剪枝
class Solution {
public:
int subarrayBitwiseORs(vector<int>& nums) {
unordered_set<int> s;
int t=0;
int maxValue=0;
for (int i=0;i<nums.size();i++){
maxValue|=nums[i];
}
for (int i=0;i<nums.size();i++){
t=nums[i];
s.insert(t);
for (int j=i+1;j<nums.size();j++){
t|=nums[j];
s.insert(t);
if (t==maxValue){
break;
}
}
}
return s.size();
}
};题意
找出包含一个 0 的最长 1 的个数
分析
本题就是一个简单的变长滑动窗口,只是套了一个位运算而已。
窗口中的状态就是含有 0 的个数,当小于等于一个时扩展,等于 1 个时收缩,每次扩展扩展收缩后更新长度即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int reverseBits(int num) {
int res=0;
int cnt=0;
int l=0;
int r=0;
while(r<32){
if (getBit(num,r)==0){
cnt++;
}
while(cnt==2){
if (getBit(num,l)==0){
cnt--;
}
l++;
}
r++;
res=res>(r-l)?res:(r-l);
}
return res;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};题意
将 M 中的位数设置到 N 中
分析
遍历 M,然后写入 N 中对应位即可
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int insertBits(int N, int M, int i, int j) {
for (int k=i;k<=j;k++){
if (getBit(M,k-i)==1){
N=setBit(N,k);
} else {
N=unsetBit(N,k);
}
}
return N;
}
int getBits(int x,int pos,int cnt){
return (x>>pos)&((1<<cnt)-1);
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
int setBit(int x,int pos){
return x|(1<<pos);
}
};题意
有两种特殊字符。第一种字符可以用一比特 0 来表示。第二种字符可以用两比特 (10 或 11) 来表示。
现给一个由若干比特组成的字符串。问最后一个字符是否必定为一个一比特字符。给定的字符串总是由 0 结束。
分析
最简单的就是模拟,要问最后是不是只是一个 0,那么从左边开始放位置,遇到 1 就跳过一个位置,遇到 0 就继续,看最后是不是就是一个 0 即可。
优化
其实这题在分布上有一点规律,我们知道,一共三种分布:
0
10
11
我们从最后开始往前扫描,显然最后一个比较为 0 才行,即
xxx0
如果 0 前面没有 1,即
0000
显然是满足的
如果 0 前面只有一个 1,即
xxxx010
再往前一位就是 xxx0010 或 xxx1010
对于 xxx0010,连续两个 0 必然会被消掉,无论是 xx x0 0 10 还是 xxx 0 0 10 这两种情况,所以最后剩下的必然是 10 两位,即不满足。
同理,对于 xxx1010,只有 xxx x1 0 10 或 xxx 10 10 这两种情况,最后 10 还是不满足。
如果 0 前面有两个 1,即
xxx0110
那么就有 xx x0 11 0 和 xxx 0 11 0 这两种消除的情况,所以满足
对于三个 1 的,根据上面的规律,不满足。
所以我们可以找到规律了,即对于最后 xxxx0111...1110 中 1 的个数,如果是奇数,则不满足,如果是偶数,则满足。
效率
时间复杂度:
空间复杂度:
代码
模拟
class Solution {
public:
bool isOneBitCharacter(vector<int>& nums) {
int i;
if (nums[nums.size()-1]==1){
return false;
}
for (i=0;i<nums.size()-1;i++){
if (nums[i]==1){
i++;
}
}
return i==nums.size()-1;
}
};统计最后 1 的个数
class Solution {
public:
bool isOneBitCharacter(vector<int>& nums) {
int cnt=0;
if (nums[nums.size()-1]==1){
return false;
}
for (int i=nums.size()-2;i>-1;i--){
if (nums[i]==1){
cnt++;
} else {
return cnt%2==0;
}
}
return cnt%2==0;
}
};题意
给定一个正整数 n ,你可以做如下操作:
如果 n 是偶数,则用 n / 2 替换 n 。
如果 n 是奇数,则可以用 n + 1 或 n - 1 替换 n 。
n 变为 1 所需的最小替换次数是多少?
分析
基本操作如下:
要把一个数变成 1,要把 n 变成 1,即 0000 0001,所以最后的结果,必然只有一个 1。
无论这个 1 出现在什么地方,后面的操作都是移位。
显然当低位为偶数时,移位不会影响 1 的个数,所以要使得满足 1 的个数,最后得奇数时,即低位为 1 时进行操作才行。
问题就在于,当低位为 1 时,我们究竟是 ++,还是 — 呢?也就是说,每次当低位为 1 时,我们都需要从两个策略中选择一个,问题就在于,怎么选。
直接看,似乎看不出来,来个例子
假设数 n 为 0111 1110,我们该怎么做呢?
现在问题来了,低位是 1,该怎么选。
假设++,变成: 0100 0000
假设—,变成 : 0011 1110
我们看到,++ 的时候,似乎 1 的个数一下子就从 6 个变成了 1 个,而 — 则还有 5 个。而我们的目的就是要通过 ++ 或 — 减少 1 的个数。
那么为什么,上面 ++ 的时候能一下子减少这么多 1 的呢?其实很简单,因为最后是 xx111,一些 1 相邻,+1 自然会把这些 1 全部都消除掉。
我们现在只来看最后数的两位:
00 → 偶数
01
10 → 偶数
11
显然,00 和 10 结尾的都是偶数,所以不管。问题在于,以 01 结尾的奇数和 11 结尾的奇数怎么操作。
按照我们上面的分析,以 11 结果的操作,进行 ++ 时,能一下子消除更多的 1.那么对于以 01 结尾的呢?
01+1 = 10
01-1 = 00
显然,01 结尾的进行 — 时消除的 1 个数更多。
所以现在我们能够进行如下算法:
但是上面还有一个例外,就是 3,3 是 0011,但是如果先 ++ 变成 0100,就要再移两次,一共三次操作。而直接 - - 变成 0010,再 - - 变成 0001,就只有两次操作。
所以根据上面的分析,我们知道了算法的思想,就是某次 ++ 或 - - 时,贪心的选择能消除 1 的个数最多的那个操作。
但是根据上面 3 那个意外,我们也发现似乎这个贪心不一定就全部满足,所以:
贪心选择消除 1 最多的操作 就一定等于 最后操作次数最少吗?
这个就需要证明了。
证明:略(我也不会)
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int integerReplacement(int t) {
int res=0;
long n=t;
while(n!=1){
if ((n&1)==0){
n>>=1;
} else if (((n&0b11)==0b11) && (n!=3)){
n++;
} else {
n--;
}
res++;
}
return res;
}
};异或是一个等价关系,应用非常广泛。
list
题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
暴力枚举
class Solution {
public:
int findMaximumXOR(vector<int>& nums) {
int res=0;
for (int i=0;i<nums.size();i++){
for (int j=i+1;j<nums.size();j++){
res=max(res,nums[i]^nums[j]);
}
}
return res;
}
};题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
题意
由差分异或得出前缀异或
分析
这题就是前缀数组和差分数组的一个变形,只是运算变成异或了而已,而已相互转换都是异或运算,而不是加减了。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> decode(vector<int>& nums, int a) {
nums.insert(nums.begin(),a);
for (int i=1;i<nums.size();i++) {
nums[i]^=nums[i-1];
}
return nums;
}
};题意
求数组异或的结果
分析
找个锤子规律
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int xorOperation(int n, int start) {
int res=0;
for (int i=0;i<n;i++) {
res^=(start+i*2);
}
return res;
}
};题意
给定差分异或数组,求原数组,且原数组是 1~n 的数组排列(答案唯一)
分析
已知
则
而已知 encoded 数组,故我们只需要知道 perm[0],就能递推求出整个 perm 数组。现在问题就是:怎么求出 perm[0]?
我们来看一下题目中还有哪些条件没有使用到:
直接看上面似乎看不出来,实际上,我们可以进行如下推理
则
而 x 和 e 数组都是已知的,故我们先通过上面的公式得出 p[0],再通过递归公式求出整个 perm 数组即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> decode(vector<int>& e) {
vector<int> p(e.size()+1);
for (int i=1;i<=p.size();i++){
p[0]^=i;
if ((i%2!=0) && (i<e.size())){
p[0]^=e[i];
}
}
for (int i=1;i<p.size();i++){
p[i]=p[i-1]^e[i-1];
}
return p;
}
};题意
可以对任意列进行反转,求最后行相等的个数
分析
如果行元素相等这个可能有很多种,可能有的行都是 1,有的行都是 0,而同时考虑很多行比较麻烦,但是只考虑一行的话,就很简单。
首先,由于可以对任意列进行反转,那么任意给定一行,我们总能将其变成行相等的情况。这是没有问题的。
那么假设数组分成很多不同类型的行,比如 1101 的有 a 种,0011 的有 b 种,那么我们要进行反转的话,只需要找到类型最多的那种行进行反转就行了。
但是现在问题就出现了:
反转类型 a 的行,类型 b 的行会不会同时也变成行相等的情况呢?
比如说
001
001
110
110
现在 001 类型和 110 类型都有两个,我们对 001 进行反转的时候,假如反转成 000,但是这个时候,110 类型的行也同时变成 111 这种行相等的情况了,所以最后的结果不是 2 而是 4 了。
那么我们该怎么解决这个问题呢?或者说,有没有什么办法,把行的类型变成反转的时候不会影响其它行的情况呢?
我们来看一下那些看上去明明类型不同,但是反转后却同时满足行相等的行,是什么情况:
001
110
11101
00010
11101010
00010101
发现了吗?没错,这些行其实就是完全取反后的,如果两个行之间取反相等,那么对它们进行列上的反转时,它们会同时达到行相等的情况。
那么现在问题就很简单了:
我们可以把数组分成很多不同类型行的组合:比如 a 和 a 取反表示一类,如 110,001 表示一类,111 和 000 表示一类。
然后找到类别最多的那种行进行反转,就是最后行相等最大的行数。
有人可能觉得行取反还是一个类型感觉有点麻烦,那我们就预处理一下嘛,先把那些表示相反的行取反,转换成同一类。比如以第一位为例,0 开头取反就是 1 开头的,那我们把所有 1 开头的行取反,就全部变成 0 开头的了,就不存在取反还是同一类的情况了。
由于要统计不同类别的个数,所以直接用个哈希表暂存一下即可,最后遍历取最值。
总结一下,就是开一些桶,某个时刻只有一个桶能满足行相等。映射桶的方式也很简单,某行和这个行所有元素取反的行映射到同一桶。最后找出最多的个桶即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int maxEqualRowsAfterFlips(vector<vector<int>>& mat) {
unordered_map<string,int> m;
string s;
int res=0;
for (int i=0;i<mat.size();i++){
s.clear();
for (int j=0;j<mat[0].size();j++){
if (mat[i][0]==1){
s+=('0'+ (mat[i][j]^1));
} else {
s+=('0'+ mat[i][j]);
}
}
m[s]++;
}
for (auto item:m) {
res=max(res,item.second);
}
return res;
}
};在寻找某数的题型中,通常和涉及到数字个数的,如某数重复,缺失,出现多少次等等题型,也是比较常见的位运算题目。
重复缺失个数确定,像偶数个奇数个确定的,通常可以用异或消除重复元素。
涉及多个奇数的,通常要将它们分离开。
重复个数不确定的可以考虑用位数统计,当然确定的也能用,算是比较万能。
更复杂一些的,可以建立 DFA 再编码解决。
通常需要知道位为 1 的地方,可以用 x&-x 快速确定。
对于范围小于 32,重复次数最多 2 次的,可以用一个 int 模拟哈希表,比如 0~9,大小写字母重复一次等。
此外,还有和位运算无关的哈希,排序遍历,计数统计,原地置换 ,set 等方法。
list
题意
求 n-1 长度的数组中,缺失的数字
分析
重复、缺失数字是典型的异或应用场景,异或的几大运算法则如下:
x^x=0,x^0=x,
故,我们可以用 0~n 共 n+1 个数字和原数组进行异或,那么除了缺失的那个数字,其它数字都有两个相同的数异或,为 0,故最后就变成缺失的那个数和 0 异或,结果就是缺失的那个数。
面试题 17.04,剑指 Offer 53,268 题,三个题一模一样.
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int missingNumber(vector<int>& nums) {
int res=nums.size();
for (int i=0;i<nums.size();i++){
res^=i;
res^=nums[i];
}
return res;
}
};题意
数组中除了一个数只出现一次,其它都出现两次,找出这个出现一次的数字
分析
最自然的方法有排序然后遍历,也可以遍历哈希或辅助数组。当然,利用数组出现一次两次这个条件,是异或的典型应用场景。
即 x^x=0,0^x=x,利用这两个公式,那么所有重复的数异或就为 0,最后 0 再和余下的数字异或,就是这个出现一次的数字了。
优化
异或时有个小技巧,我们不用开个 res 变量来保存结果,直接用 nums[0] 来存结果即可。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int singleNumber(vector<int>& nums) {
for (int i=1;i<nums.size();i++){
nums[0]^=nums[i];
}
return nums[0];
}
};题意
t 比 s 多一个字符,找到该字符
分析
这题就是 136 稍微变了一下,只是分成两个集合而已,本质上还是情侣中找单身狗,即只有一个出现一次,其它出现两次。
最简单的自然是用异或消除重复了,两个全部异或起来,结果就是单身狗。
稍微复杂点的就是根据抽屉原理,s 所有字符在对应位的和,一定比 t 对应和小。因为 s 中有的 t 都有,多出来的就是多的元素了。
参考 136,287 等题即可。
优化
可以把结果存在 s[0] 中,不用新开变量.
效率
时间复杂度:
空间复杂度:
代码
异或
class Solution {
public:
char findTheDifference(string s, string t) {
if (s.empty()){
return t[0];
}
for (int i=1;i<s.size();i++){
s[0]^=s[i];
s[0]^=t[i];
}
s[0]^=t[0];
s[0]^=t[t.size()-1];
return s[0];
}
};位数统计
class Solution {
public:
char findTheDifference(string s, string t) {
int cnt1;
int cnt2;
char res=0;
for (int offset=0;offset<8;offset++){
cnt1=0;
cnt2=0;
for (int i=0;i<s.size();i++){
cnt1+=((s[i]>>offset)&1);
}
for (int i=0;i<t.size();i++){
cnt2+=((t[i]>>offset)&1);
}
if (cnt2>cnt1){
res|=(1<<offset);
}
}
return res;
}
};题意
长度为 n+1 的数组,里面的数字在 1~n 范围内,且有一个元素至少重复出现1次,找出该元素。
分析
注意是至少重复1次,即至少 2 个一样的,个数 ≥2,不是等于 2.如果等于 2 的话,直接异或就行了,这是第一点。
要找重复元素,其实是典型的哈希表应用场景,故可以用哈希表来做。不过由于要空间复杂度 ,就只能考虑用位来代替普通的哈希表了,即用 1个int变量来表示,每位表示一个数字。但是,由于只有 32 位,故最多表示 32 个数,可以用在字母,或一个数字上,但是这题范围 1 <= n <= 10^5 ,故还是不行。
异或不行,哈希也不行,重复次数还不一定,那就只能考虑位数统计了。即类似于 137 题,根据抽屉原理,原先 1~n 数在每一位的个数是一定的,新数组在对应位如果更多,就必然是重复元素的。
故我们统计 1~n 对应位,即数组每个数对应位,求和比较即可。由于要空间复杂度,故用变量暂存结果。
优化
可将统计两个数组的位数时同时进行,只是 1n 变成统计 0n 对应位而已,但是结果不变。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int cnt1;
int cnt2;
int res=0;
for (int offset=0;offset<31;offset++){
cnt1=0;
cnt2=0;
for (int i=1;i<nums.size();i++){
cnt1+=((i>>offset)&1);
}
for (int i=0;i<nums.size();i++){
cnt2+=((nums[i]>>offset)&1);
}
if (cnt2>cnt1){
res|=(1<<offset);
}
}
return res;
}
};class Solution {
public:
int findDuplicate(vector<int>& nums) {
int cnt1;
int cnt2;
int res=0;
for (int offset=0;offset<31;offset++){
cnt1=0;
cnt2=0;
for (int i=0;i<nums.size();i++){
cnt1+=((i>>offset)&1);
cnt2+=((nums[i]>>offset)&1);
}
if (cnt2>cnt1){
res|=(1<<offset);
}
}
return res;
}
};题意
一个数字出现 1 次,其余出现 3 次,找出出现 1 次的数字。
位数统计
看上去这题似乎和 136,168 类似,应该用异或来消除重复的元素。不过很可惜,这个题并不能使用异或。
异或的使用场景应该是 1 个 奇数或偶数个,不过很可惜,这题全是奇数个。我们应该考虑 3 和 1 这个特殊的数字。换句话说,一个数只出现 3 次能说明什么?能被 3 整除。出现一次呢?不能被 3 整除吗?显然不是,因为这个数可能就是 3 的倍数。
虽然对整个数字来说,上面的思路似乎有些问题。不过在位运算题目中,显然我们也可以考虑每一位。那么对于任意一位,出现 3 次,显然和为 3,出现一次和就为 1。换句话说,如果把所有数的相应位加起来,就应该是 1331133 这种形式。相应位为 1 显然就是我们要求的那个数字的位数。
所以思路很明确了,我们把所有对应位的和求出来,然后看哪些是 1,然后组成的数就是要求的数,如 1331133 显然就是 1001100。
class Solution {
public:
int singleNumber(vector<int>& nums) {
vector<int> t(32);
int res=0;
for (int offset=0;offset<32;offset++){
for (int i=0;i<nums.size();i++){
t[offset]+=((nums[i]>>offset)&1);
}
}
for (int i=0;i<32;i++){
res+=((t[i]%3)<<i);
}
return res;
}
};我们需要对每一个数统计 32 个位,故 ,空间复杂度用了一个 32 int 的数组,应该算 .
组合逻辑电路设计
如果说上面位数统计算是比较常见和基础的话,下面的组合逻辑就有点专业了,毕竟需要数电的基础知识,并且还有一些 DFA 的概念。我看了一个小时才看懂题解,如果没有学过数电可能就看不懂了。
在位数统计中,我们是根据每位数的出现次数来判断最后这一位应该是什么的,我们是把数据的位数存下来了。在上面位数统计中,我们知道,一位数要么出现 0 次,要么出现 1 次,要么就出现 3 的倍数次。
因为 3 的倍数次最后还是要对 3 取余,那么起始就等价于出现 0 次。换句话说,我们可以建立一个状态机,对于每一位数,它的状态一定在状态机中。建立状态机的思想,这是这题第一个要点。
状态机有三种状态:a,b,c 分别表示出现 0,1,2 次。
状态转移有两种方式:输入 0 或 1.
如下图
对于每一位,它就应该满足这个 DFA 的状态转移方式(不知道 DFA 则需要先学习下状态机)。
注意,是对于每一位满足这个 DFA。(初始状态我们都设为 a,即出现 0 次)。
如 [2,2,3,2],即 [0010,0010,0011,0010],对于最高位,出现次序为 0,0,0,0,那么在 DFA 中,显然就是 a→a→a→a→a 这几种转移过程,对于低第二位,次序为 1,1,1,1,则转移过程为 a→b→c→a→b,最后一个次序为 0,0,1,0,转移过程为 a→a→a→b→b.
那么最后所有位的状态应该是 aabb。
而 a 即表示该为出现 0 次,那么最后该为 0。b 表示出现 1 次,该为 1.故 aabb 就是 0011,即 3.所以最后结果为 3.
需要注意的是,由于出现次数要么 0,要么 1 要么 3,而 3 次最后就是 0 次,所以该 DFA 的终止状态应该是 a 或 b。
画出上面的 DFA 后,有两种方式来实现它:
如果按 DFA 常规方式去处理的话,显然太麻烦了。所以我们可以按第二种方式来做。使用组合逻辑电路思维来设计,这是这题第二个要点。
由于一共有 3 个状态,所以需要两位才能表示每一个状态。我们假设 xy 为当前状态,那么设
| xy | 状态 |
|---|---|
| 00 | a |
| 01 | b |
| 10 | c |
根据数电中组合逻辑电路知识,我们知道上面这个 DFA 其实就是个组合逻辑电路,要设计电路的话,我们需要知道输入状态,输出状态,然后画出真值表,再求出表达式,最后化简,再画出电路图。而在这个题中,我们只需求出表达式即可。
真值表其实就是上面 DFA 的表格化,就是当前状态是什么,输入状态是什么,下一个状态又是什么
| 当前状态 xy | 输入 | 下一个状态 xy |
|---|---|---|
| 00(a) | 0 | 00(a) |
| 00(a) | 1 | 01(b) |
| 01(b) | 0 | 01(b) |
| 01(b) | 1 | 10(c) |
| 10(c) | 0 | 10(c) |
| 10(c) | 1 | 00(a) |
求出表达式:
注意了,上面的 x,y,x,y 都是二进制数。我们求得是对于一位 来说,它是怎么转换状态的。而我们要求的是 int 类型,有 32 位,所以,如果按照这种一位的求法,我们需要计算 32 次。
也就是遍历每一个数字,然后对某位数进行上面这样的转换,那么问题来了:
既然每一位都要进行相同的计算,而且不同位之间的计算是相互不影响的(按位运算,位运算就是不影响的),那我们为什么不一个数字的 32 位同时进行运算呢?
这就是优化的第一点了,我们可以用两个 int 的变量之间来表示某个时刻,某个数,32 位的当前分别在对应 32 个状态机中的状态。 也就是常见的状态压缩。而使用状态压缩,这是这题第三个要点。
和普通一位进行状态转移的表达式是一样的,只是之前是某一位,现在是 32 一起而已。
假设最后结果状态为 x,y,按照我们上面的分析,其实就是 a 状态和 b 状态的一些组合。那么问题来了,我们怎么根据这个状态,得到我们想要的数字结果呢?
这就有一点编程的技巧性了,由于我们之前对状态的编码是 x(0),y(1),而我们知道最后结束状态一定是 a 或 b。换句话说,假设处于 a 状态,那么 当前编码就是 xy=00。而如果当前处于 b 状态,编码 xy=01.
还记得 a 状态和 b 状态表示什么意思吗?a 是出现 0 次,b 是出现 1 次。那么自然的,在对应位,出现 0 次就是 0,出现一次就是 1。也就是说,如果该位状态是 a,那么就应该是 0,如果是 b,就应该是 1。而这,和对应编码 xy 的 y 是一模一样的。
换句话说:最后状态编码 xy 中的 y,对应的数字,恰好就等于我们要得到的结果 ,这一点非常巧妙,也是本题最后一个要点。
如果我们之前编码 a,b 时,对应 xy 处理得不好,那么显然就得不到这么巧合的结果了。
最后我们再来总结一下这个题的要点:
当然,上面由于我们求的表达式是直接根据真值表得到的,也可以优化一下,不过那就更是纯粹的数电知识了,就不深入了。
最后再来分析一下复杂度:
我们只遍历了一遍,时间复杂度就为 ,只用到几个辅助变量,故空间复杂度为 .
class Solution {
public:
int singleNumber(vector<int>& nums) {
int x=0;
int oldX=0;
int newX=0;
int y=0;
bool a,b;
for (int z=0;z<nums.size();z++){
oldX=x;
newX=((~oldX)&y&nums[z])|(oldX&(~y)&(~nums[z]));
y=((~x)&(~y)&nums[z])|((~x)&y&(~nums[z]));
x=newX;
}
return y;
}
};题意
数组中有一个缺失的,一个重复的元素,要找到这两个元素。
分析
这题看上去有点像 136,268 这几个题,缺失元素或重复元素,典型的异或场景。异或的作用主要是消除重复的元素,由于 x^x=0,x^0=x,且异或运算是二元运算,即满足结合,交换律。
所以这题我们也可以用 1~n 的元素进行异或,那么就可以消除掉原本存在奇数次的元素,即那么存在的,自然就只有重复的元素,并且还有一个元素缺失也无法消除掉,故假设重复元素为 a,缺失元素为 b,那么最后异或的结果就是 a^b.
但是显然,我们想要的是两个数,而现在得到的却是一个数,说明我们的思路出了一点问题。像 136,268 这种题都是集合里只有一个特殊元素的,而这题却又两个。所以,如果有什么办法,能把集合划分为两个集合的话,分别对它们异或就能得到结果了。
比如 [1,2,2,4],我们将其和 1~4 集合合并,即 [1,1,2,2,2,3,4,4] 集合,将新集合全部异或得到的结果就是 2^3.而如果我们能将该集合划分为两个集合,如 [1,1,2,2,2],[3,4,4],然后分别异或就能得到结果 2 和 3 了。
要划分的话,显然需要找到一个条件,然后一个集合满足,一个不满足,即变成二元状态,而与运算就可以做到。
我们再来看,由于重复元素和缺失元素一定不相等,那么 a^b 一定不等于 0,换句话我,a^b 转换为二进制,那么,必然有一位为 1. 而这个 1 同样也是 a 和 b 对应位异或而来的。而异或要为 1,只能两个数一个 0 一个 1才行,即 1^0=1.
换句话说,重复元素和缺失元素在某一位上,一定是一个为 1 一个为 0 的。而我们知道 1&1=1,1&0=0。所以如果我们将所有元素的这一位与 1 相与,那么我们就能将重复元素和缺失元素划分到两个集合中。且相同元素自然也是划分到一个集合中的。
比如,对于 1,2,3,4 三个数,将数字转换为二进制,有
1 → 0001
2 → 0010
3 → 0011
4 → 0100
而重复数字 ^ 缺失数字= 2^3=0010^0011=0001,显然最后一位就是 1.我们将集合 [1,1,2,2,2,3,4,4] 每个数最低位与 1 相与,就能划分为集合 [1,1,3],[2,2,2,4,4],然后分别相与,就能得到结果 3 和 2.
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> findErrorNums(vector<int>& nums) {
int xorTmp=0;
int tmp=0;
int a=0;
int b=0;
int offset=0;
// 和 [1,size] 的数相与
for (int i=0;i<nums.size();i++){
xorTmp^=(i+1);
xorTmp^=nums[i];
}
// 这时 xorTmp = a^b
tmp=xorTmp;
// 找到 tmp 对应位为 1 的偏移量,即 offset
while((tmp&1)==0){
offset++;
tmp>>=1;
}
// 遍历数组,然后将其划分为两部分
for (int i=0;i<nums.size();i++){
if (((nums[i]>>offset)&1)==0){
a^=nums[i];
} else {
b^=nums[i];
}
if ((((i+1)>>offset)&1)==0) {
a^=(i+1);
} else {
b^=(i+1);
}
}
// 判断谁是重复的元素
for (int i=0;i<nums.size();i++) {
if (nums[i]==a) {
return {a,b};
}
}
return {b,a};
}
};题意
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。找出这两个数字。
分析
这题就是 645 的简单版,645 还要新组成集合,而这个已经弄好了。参考 645 即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
int tmp=0;
int offset=0;
int a=0;
int b=0;
for (int i=0;i<nums.size();i++){
tmp^=nums[i];
}
while(((tmp>>offset)&1)==0){
offset++;
}
for (int i=0;i<nums.size();i++){
if (((nums[i]>>offset)&1)==1){
a^=nums[i];
}
}
return {a,tmp^a};
}
};题意
分析
参考 645,剑指 Offer 56 - I. ,基本一样
题意
给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O (N) 时间内只用 O (1) 的空间找到它们吗?
分析
参考 645,260,剑指 Offer 56 - I. ,基本一样
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
int xorTmp=0;
int tmp=0;
int a=0;
int b=0;
int offset=0;
// 和 [1,size] 的数相与
for (int i=0;i<nums.size();i++){
xorTmp^=nums[i];
}
// 和 [1,size] 的数相与
for (int i=1;i<=nums.size()+2;i++){
xorTmp^=i;
}
// cout<<xorTmp<<endl;
// 这时 xorTmp = a^b
tmp=xorTmp;
// 找到 tmp 对应位为 1 的偏移量,即 offset
while((tmp&1)==0){
offset++;
tmp>>=1;
}
// 遍历数组,然后将其划分为两部分
for (int i=0;i<nums.size();i++){
if (((nums[i]>>offset)&1)==0){
a^=nums[i];
} else {
b^=nums[i];
}
}
for (int i=1;i<=nums.size()+2;i++){
if (((i>>offset)&1)==0){
a^=i;
} else {
b^=i;
}
}
return {b,a};
}
};题意
一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。且数组中有多个数是重复的,找出一组重复的数。
分析
最简单的自然排序然后遍历,虽然空间复杂度 ,但是时间复杂度是 ,要优化的话,可以空间换时间,用哈希表,但是这样虽然时间复杂度变成线性 了,但空间复杂度又增加了 .
那么有没有办法再进行优化呢?
优化
想要时间复杂度不增加,原数组又无序,显然只能哈希了,但是如果哈希的话,又要新开数组存值,那么有办法降低这个空间吗?
通常来说有两种方法:
位图思想很容易理解,我们只需要知道一个数是否存在,不需要知道它是什么。于是,而是否存在这个信息只需一位就能保存,所以我们可以用位图来存,可以新开 bitset,如果数字范围比较小的话,还能用一个 int 来模拟位图。
虽然位图也能解决,但是就没有更优化的策略了吗?其实是有的,那就是原地哈希。由于这个题给定的数字比较特殊,即数组长度为 n,数字范围却是 1~n-1。那么,我们可以借用这个数组本身,实现哈希的功能。
哈希表最主要的是映射规则,而在这里,我们的映射规则也很简单:i→i,即 key 和 value 是一样的。即 nums[i]=i,换做哈希写法,就是 map[i]→i.
而由于有重复元素,所以必然最终有两个数映射到一个位置中,那么我们再遍历的过程中,只要判断对应位置中,是否已经有了元素,有的话,这个元素就是重复的。
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
for (int i=0;i<nums.size();i++){
if (nums[i]==i){
continue;
}
if (nums[i]==nums[nums[i]]){
return nums[i];
}
if (nums[nums[i]]==nums[nums[nums[i]]]) {
return nums[nums[i]];
}
swap(nums[i],nums[nums[i]]);
}
return 0;
}
};题意
找到出现次数超过一半的元素
分析
典型的位数统计题目,直接扫描某个数字的每一位,相加然后统计,如果超过一半,说明该位就是 1,否则为 0.
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
vector<int> t(32);
int res=0;
for (int i=0;i<32;i++){
for (int j=0;j<nums.size();j++){
t[i]+=((nums[j]>>i)&1);
}
}
for (int i=0;i<32;i++){
if ((t[i]<<1)>nums.size()) {
res=setBit(res,i);
} else {
res=unsetBit(res,i);
}
}
return res;
}
int setBit(int x,int pos) {
return x | (1<<pos);
}
int unsetBit(int x,int pos){
return x & (~(1<<pos));
}
};个数统计主要是统计 1,0 的个数。
list
题意
计算二进制数中 1 的个数
枚举
最简单的自然是枚举了,遍历 32 位,是 1 的就统计即可。
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt=0;
for (int i=0;i<32;i++){
cnt+=getBit(n,i); // 位 1 的个数也等于所有位的和
}
return cnt;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};或移位到最低为判断
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt=0;
while(n!=0){
cnt+=(n&1);
n>>=1;
}
return cnt;
}
};枚举的时间复杂度为
消除位优化
在枚举时,我们是遍历位再判断,实际上,我们可以使用 x&(x-1) 直接清楚最低位的 1,然后直到为 0 为之。
class Solution {
public:
int hammingWeight(uint32_t n) {
int cnt=0;
while(n!=0){
n=(n&(n-1));
cnt++;
}
return cnt;
}
};时间复杂度为
分治
实际上,我们还有一种分治的做法,前面说了,求 1 的个数其实就是求各位的和。也就是,求 32 位的和。
而类似于归并,我们可以先求 16 位的和,再归并。求 16 的又通过先求 8 位的和,求 8 位的又先求 4 位的,求 4 位的又求 2 位的。
所以,我们反过来,先求所有相邻 2 位的和,再求相邻 4 位的话,再求相邻 8 位的和,再求相邻 16 位的和,最后求 32 位的和即可。
并且,我们就利用原遍历存中间结果,不用递归什么。要求几位的和,那么相应低几位就是和。比如求相邻 8 位的和,那么 4 4 右边那 4 位就是两个 4 位的和。
假设我们要求 00001011 这一个字节中 1 的个数,那么
我们要求相邻2位的和,显然,我们需要把两位每位取出来
数取出来后,要相加的话,显然要对齐才行,我们需要把偶数位取出来的右移 1 次,再相加
于是,原先的相邻两位,现在值就是它们 1 的个数
原来的数
同理,接下来就要求相邻 4 位 的了,取相邻 2 位的数
移位相加
而原来的数显然就是 0 和 3 个。
同理,求相邻 8 位时,显然就是上面这相邻 4 位相加,结果还是一样的。最后就是
同理,要求 32 位的话,继续求相邻 16,32 位的即可。
而取数也有两种取法,先取低 n 位,再取高 n 位,然后高 n 位向右移位再相加。也可以再取高位之前,先对整个数移位,把高位变成低位,再取也行。
class Solution {
public:
int hammingWeight(uint32_t n) {
// 取奇数位 取偶数位,然后右移变成低位,再相加
n=(n&0x55555555)+((n&0xaaaaaaaa)>>1);
n=(n&0x33333333)+((n&0xcccccccc)>>2);
n=(n&0x0f0f0f0f)+((n&0xf0f0f0f0)>>4);
n=(n&0x00ff00ff)+((n&0xff00ff00)>>8);
n=(n&0x0000ffff)+((n&0xffff0000)>>16);
return n;
}
};
class Solution {
public:
int hammingWeight(uint32_t n) {
// 取奇数位 数先右移,把偶数位变成奇数位,然后取再相加
n=(n&0x55555555)+((n>>1)&0x55555555);
n=(n&0x33333333)+((n>>2)&0x33333333);
n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);
n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
return n;
}
};上面这种方法时间复杂度为 。上面这种分治的方法还有一些取数和加减上的优化,不过原理都是一样的,就不展开了,深入可以参考 回归本源一一位运算及其应用 这篇文章。
题意
计算 0~n+1 所有数,对应数二进制中 1 的个数
分析
用 191 题的解法直接写出汉明重量,即每数中 1 的个数,然后遍历即可。
优化
无,优化个锤子,dp,不存在的。
效率
时间复杂度:,k 为每个数中 1 的个数
空间复杂度:
代码
class Solution {
public:
vector<int> countBits(int n) {
vector<int> res;
for (int i=0;i<=n;i++){
res.push_back(count(i));
}
return res;
}
int count(int n){
n=(n&0x55555555)+((n>>1)&0x55555555);
n=(n&0x33333333)+((n>>2)&0x33333333);
n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);
n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
return n;
}
};题意
判断 1 的个数是质数的数
分析
将其拆分为两个过程:
第一步参考 191,用分治。第二步按理说应该遍历判断的,不过这题给定的范围,数最大为 ,即 1 的个数最大为 19,则我们直接打表出可能是质数的数判断即可。
优化
无
效率
时间复杂度:,k 为 1 的个数
空间复杂度:
代码
class Solution {
public:
int countPrimeSetBits(int left, int right) {
int res=0;
for (int i=left;i<=right;i++){
if(isPrime(count(i))){
res++;
}
}
return res;
}
int count(int n){
n=(n&0x55555555)+((n>>1)&0x55555555);
n=(n&0x33333333)+((n>>2)&0x33333333);
n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);
n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
return n;
}
bool isPrime(int n){
return n==2||n==3||n==5||n==7||n==11||n==13||n==17||n==19;
}
};题意
先按数中 1 的个数排序,再按大小排序
分析
这题其实不难,就考察获取 1 的个数和传入的比较函数写法。获取 1 的个数参考 191 用分治即可,而重写比较函数则比较麻烦,不熟悉就会出问题。(和算法没啥关系,就是语言熟悉程度)。
需要注意有两点:
优化
无
效率
时间复杂度:
空间复杂度:
代码
// sort 的第三个变量必须是 static 或全局变量
int count(int x){
x=(x&0x55555555)+((x>>1)&0x55555555);
x=(x&0x33333333)+((x>>2)&0x33333333);
x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
x=(x&0x0000ffff)+((x>>16)&0x0000ffff);
return x;
}
bool cmp(int a,int b){
int ca=count(a);
int cb=count(b);
// 先比较 1 的个数是否相等
if (ca==cb){
return a<b; // 相等则返回数小的个
}
return ca<cb; // 1 的个数不等,则返回数小的
}
class Solution {
public:
vector<int> sortByBits(vector<int>& arr) {
sort(arr.begin(),arr.end(),cmp);
return arr;
}
};题意
统计不同的位数个数
分析
不同 → 异或为 1,故我们将两数异或,就转换为求 1 的个数。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int hammingDistance(int m, int n) {
int x=m^n;
x=(x&0x55555555)+((x>>1)&0x55555555);
x=(x&0x33333333)+((x>>2)&0x33333333);
x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
x=(x&0x0000ffff)+((x>>16)&0x0000ffff);
return x;
}
};同 461,只是注意溢出。
题意
计算任意两数间的汉明距离
分析
这题是 461 题的拓展,461 又是 191 的拓展,故最简单的自然是枚举任意两数异或,然后统计 1 的个数即可。
优化
不出意外的,枚举超时了,只能考虑优化。而很多优化都是展开固定的集合,比如字符串的题里面就有很多展开字符的,位运算里则有很多展开位数的。
所以这题我们可以考虑遍历每一位下每个数的和,然后再统计,比如某位 1 的个数为 n,则 0 的个数为 nums.size()-n,则一共有 个不同的数。(这个即简单的组合数学,0 里面挑一个,1 里面挑一个)
效率
时间复杂度:优化前 ,优化后
空间复杂度:
代码
暴力
class Solution {
public:
int totalHammingDistance(vector<int>& nums) {
int res=0;
for (int i=0;i<nums.size();i++){
for (int j=i+1;j<nums.size();j++){
res+=count(nums[i]^nums[j]);
}
}
return res;
}
int count(int x){
x=(x&0x55555555)+((x>>1)&0x55555555);
x=(x&0x33333333)+((x>>2)&0x33333333);
x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
x=(x&0x0000ffff)+((x>>16)&0x0000ffff);
return x;
}
};优化
class Solution {
public:
int totalHammingDistance(vector<int>& nums) {
int res=0;
int cnt=0;
for (int i=0;i<32;i++){
cnt=0;
for (int j=0;j<nums.size();j++){
cnt+=getBit(nums[j],i);
}
res+=(cnt*(nums.size()-cnt));
}
return res;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};进制题设计不同数进制间转换问题。
list
题意
将十进制转换为十六进制
分析
当为正数时,进制的转换就和普通的进制转换没有区别,就是辗转相除法。问题在于当数为负数时。
我们一般辗转相除法是这样的
num%16
num/=16但是,当 num 为负数时,上面的做法就行不通了,因为上面的做法都是基于十进制的,所以,为了通用,而且由于 16 是 2 的幂,所以我们都转换为二进制操作。
而 4 位二进制就转换为一个十六进制,所以我们每次取低 4 位组成一个十六进制,然后右移 4 位即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string toHex(int num) {
string res;
if (num==0){
return "0";
}
if (num>0){
while(num!=0){
res.insert(res.begin(),toChar(num&0xf));
num>>=4;
}
return res;
}
for (int i=0;i<8;i++){
res.insert(res.begin(),toChar(num&0xf));
num>>=4;
}
return res;
}
char toChar(int x){
switch(x){
case 10:
return 'a';
case 11:
return 'b';
case 12:
return 'c';
case 13:
return 'd';
case 14:
return 'e';
case 15:
return 'f';
default:
return x+'0';
}
}
};题意
十进制转换为七进制
分析
类似于 405 题转换为十六进制,同样分正负两种情况。正数直接辗转相除法即可得到答案,问题是当数是负数时该怎么处理。
在 405 中,由于负数是用补码表示的,所以要将其转换为二进制处理。而在这个题中,正负的区别只是符号的区别,故我们提前预处理一下,将负数转换为正负,最后再考虑增加负号即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string convertToBase7(int num) {
if (num==0){
return "0";
}
string res;
string chars="0123456";
bool sign=false;
if (num<0){
sign=true;
num=-num;
}
while(num!=0){
res.insert(res.begin(),chars[num%7]);
num/=7;
}
if (sign){
res.insert(res.begin(),'-');
}
return res;
}
};题意
将十进制浮点数转换为二进制字符串
分析
由于给定范围就在 0~1,故无序取整转换,且由于小于 0,故只需转换小数部分即可。原理就是小数部分乘 2,然后判断是否进位即可。
而要判断是否能够转换成功,即精度超过 32 位,那么最后判断一下转换后的结果是否超过 32 位即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string printBin(double num) {
int a;
string res="0.";
while(num!=0){
a=0;
num*=2;
a=(int)num;
if (a==1){
num--;
}
res+=(a+'0');
}
return res.size()<32?res:"ERROR";
}
};题意
判断 1~N 的所有元素的二进制是否是 s 的子串
分析
最简单的思路分成三步:
这是最暴力的做法
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
bool queryString(string s, int n) {
for (int i=1;i<=n;i++){
if (s.find(toString(i))==-1){
return false;
}
}
return true;
}
string toString(int x){
string res;
string chars="0123456789";
while(x!=0){
res.insert(res.begin(),chars[x&1]);
x>>=1;
}
return res;
}
};题意
找出不在数组中的同长度数字
分析
最简单的,自然是把字符串数组转换为数字,然后用哈希表存起来。再从 0 递增遍历判断谁不在哈希表中即可。
需要注意的是,转换成数字需要是十进制才行,如果是二进制,会溢出。只需要编写字符串和数字之间转换的函数即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string findDifferentBinaryString(vector<string>& nums) {
unordered_map<int,bool> m;
for (int i=0;i<nums.size();i++){
m[toInt(nums[i])]=true;
}
int n=0;
while(m.count(n)!=0){
n++;
}
return toString(n,nums[0].size());
}
int toInt(string s){
int res=0;
for (int i=0;i<s.size();i++){
res+= (((s[s.size()-1-i]-'0')&1)<<i);
}
return res;
}
string toString(int n,int length){
string res;
while(n!=0){
res.insert(res.begin(),'0'+(n&1));
n>>=1;
}
while(res.size()<length){
res.insert(res.begin(),'0');
}
return res;
}
};题意
转换为 k 进制
分析
最基本和基础的进制转换题,直接辗转相除法即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int sumBase(int n, int k) {
int res=0;
while(n!=0){
res+=(n%k);
n/=k;
}
return res;
}
};题意
求一个数是否被 5 整除
分析
给定的数组转换为对应的数很简单,从左至右扫描就行了,问题在于怎么判断是否被 5 整除,如果是 2 的幂的话,移位比较方便,也不会溢出。但是 5 就不行了,显然只能 %5,但是由于数组长度为 30000,所以哪怕是 long long 也会超时(第一次就超时了)。
所以,这里就涉及到了大数求余问题,从结论简单来说,每次对 5 取余再加减,对结果不会影响。也就是同余定理。
同余定理在许多地方应用广泛,前缀和,滑动窗口,位运算,数学等等 tag 都有它的身影,甚至我做离散数学时都遇到过几次证明群。因此这里做一个总结:
若 a%p=b%p,c%p=d%p
1. (a+c)%p=(b+d)%p
2. (a-c)%p=(b-d)%p
3. (a*c)%p=(b*d)%p
若 a%p=b%p,则
1. a^n%p=b^n%p
常见的有
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p + p) % p // 可能差是负数
(a * b) % p = (a % p * b % p) % p
(a^b) % p = ((a % p)^b) % p换言之,一个很大的数可能继续相加会溢出,那我们先 %p 再加减不会影响结果。
两个数相乘的结果可能溢出,我们先分别%p再乘也不会影响结果。
https://zh.wikipedia.org/zh-hans/同餘
https://zhuanlan.zhihu.com/p/96666921
https://jason87459473.wordpress.com/2018/09/10/同余定理/
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<bool> prefixesDivBy5(vector<int>& nums) {
vector<bool> res;
int data=0;
for (int i=0;i<nums.size();i++){
data=data%5; // 关键点
data+=data;
data+=nums[i];
res.push_back(data%5==0);
}
return res;
}
};题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string baseNeg2(int n) {
if (n==0){
return "0";
}
string res;
while(n!=0){
res.push_back((n&1)+'0');
n=-(n>>1);
}
reverse(res.begin(),res.end());
return res;
}
};题意
分析
优化
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> addNegabinary(vector<int>& arr1, vector<int>& arr2) {
int a=0;
int b=0;
int t=0;
vector<int> res;
for (int i=arr1.size()-1;i>-1;i--){
a+=(pow(-2,(arr1.size()-1-i))*arr1[i]);
}
for (int i=arr2.size()-1;i>-1;i--){
b+=(pow(-2,(arr2.size()-1-i))*arr2[i]);
}
t=a+b;
while(t!=0){
res.push_back(t&1);
t=-(t>>1);
}
if (res.empty()){
res.push_back(0);
}
reverse(res.begin(),res.end());
return res;
}
};用位运算模拟另一个运算,如加减乘除,还有一些用数组,字符串进行模拟加减乘除的,原理都差不多。
list
题意
用位运算实现加法
分析
这题其实就是考察加法器的实现原理,如果学过数电的话,就比较简单了,当然没学过也没关系,我们来思考一下,我们手动计算加法时,是怎么计算的。比如 123+789
对于每一位,我们就是对应位相加,然后再加上低位的进位。比如 3+9 没有低位进位,为 2,2+8 有低位进位,为 1,7+1有低位进位,为 9,最后就是 912.
所以对于每一位,计算公式就是 sum=a+b+cf。cf 就是低位的进位。
而对于计算机来说,底层的加法其实就是这个原理,稍微上层的 add 指令,或者用 32 位寄存器模拟 64 位加法的 adc 指令,以及更高层的 c 语法中的 + 运算,全都是这个原理。同理,减法 sub,sbb 就是减去借位 cf。
同理,二进制加法也是这个原理,直接两数 a+b 相加的电路叫半加器,位运算中异或 ^ 就是一个半加器,即 a^b 就是 a 和 b 不考虑进位的加法。而加上进位的电路就叫全加器。
原始模拟
原理已经理解了,那我们直接来模拟吧,就是从最低位开始向高位扫描,每一位半加再加上进位,然后再保存向高位的进位即可。
class Solution {
public:
int add(int a, int b) {
int res=0;
int cf=0;
int tmpSum=0;
for (int i=0;i<32;i++){
// 异或就是半加器
tmpSum=getBit(a,i) ^ getBit(b,i) ^ cf; // 获取当前位应该的值
if (tmpSum==0){
res=unsetBit(res,i);
} else {
res=setBit(res,i);
}
cf=(getBit(a,i)+getBit(b,i)+cf)>1?1:0; // 获取向高位的进位
}
return res;
}
int flapBit(int x,int pos){
return x^(1<<pos);
}
int setBit(int x,int pos) {
return x|(1<<pos);
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};直接模拟的话,时间复杂度扫一遍,应该是 .
32 位一起加
前面我们是一位一位的扫描,每次就把一位给加好了。其实在位运算中,经常会有这种方法:需要对每一位进行操作,那么可以并行同时对 32 位一起进行。
在这里也是一样的,我们每一位都需要做两个加法,即那么我们 32 位一起进行操作。
但是由于每一位半加之后,低位可能对进位半加又进位,所以要一直半加,直到没有进位了为之。(好吧,这个的确没有一位位的模拟好理解。)
class Solution {
public:
int add(int a, int b) {
int sum=a;
int cf=b;
while(b!=0){
sum=a^b;
cf=(uint32_t)(a&b)<<1;
a=sum;
b=cf;
}
return sum;
}
};时间复杂度自然也是 .
递归实现
在 32 位一起加的过程中,我们对每一位进行的操作其实都是一样的,那么自然也能写成递归形式。
class Solution {
public:
int add(int a, int b) {
return (b==0)?a:add(a^b,(uint32_t)(a&b)<<1);
}
};复杂度应该也是 .
同 371
同 371
题意
字符串模拟实现加法器
分析
和 371 一个原理,只是变成字符串存储了而已,还是一样的半加+进位,再向高位进位。
需要注意的是,由于初始字符串不对齐,所以需要提前预处理一下。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string addBinary(string a, string b) {
char cf='0';
string res;
// 预处理,将两字符对齐,并且增加一个高位
if (a.size()>b.size()){
a.insert(a.begin(),'0');
int m=a.size()-b.size();
for (int i=0;i<m;i++){
b.insert(b.begin(),'0');
}
} else if (a.size()<b.size()){
b.insert(b.begin(),'0');
int m=b.size()-a.size();
for (int i=0;i<m;i++){
a.insert(a.begin(),'0');
}
} else {
b.insert(b.begin(),'0');
a.insert(a.begin(),'0');
}
// 遍历模拟全加器
for (int i=a.size()-1;i>-1;i--){
res.insert(res.begin(),add(a[i],b[i],cf));
cf=getCarry(a[i],b[i],cf);
}
// 最后消除高位 0
if (res[0]=='0'){
res.erase(res.begin());
}
return res;
}
char add(char a,char b,char cf){
return (((a+b+cf-'0'-'0')=='0') || ((a+b+cf-'0'-'0')=='2'))?'0':'1';
}
char getCarry(char a,char b,char cf) {
return ((a+b+cf-'0'-'0')>='2')?'1':'0';
}
};同 67
题意
字符串模拟十进制加法器
分析
原理同 67,只是这题变成十进制了而已,那么进位和半加更改一下判断即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string addStrings(string a, string b) {
char cf='0';
string res;
// 预处理,将两字符对齐,并且增加一个高位
if (a.size()>b.size()){
a.insert(a.begin(),'0');
int m=a.size()-b.size();
for (int i=0;i<m;i++){
b.insert(b.begin(),'0');
}
} else if (a.size()<b.size()){
b.insert(b.begin(),'0');
int m=b.size()-a.size();
for (int i=0;i<m;i++){
a.insert(a.begin(),'0');
}
} else {
b.insert(b.begin(),'0');
a.insert(a.begin(),'0');
}
// 遍历模拟全加器
for (int i=a.size()-1;i>-1;i--){
res.insert(res.begin(),add(a[i],b[i],cf));
cf=getCarry(a[i],b[i],cf);
}
// 最后消除高位 0
if (res[0]=='0'){
res.erase(res.begin());
}
return res;
}
char add(char a,char b,char cf){
char c = a+b-'0';
if (c>'9'){
c=c-'9'+'0'-'1'+'0';
}
c=c+cf-'0';
if (c>'9'){
c=c-'9'+'0'-'1'+'0';
}
return c;
}
char getCarry(char a,char b,char cf) {
return ((a+b+cf-'0'-'0')>'9')?'1':'0';
}
};题意
实现全加器,一个操作数固定为 1
分析
最后清除高位 0 即可,可参考 989,415,67,371 等题
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> plusOne(vector<int>& nums) {
int cf=1;
nums.insert(nums.begin(),0);
int tmp;
for (int i=nums.size()-1;i>-1;i--){
tmp=nums[i];
nums[i]=getHalfSum(nums[i],cf);
cf=getCarry(tmp,cf);
}
if (nums[0]==0){
nums.erase(nums.begin());
}
return nums;
}
int getHalfSum(int a,int cf){
if ((a+cf)>9){
return a+cf-10;
}
return a+cf;
}
int getCarry(int a,int cf){
return (a+cf)>9?1:0;
}
};题意
一个数组,一个整数,求和
分析
最简单的自然是数组转换成整数,然后求和再转换会数组。不过这样就把求和的过程交给底层去做了,这题就没意义了。为了提高难度,所以反过来,手动模拟加法过程。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<int> addToArrayForm(vector<int>& a, int k) {
vector<int> b,res;
int cf=0;
while(k!=0){
b.insert(b.begin(),k%10);
k/=10;
}
// 预处理,将两字符对齐,并且增加一个高位
if (a.size()>b.size()){
a.insert(a.begin(),0);
int m=a.size()-b.size();
for (int i=0;i<m;i++){
b.insert(b.begin(),0);
}
} else if (a.size()<b.size()){
b.insert(b.begin(),0);
int m=b.size()-a.size();
for (int i=0;i<m;i++){
a.insert(a.begin(),0);
}
} else {
b.insert(b.begin(),0);
a.insert(a.begin(),0);
}
// 遍历模拟全加器
for (int i=a.size()-1;i>-1;i--){
res.insert(res.begin(),getHalfSum(a[i],b[i],cf));
cf=getCarry(a[i],b[i],cf);
}
// 最后消除高位 0
if (res[0]==0){
res.erase(res.begin());
}
return res;
}
int getHalfSum(int a,int b,int cf){
if ((a+cf+b)>9){
return a+b+cf-10;
}
return a+cf+b;
}
int getCarry(int a,int b,int cf){
return (a+cf+b)>9?1:0;
}
};题意
手动模拟乘法过程
分析
和题意一样,完全手动模拟。
优化
无(?)
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
string multiply(string num1, string num2) {
if (num1=="0"||num2=="0"){
return "0";
}
string res="0";
string tmp;
for (int i=num1.size()-1;i>-1;i--){
tmp=multiplySingle(num1[i],num2);
for (int j=0;j<num1.size()-i-1;j++){
tmp.push_back('0');
}
res=addStrings(res,tmp);
}
return res;
}
string multiplySingle(char a,string b){
string res;
string tmp;
for (int i=b.size()-1;i>-1;i--){
tmp=mul(a,b[i]);
for (int j=0;j<b.size()-i-1;j++){
tmp.push_back('0');
}
res=addStrings(res,tmp);
}
return res;
}
string mul(char c1,char c2){
string res;
int m=(c1-'0')*(c2-'0');
res.push_back((m/10)+'0');
res.push_back((m%10)+'0');
if (res[0]=='0'){
res.erase(res.begin());
}
return res;
}
string addStrings(string a, string b) {
char cf='0';
string res;
// 预处理,将两字符对齐,并且增加一个高位
if (a.size()>b.size()){
a.insert(a.begin(),'0');
int m=a.size()-b.size();
for (int i=0;i<m;i++){
b.insert(b.begin(),'0');
}
} else if (a.size()<b.size()){
b.insert(b.begin(),'0');
int m=b.size()-a.size();
for (int i=0;i<m;i++){
a.insert(a.begin(),'0');
}
} else {
b.insert(b.begin(),'0');
a.insert(a.begin(),'0');
}
// 遍历模拟全加器
for (int i=a.size()-1;i>-1;i--){
res.insert(res.begin(),add(a[i],b[i],cf));
cf=getCarry(a[i],b[i],cf);
}
// 最后消除高位 0
if (res[0]=='0'){
res.erase(res.begin());
}
return res;
}
char add(char a,char b,char cf){
char c = a+b-'0';
if (c>'9'){
c=c-'9'+'0'-'1'+'0';
}
c=c+cf-'0';
if (c>'9'){
c=c-'9'+'0'-'1'+'0';
}
return c;
}
char getCarry(char a,char b,char cf) {
return ((a+b+cf-'0'-'0')>'9')?'1':'0';
}
};题意
递归实现十进制乘法
分析
乘法本质是加法,而且十进制和二进制方法也有一些不同。对于十进制,最简单的自然是相加,即 A*B=A 个 B 相加,无论递归还是迭代都很容易写出。
优化
对加法的优化基本思想类似于快速幂,都是分治的思想,而在乘法中,有一个稍微更简便的算法,即俄夫乘法
而具体实现也有递归和迭代方式,这题要递归,而分治快速幂的话,更多是迭代利用过去求得的结果来算的。
而乘法还有其它一些实现,必然手动模拟乘法,字符串高精度就是这么算的。参考 43 题,还有二进制乘法,也可以模拟手动或换成加法来算。
效率
时间复杂度:
空间复杂度:
代码
简单加法
class Solution {
public:
int multiply(int A, int B) {
if (B==0){
return 0;
}
return A+multiply(A,B-1);
}
};分治优化
class Solution {
public:
int multiply(int A, int B) {
if (A==1){
return B;
}
if ((A&1)==0){
return multiply(A>>1,B<<1);
}
return multiply((A-1)>>1,B<<1)+B;
}
};再优化为一个调用(A 为奇数时,除以二的结果是一样的,所以只需要根据 A 的奇偶判断是否加一个 B 即可)
class Solution {
public:
int multiply(int A, int B) {
int res=B;
(A!=1)&&(res=(((-(A&1))&B)+multiply(A>>1,B<<1)));
return res;
}
};题意
模拟除法
减法
除法本质上是减法,因此我们可以模拟减法的过程,而且这题只求商,商就是指 a 能分成多少个 b,即 a-kb 必然刚刚够分,故我们直接每次减去一个 b,然后看最后什么时候不够分就行。
只是需要注意溢出,以及正负,所以可能是加可能是减,判断的条件也可能是大于 0 或小于 0.
减法变形
由于直接模拟减法展开两数分正负,有 4 种情况过于复杂。故我们考虑优化:提前将其变成同号再计算。
而 int 的范围在 中,故负数的范围大稍微多一点,如果转换为正数的话,那么 转换为对应的相反数 就会溢出。
故我们将其转换为负数。
而由于根据是否同号,最后的商也分正负,故我们需要提前记录下是否同号,然后最后再将商转换一下即可。
无论是减法和减法变形,其时间复杂度都非常的高,取决于商的大小。为 如果商非常大,如 ,就要循环 次,显然会超时。
模拟减法展开
class Solution {
public:
int divide(int a, int b) {
if (a==INT_MAX && b==1){
return INT_MAX;
}
if (a==INT_MAX && b==-1){
return INT_MIN;
}
if (a==INT_MIN && b==1){
return INT_MIN;
}
if (a==INT_MIN && b==-1){
return INT_MAX;
}
if (a==INT_MAX && b==INT_MAX){
return 1;
}
if (a==INT_MIN && b==INT_MIN){
return 1;
}
if (a==0 || b==0){
return 0;
}
int res=0;
bool f=(a^b)<0;
if (a>0 && b>0){
while(a>=0){
res++;
a=a-b;
}
}else if (a>0 && b<0){
while(a>=0){
res++;
a=a+b;
}
} else if (a<0 && b>0){
while(a<=0){
res++;
a=a+b;
}
} else if (a<0 && b<0){
while(a<=0){
res++;
a=a-b;
}
}
res--;
if (f) {
res=(~res)+1;
}
return res;
}
};减法优化
class Solution {
public:
int divide(int a, int b) {
int res=-1;
if (a==INT_MIN && b== -1){
return INT_MAX;
}
if (b==0 || a==0){
return 0;
}
// true 则同号
bool sign = ((a^b)>=0);
int abs_a=(a<0)?a:(~a+1);
int abs_b=(b<0)?b:(~b+1);
while(abs_a<=0){
res++;
abs_a-=abs_b;
}
return sign?res:(~res+1);
}
};减法倍增优化
class Solution {
public:
int divide(int a, int b) {
int res=0;
if (a==INT_MIN && b== -1){
return INT_MAX;
}
if (a==INT_MIN && b== 1){
return INT_MIN;
}
if (b==0 || a==0){
return 0;
}
// true 则同号
bool sign = ((a^b)>=0);
int abs_a=(a<0)?a:(~a+1);
int abs_b=(b<0)?b:(~b+1);
if (abs_a>abs_b){
return 0;
}
res=div(abs_a,abs_b);
return sign?res:(~res+1);
}
int div(int a,int b) {
if (a > b) { // 当分母比分子大时,返回 0
return 0;
}
int count=1;
int tmpB=b;
while(a<=(tmpB+tmpB)) { // 迭代时,倍增 b,看最多能减到什么时候
count += count;
tmpB += tmpB;
}
return count + div(a-tmpB,b); // 递归时,则减 a,b 不变,余下的 a 又重新开始倍增
}
};同 29
题意
用位运算找出大的数
分析
基本思路是:
实现三目表达式
首先我们来看,三目表达式的基本格式是怎么样的
cond?x:y;cond 是逻辑表达式,x 是 cond 为真即 1 是的 值,y 是 cond 为 0 的值。
显然,这是一个分支 ,但是我们不能用分支语句,所以必然的,我们要想办法实现分支语句。
基本思路有两个
逻辑表达式参考 offer 64 题中的思路,即利用 A&&B,当 A 为 false 时,B 不会执行,这就是个分支语句。
实现如下
class Solution {
public:
int maximum(int a, int b) {
return branch(a>b,a,b);
}
int branch(bool cond,int x,int y){
int res=y;
(cond)&&(res=x);
return res;
}
};短路特性比较巧妙,而且是个语言特性,我们来继续看位运算的方式。
要实现分支,其实最简单的就是逻辑表达式中的或逻辑,即 |,即实现如
x|y类似这种形式的表达式,就能得到结果,也就是说,我们要自己构造逻辑表达式。
而类似要得到分支的结果,我们需要一方等于 0 才行,即 0 | a 这种形式,而要得到 0,显然需要与操作才行。
也就是说,我们需要得到
(?&x)|(?&y)这种格式的一个表达式,而 ? 就是根据 cond 进行一些变化得到的。
在或表达式中,除了一方要得到 0 之外,另一方显然应该得到正确的结果,即 x 或 y。也就是说:在 | 的两方,要是一个得到 0,一个保持不变。
而有时与运算,显然
换句话说,我们需要根据输入的 cond 是 1 或者 0,得到一个为 0,一个为 -1 的结果。
但是 -1 显然太麻烦了,如果是得到 0 或 1 的话,就比较方便。而 0 取负数实际上还是 0,所以,我们只需要知道:
怎么根据输入 0 或 1,得到 0,1 或 1,0 即可。
假设我们需要得到的表达式为
(m&x)|(n&y)而上面要得到的显然是一个真值表,所以我们画出真值表
而又要取个负数,故
则最终的逻辑表达式,应该是
则能写出代码如下
class Solution {
public:
int maximum(int a, int b) {
return branch(a>b,a,b);
}
int branch(bool cond,int x,int y){
return ((-cond)&x)|((-(!cond))&y);
}
};上面我们用到了 ! 表达式,似乎也是个表达式,干脆都换成位运算,即 0 变成 1,1 变成 0,这个操作就等于和 1 异或的结果。则
class Solution {
public:
int maximum(int a, int b) {
return branch(a>b,a,b);
}
int branch(bool cond,int x,int y){
return ((-cond)&x)|((-(cond^1))&y);
}
};当然,除了上面这种分析方法,还有其它一些方法,比如下面这个表达式也实现了三目表达式,原理我还没看懂
class Solution {
public:
int maximum(int a, int b) {
return branch(a>b,a,b);
}
int branch(bool cond,int x,int y){
return y ^ ((x ^ y) & -cond);
}
};优化
无
效率
时间复杂度:
空间复杂度:
题意
不用分支,循环,乘除,三目表达式求和
分析
这题很骚,我学到不少东西,我们来一个个的分析
要求很多数的和,自然两个思路:
用加法的基础,显然有循环和递归两种方式,而这里不用循环。所以:我们只能用递归代替循环,这是第一点。
比如随手一个循环
class Solution {
public:
int sumNums(int n) {
int res=0;
for (int i=0;i<=n;i++){
res+=i;
}
return res;
}
};用于用了循环,所以我们考虑递归
class Solution {
public:
int sumNums(int n) {
if (n==0){
return 0;
}
return n+sumNums(n-1);
}
};显然,递归必然要考虑递归出口的问题,也就是说,必然会用到分支,但是分支语句又不让用,怎么办呢?
我考虑过用位运算实现三目表达式,比如这样
class Solution {
public:
int sumNums(int n) {
return n+sumNums(n-1)^(sumNums(n-1)&(-(n==0)));
}
};如果是普通的数的话,结果上是没问题的,但是这是函数,所以还是会一直递归调用,最后会栈溢出。所以,我们得必须解决分支的问题。
想了半天,没有办法,只能看题解了,题解利用了逻辑表达式中的短路原理,即
A && B,如果 A 为 false,则 B 不会执行。同理,A || B ,如果 A 为 true,B 也不会执行。这其实就是个分支语言,只不过是简单的 if 单分支而已。所以,用逻辑表达式短路原理实现分支,这就是这个题的第二个难点了。
所以我们可以写出下面语句
class Solution {
public:
int sumNums(int n) {
int res=0;
(n!=0)&&(res=(n+sumNums(n-1)));
return res;
}
};现在当我们有了
这两个利器之后,基本的其它方法都可以实现了。
比如说,由于是等差公式,所以还可以用求和公式简化,即
但是,这里出现了乘法 ,而我们又不允许使用乘法,所以,又用位运算自己实现了个乘法,原理是俄夫乘法。
class Solution {
public:
int sumNums(int n) {
return multiply(n,n+1)>>1;
}
int multiply(int A, int B) {
int res=B;
(A!=1)&&(res=(((-(A&1))&B)+multiply(A>>1,B<<1)));
return res;
}
};上面这个可以说基本压缩到极限了,我们用了递归,位运算,短路原理等。
其实这个题还有一个更骚的算法,把空间换时间的思想也用到了极限,不,应该叫直接利用空间大小得出答案。是的,你已经想到了,由于 sum=(n*(n+1) )/2,而 nm 的大小和二维数组大小计算方式一样,所以,我们可以定义一个 n(n+1) 大小的数组,然后除以二(右移1位),就能得到答案。
class Solution {
public:
int sumNums(int n) {
char a[n][n+1];
return sizeof(a)>>1;
}
};效率
时间复杂度:加法的复杂度为 ,乘法主要是分治,为 ,而利用数组则是
空间复杂度:
题意
用 | & 实现 ^
分析
这是 CSAPP 上的一个 lab,顺便做了.
像这种题第一反应就是画真值表再看,毕竟是典型的电路设计题。
可以看出来,重点是中间两行的结果要为 1
而与都为 0,故考虑将与取反再看
这下就很清晰了,直接只有中间两行都是其,其它都至少有个 0,而这和相与的规则是一样的,故相与即可。即
先 AB 相与取反,再 AB 相或,最后再与起来即可。
而我们在这里用到了取反操作,最简单的自然是 ! ,不过既然都使用位运算,干脆都用位运算实现。而 0 和 1 之间的取反操作最简单的是直接和 1 异或,即 x^1,但是,我们这里就是要实现异或,所以显然不行。
我们先来分析一下 0 和 1 的二进制,看看它们之间有什么规律:
0 → 0000 0000 → 全 0
1 → 0000 0001 → 只有最低位为 1
从上面可以看出来, 0 和 1 之间的区别就是最低位相反,而这就很简单了,不用异或实现取反,还有按位取反的 ~ 操作,但是 ~ 所有位的都反,就都变成 1 了,而非最低位应该是 0 才对,所以我们将其它位清 0,而清零操作就是和 0000 00001 即 1 相与即可。
故,最终可写出代码如下
int getXor(int a,int b) {
return ((~(a&b))&1)&(a|b);
}问一个数是否是 n 的幂,解法比较多,有如下几种思路:
list
题意
判断数是否是 2 的幂
辗转相除法
最简单和暴力的思路,如果是 n 的幂,那么说明值就是 1nnn...*n=m.那么我们反过来,m 就会被 n 整除,一直到为 1.
这是最暴力和直接的思路,适用于所有判断幂的情况,不过时间复杂度达到了 .
class Solution {
public:
bool isPowerOfTwo(int n) {
if (n<=0){
return false;
}
while(n%2 == 0){
n>>=1;
}
return n==1;
}
};打表
第二种方法也比较简单和直接,要判断是不是 n 的幂,由于 int 是有范围的,所以最终的结果其实也是固定的一些数,那么我们提前找出这些数判断即可。
打表也是判断 n 的幂的通用解法之一,虽然看上去似乎太?,但是实际上这种方法在我们真正写项目过程中反而常用一些。
当然,我们也可以不打表,而是直接计算出对应的值,然后判断是否相等也行。显然打表的时间复杂度位 .
class Solution {
public:
bool isPowerOfTwo(int n) {
switch (n) {
case 1:
case 2:
case 4:
case 8:
case 16:
case 32:
case 64:
case 128:
case 256:
case 512:
case 1024:
case 2048:
case 4096:
case 8192:
case 16384:
case 32768:
case 65536:
case 131072:
case 262144:
case 524288:
case 1048576:
case 2097152:
case 4194304:
case 8388608:
case 16777216:
case 33554432:
case 67108864:
case 134217728:
case 268435456:
case 536870912:
case 1073741824:
return true;
default:
return false;
}
}
};二进制规律
要判断似乎是 2 的幂,我们可以找到所有幂,然后看它们有什么规律。
1 →0001
2 → 0010
4 → 0100
8 → 1000
如上,我们发现,2 的幂其实就是只有一个 1 的二进制数,因此,我们可以利用这一点来判断,如果只有一个 1,那么就是 2 的幂。
而判断是否只有一个 1,也有两种方法:
显然找规律的时间复杂度为 .
class Solution {
public:
bool isPowerOfTwo(int n) {
return n>0 && (n&(n-1))==0; // n&(n-1) 是获取消除最低位的 1
}
};class Solution {
public:
bool isPowerOfTwo(int n) {
return n>0 && (n&(-n))==n; // n&-n 是获取最低位的 1
}
};同余定理
若 2 的幂为 n,则 n 的质因数只有 2,故 n 必然等于 1222..,换句话说,不同幂 m,n 之间必然能够整除,即如果 .而我们要判断一个数是不是 2 的幂,即判断是不是 ,那么这个数同样也能够被比它大的 2 的幂整除。由于 int 最大 ,故 2 的幂最大为 2<<29,则必有 。
class Solution {
public:
bool isPowerOfTwo(int n) {
return n>0 && (2<<29)%n==0;
}
};题意
判断数是否是 3 的幂
辗转相除法
最简单和暴力的思路,如果是 n 的幂,那么说明值就是 1nnn...*n=m.那么我们反过来,m 就会被 n 整除,一直到为 1.
这是最暴力和直接的思路,适用于所有判断幂的情况,不过时间复杂度达到了 .
class Solution {
public:
bool isPowerOfThree(int n) {
if (n<=0){
return false;
}
while(n%3 == 0){
n/=3;
}
return n==1;
}
};打表
类似的,我们也可以打表
class Solution {
public:
bool isPowerOfThree(int n) {
switch (n) {
case 1:
case 3:
case 9:
case 27:
case 81:
case 243:
case 729:
case 2187:
case 6561:
case 19683:
case 59049:
case 177147:
case 531441:
case 1594323:
case 4782969:
case 14348907:
case 43046721:
case 129140163:
case 387420489:
case 1162261467:
return true;
default:
return false;
}
}
};寻找规律
1→0001
3→0011
9→1001
27→11011
看上去似乎没有规律,我们分析一下 x=x3=x(2+1)=x*2+x=x<<1+x。也就是说先移位再加,比如 0011=0001 移位得 0010+0001=0011.
的确有规律,不过这个规律不好判断。故无法使用这个方法。
同余定理
同理判断 2 的幂,由于 3 也是一个质数,所以 3 的幂必然也为 333*..,则必然 3 的幂的最大值必然会被 整除,而在 int 范围内,3 的幂的最大值为 1162261467,则必有 1162261467%m=0
class Solution {
public:
bool isPowerOfThree(int n) {
return n>0 && 1162261467%n==0;
}
};题意
判断数是否是 4 的幂
辗转相除法
class Solution {
public:
bool isPowerOfFour(int n) {
if (n<=0){
return false;
}
while(n%4 == 0){
n>>=2;
}
return n==1;
}
};打表
class Solution {
public:
bool isPowerOfFour(int n) {
switch (n) {
case 1:
case 4:
case 16:
case 64:
case 256:
case 1024:
case 4096:
case 16384:
case 65536:
case 262144:
case 1048576:
case 4194304:
case 16777216:
case 67108864:
case 268435456:
case 1073741824:
return true;
default:
return false;
}
}
};二进制规律
1 → 0000 0001
4 → 0000 0100
16→ 0001 0000
64→ 0100 0000
可以看到,4 的幂的二进制就是奇数位是 1,且只有一个 1,那么我们可以根据这一点写出判断条件:
class Solution {
public:
bool isPowerOfFour(int n) {
if ((n<=0) || ((n&(n-1))!=0)){
return false;
}
int cnt=-1;
while(n!=0){
cnt++;
n>>=1;
}
return cnt%2==0;
}
};当然,我们还可以从另外的角度来分析这个性质。即不是判断右边位数为奇数,而是直接判断 1 在奇数上,怎么做呢?其实很简单。
奇数上为 1 时,最大的数为 0101 0101 0101 ...即 0x55555555.如果只有奇数上为 1 的话,那么这个数和 0x55555555 相与必然结果还是该数。
如 0x0010 0000 & 0x5555 5555=0x0010 0000,因为 0x5555555 只有奇数上为 1,那么如果要求的数没有 1,或者偶数上为 1,和 0x5555555 相与之后结果都会变化。故如果 0x5555555&n==n,则 n 就是 4 的幂。当然,有人可能会问,如果不只一个 1 呢,比如 5 就是 0101,也满足这个条件。那我们提前过滤一遍即可。
class Solution {
public:
bool isPowerOfFour(int n) {
if (n<=0 || ((n&(n-1)) !=0)){ // 小于 0 或 1 的个数多于 1,则失败
return false;
}
return (0x55555555 & n)==n;
}
};同余定理
.
class Solution {
public:
bool isPowerOfFour(int n) {
return n>0 && ((n&(n-1))==0) && ((n%3)==1);
}
};转换为 2 的幂
,故对其开方后,如果还是 2 的幂的话,那么就是 4 的幂。
class Solution {
public:
bool isPowerOfFour(int n) {
int m;
if (n<=0 || ((n&(n-1)))!=0){
return false;
}
m=sqrt(n);
if (m*m!=n){ // 判断是否是平方数
return false;
}
return n>0 && (2<<29)%m==0;
}
};判断任意数 n 的幂,有多种方法,其中辗转相除,打表法是通用的。而如果 n 是质数,可以找幂最大值判断是否整除,其它的则要看 n 的性质,有的可以根据其二进制数的规律,有的可以根据同余定理,有的也可以转换为其它数判断。
题意
找到和为 2 的幂的两个数
分析
由于给定的范围,2 的幂个数是确定的,所以对于一个数,要找到另一个数是否和为幂,而要找的那几个数就已经固定了,所以枚举这几个幂,就变成无序数组求两数和问题了,即 1 题,直接用哈希表暂存即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int countPairs(vector<int>& nums) {
unordered_map<int,int> m;
int res=0;
vector<int> power={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152};
for (int i=0;i<nums.size();i++){
for (int j=0;j<power.size();j++){
if (((power[j]-nums[i])>-1) && (m.count(power[j]-nums[i])!=0)){
res%=1000000007;
res+=m[power[j]-nums[i]];
}
}
m[nums[i]]++;
}
return res;
}
};逆转位数通常涉及到局部逆转,分治等思想。
list
题意
反转十进制整数
分析
刚开始我想错了,以为直接反转 4 个字节就行了,结果发现并不是 BCD 码,十进制反转不能从二进制考虑。于是就简单写了个辗转相除法,结果可能溢出。那么溢出就是这题处理的要点了。
最简单的自然是用更大范围的数暂存,不过题目又限制不能用。那么我们就只能避免溢出了。通常,判断溢出的方式有两种:
而避免溢出也通常有两种方式:
第一个不用多说,第二个则比较常见,通常是移项然后变运算法则。比如加法变减法,乘法变除法,常见的如二分中 mid=(r-l)/2+l 就是加法变减法。
这题也是一样,溢出时 n*10>INT_MAX → n>INT_MAX/10,下溢同理。
当然,除了辗转相除法,还有转换成字符串模拟等方法处理。
优化
无
效率
时间复杂度:
空间复杂度:
代码
更大存储空间
class Solution {
public:
int reverse(int x) {
int res=0;
while(x!=0){
if ((long)res*10>INT_MAX || (long)res*10<INT_MIN){
return 0;
}
res=res*10+x%10;
x/=10;
}
return res;
}
};变运算
class Solution {
public:
int reverse(int x) {
int res=0;
while(x!=0){
if (res>(INT_MAX/10)){
return 0;
}
if (res<(INT_MIN/10)){
return 0;
}
res=res*10+x%10;
x/=10;
}
return res;
}
};题意
将二进制数逆转
分析
一看到逆转,就想到字符串逆转。在字符串逆转中,双指针是最常见的了,不过这题显然不好双指针,但是逆转的原理是一样的。
通常,我们可以通过局部反转达到目的,这题也是一样的,分治来做。
同理,整数也是一样的,只是变成 32 位了而已。
现在第二个问题就是,我们怎么反转对应的位。其实很简单,分成三步:
即 x=(x<<m)|(x>>n).
当然,我们其实也可以模拟字符串反转的方式,不过效率不咋地,可以当作练习位运算的操作,涉及取位,设置等。
优化
无
效率
时间复杂度:
空间复杂度:
代码
分治
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
n=((n&0xffff0000)>>16)|((n&0x0000ffff)<<16);
n=((n&0xff00ff00)>>8)|((n&0x00ff00ff)<<8);
n=((n&0xf0f0f0f0)>>4)|((n&0x0f0f0f0f)<<4);
n=((n&0xcccccccc)>>2)|((n&0x33333333)<<2);
n=((n&0xaaaaaaaa)>>1)|((n&0x55555555)<<1);
return n;
}
};双指针
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
int i=0;
int j=31;
while(i<=j){
n=swap(n,i,j);
i++;
j--;
}
return n;
}
int swap(int n,int i,int j){
int vi=getBit(n,i);
int vj=getBit(n,j);
if (vi==0){
n=unsetBit(n,j);
} else {
n=setBit(n,j);
}
if (vj==0){
n=unsetBit(n,i);
} else {
n=setBit(n,i);
}
return n;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int unsetBit(int x,int pos){
return x&(~(1<<pos));
}
};题意
交换奇数位和偶数位
分析
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int exchangeBits(int num) {
return ((num&0xaaaaaaaa)>>1)|((num&0x55555555)<<1);
}
};集合操作是指用一个数表示成一个集合,然后用位运算代替集合的基本操作。是竞赛中比较常见的题型之一。
list
题意
生成数组子集
分析
对于数组中的某个元素,它只有两种状态:即出现或不出现。
所以,我们能用一个数组长度的变量,表示某个集合的元素情况。比如 [1,2,3] 的子集 ,用 101 就可以表示子集 [1,3]。
而我们要生成所有子集,所以,我们只需要找到所有表示状态的变量即可。即 0000~1111,我们枚举所有变量,然后对于每一个变量,即一个子集。然后遍历元素看是否需要在其中即可。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
vector<int> t;
int cnt=0;
int n=pow(2,nums.size());
for (int i=0;i<n;i++){
t.clear();
for (int j=nums.size()-1;j>-1;j--){
if (getBit(i,j)){
t.push_back(nums[j]);
}
}
res.push_back(t);
}
return res;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};题意
生成子集,但不能有重复元素
分析
本题就是在 78 的基础上增加了去重的操作,问题就在于怎么去重。
我们可以排序,如果上一个元素相同,且没有取,那么就要跳过当前元素。
比如 [1,2,2],重复的两个即
101
110
当第一次取了 101 后,这时该取 110 了,但是 110 和 101 是相同的,理论上我们应该跳过,那我们是怎么判断的呢?
其实是这样的:
1,2,2
1 1 0
当我们取 110 的 第二个 1,即 12x 的 2 时,如果后面的 x 和 2 相同,并且为 0,说明什么?说明上一次已经取了这个元素了,即上一次已经取了 1x2 了,所以我们不应该再取 12x,这时就该跳过。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>> res;
vector<int> t;
int cnt=0;
int n=pow(2,nums.size());
bool flag;
sort(nums.begin(),nums.end());
for (int i=0;i<n;i++){
t.clear();
flag=true;
for (int j=nums.size()-1;j>-1;j--){
if (getBit(i,j)){
if ((j-1)>-1 && getBit(i,j-1)==0 && nums[j]==nums[j-1]){
flag=false;
}
t.push_back(nums[j]);
}
}
if (flag){
res.push_back(t);
}
}
return res;
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};题意
找出两个没有交集的数组长度之积的最大值
分析
要找没有交集的两集合,故我们可以考虑使用位运算将其映射到 int 变量中,每一位表示一个元素,然后使用与运算表示交集。
优化
无
效率
时间复杂度:
空间复杂度:
代码
class Solution {
public:
int maxProduct(vector<string>& words) {
vector<int> sets(words.size());
int res=0;
for (int i=0;i<words.size();i++){
for (int j=0;j<words[i].size();j++){
sets[i]=setBit(sets[i],words[i][j]-'a');
}
}
for (int i=0;i<sets.size();i++){
for (int j=i+1;j<sets.size();j++){
if ((sets[i]&sets[j])==0){
int t=words[i].size()*words[j].size();
res=max(res,t);
}
}
}
return res;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
};题意
位运算模拟集合+子集生成+哈希统计个数
分析
一看要使得所有字符在其它字符中,就想到了位运算模拟集合,然后交集就等于其本身。也是状态压缩的一种方式。
分别把 words 和 puzzles 压缩到两个 int 数组里面,而要找交集等于自己,其实就是相与等于自己。
所以暴力的,就是对于每个 puzzles,遍历 words,但是这样复杂度有点高,O(nm) 会超时。
优化
优化也很简单,把 words 放在哈希表里,对于每个 puzzles,我们不遍历 words 了,而是生成 puzzles 对应的子集,然后判断这个子集在不在哈希表中即可。这样就把复杂度从遍历 puzzles 优化到了生成 puzzles 的子集。
效率
时间复杂度:,优化
空间复杂度:
代码
压缩+暴力枚举
class Solution {
public:
vector<int> findNumOfValidWords(vector<string>& words, vector<string>& puzzles) {
vector<int> set1(words.size());
vector<int> set2(puzzles.size());
vector<int> res(puzzles.size());
int tmp=0;
for (int i=0;i<words.size();i++){
for (int j=0;j<words[i].size();j++){
set1[i]=setBit(set1[i],words[i][j]-'a');
}
}
for (int i=0;i<puzzles.size();i++){
for (int j=0;j<puzzles[i].size();j++){
set2[i]=setBit(set2[i],puzzles[i][j]-'a');
}
}
for (int i=0;i<set2.size();i++){
tmp=0;
for (int j=0;j<set1.size();j++){
if (((set2[i]&set1[j])==set1[j])&&(getBit(set1[j],puzzles[i][0]-'a'))){
tmp++;
}
}
res[i]=tmp;
}
return res;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};压缩+子集生成+哈希查询
class Solution {
public:
vector<int> findNumOfValidWords(vector<string>& words, vector<string>& puzzles) {
vector<int> set1(puzzles.size());
vector<int> res(puzzles.size());
unordered_map<int,int> m;
int tmp=0;
int t=0;
int cnt=0;
// 将 words 映射成 int,并且存于 map 中
for (int i=0;i<words.size();i++){
t=0;
for (int j=0;j<words[i].size();j++){
t=setBit(t,words[i][j]-'a');
}
m[t]++;
}
// 将 puzzles 映射成 int,且存于数组中
for (int i=0;i<puzzles.size();i++){
for (int j=0;j<puzzles[i].size();j++){
set1[i]=setBit(set1[i],puzzles[i][j]-'a');
}
}
int n=pow(2,7);
for (int i=0;i<puzzles.size();i++){ // 枚举 puzzles
cnt=0;
for (int j=0;j<n;j++){ // 根据 j 生成 puzzles 的子集
if (getBit(j,0)==0){
continue;
}
tmp=0; // 暂存生成的子集
for (int k=0;k<7;k++){ // 枚举 puzzles[i] 的字母
if (getBit(j,k)){
tmp=setBit(tmp,puzzles[i][k]-'a');
}
}
cnt+=m[tmp];
}
res[i]=cnt;
}
return res;
}
int setBit(int x,int pos){
return x|(1<<pos);
}
int getBit(int x,int pos){
return (x>>pos)&1;
}
};状态压缩就是将问题中的一些状态压缩到一个 int 变量中,然后通过位运算进行状态间的转换和判断。这种方式在许多题目中都很常见,比如 dp 中。常见的将 int 视为集合进行操作就是典型的状态压缩题型之一。
list
位运算总体来说其实是比较简单的,基本操作也就与或非移位异或,但是它的使用场景其实一点都不少,无论是底层编程,还是项目中的优化,又或者在面试中,或者单纯的算法中,它的场景非常的多。要掌握好位运算的话,需要了解一些基本的底层知识,如计组,数电的一些知识,数的编码,位运算也通常在 dp,前缀树,状态压缩,回溯等较难的算法中出现,要掌握好还是需要大量的练习才行。