常见位运算总结
1、基础位运算:^ --> 相同为0,相异为1/ 无进位相加
2、给一个数n,确定它的二进制表示中的第x位是0还是1:(n >> x)&1,结果为0说明第x位是0,结果为1说明第x位为1;或者 n&(1<<x) 结果不为0也可以说明第x位为1
3、将一个数n的二进制表示的第x位改成1:n = n | (1<<x)
4、将一个数n的二进制表示的第x位改成0:n = n &( ~(1<<x))
5、提取一个数n二进制表示中最右侧的1:n = n & (-n),其中-n是将最右侧的1,左边的区域全部变成相反数
6、去除一个数n二进制表示中最右侧的1:n = n & (n-1),其中n-1是将最右侧的1已经右边的部分全部变相反。
7、异或运算的运算律:a ^ 0 = a,a ^ a = 0,a ^ b ^ c = a ^ (b ^ c)。
例题一:两整数之和

1、分析:题目要求简单明了,但是不让我们使用加减符号还要返回两整数之和,那我们可以考虑用异或位运算,不过异或的结果并没有考虑进位,所以我们还要计算出两整数相加的进位情况,这个可以借助与运算来解决,下面给出图示方便理解:
class Solution {
public:
int getSum(int a, int b) {
int ret = a^b;
unsigned int And = ((unsigned int)(a&b))<<1;//(a&b)=-1再左移到符号位属于未定义行为
while(And)
{
int tmp = ret;//下面的And里面的ret不能是更新后的
ret = ret ^ And;
And = ((unsigned int)(tmp & And))<<1;
}
return ret;
}
};
例题二:只出现一次的数字II

1、分析:这道题可以用位图的思想尝试解决:因为只有一个数是只出现一次,其他都是三次,那么我们从所有的数结合在一起来看,是不是32位bit位图里,每一个bit位是1的总数一定是3n+1(只出现一次的数的这个bit位是1)或者3n(只出现一次的数的这个bit位是0),那么我们可以遍历nums数组,计算每一个bit位的1的总数目,然后去模3,如果结果是1,是不是可以说明只出现一次的数的这个bit位是1。
2、代码:
class Solution {
public:
int singleNumber(vector<int>& nums) {
//计算所有数32个比特位每一个比特位的总和,%3==1,single对应比特位为1
int ret = 0;
for(int i =0;i<32;i++)
{
int sum =0;
for(auto x:nums)
{
//计算1的个数
if(((x>>i)&1)==1)
sum++;
}
if(sum%3==1)
{
ret = ret|(1<<i);
}
}
return ret;
}
};
例题三:消失的两个数字

1、这道题虽然是困难题,但是如果做过这两道题:260.只出现一次的数字III 和 268.丢失的数字
那这道题还是很简单的。

将nums数组所有元素和1到nums.size()+2的数字异或得到只出现一次的a^b,因为a和b不相同,一定可以在异或结果找到为1的比特位,再根据这个比特位将所有数分成两部分,分别再异或一次,就可以把a和b给区分开了。
2、代码:
class Solution {
public:
vector<int> missingTwo(vector<int>& nums) {
int xorall = 0;
for(auto e:nums) xorall^=e;
for(int i =1;i<=nums.size()+2;i++)
{
xorall^=i;//缺的两个数的异或结果
}
//找到xorall哪一个比特位为1
int lowbit = 0;
while(1)
{
if(((xorall>>lowbit)&1)==1) break;
lowbit++;
}
int a = 0,b=0;
//根据lowbit将nums和【1,nums.size()+2】分成两部分
for(auto e:nums)
{
if(((e>>lowbit)&1)==1) a^=e;
else b^=e;
}
for(int i =1;i<=nums.size()+2;i++)
{
if(((i>>lowbit)&1)==1) a^=i;
else b^=i;
}
return {a,b};
}
};
模拟
例题一:提莫攻击

1、分析:看了示例1大概可以明白这道题需要我们做什么,我们可以抽象成一个数轴:如果b-a>=duration,是不是就是第b秒之前就执行完duration秒,中毒时间要加duration;如果b-a<duration,就相当于没有中毒duration秒就再次中毒,那中毒时间要加b-a秒,之后再从第b秒开始计算中毒时间。最后一次中毒因为不需要判断差值,所以就直接加上duration。

2、代码
class Solution {
public:
int findPoisonedDuration(vector<int>& timeSeries, int duration) {
int ret = 0;
for(int i= 0;i<timeSeries.size()-1;i++)
{
int x = timeSeries[i+1]-timeSeries[i];
if(x>=duration) ret+=duration;
else ret+=x;
}
ret+=duration;
return ret;
}
};
例题二:Z字形变换

1、分析:这道题需要我们将输入字符先排成Z字形,然后再从左往右依次读取字符形成新的字符串,如果我们直接进行模拟,就需要创建矩阵,把字符串的字符按照一定顺序放入矩阵,之后再把矩阵的字符一一读取形成新的字符,假设这个矩阵有len列,这样做时间复杂度和空间复杂度是O(n*len)。
我们可以观察这个矩阵得出规律,下面不用具体例子,用下标来模拟上面这个过程,然后再得出规律,看是否能降低时间复杂度和空间复杂度:

上图n=4,观察第一行:是不是第一行的公差是6,这个公差可以由2*n-2得到;最后一行从下标3开始,就是n-1,公差也是6;第二行根据红色连线,应该是先有(1,6-1),然后(1+6,6-1+6)得到(7,11);第三行也是如此。可以初步得出规律:
(1)第一行:0->0+d->0+2d->....
(2)最后一行:n-1->n-1+d->n-1+2d->....
(3)其它行(假设第 i +1行):(i,d-i)->(i+d,d-i+d)->(i+2d,d-i+2d)->....
单看n = 4这个规律可能不够准确,可以再对照n=3,会发现也符合上述规律。
每一行的边界条件都是不要超过输入字符串长度,并且在写其它行代码时的判断条件有两个,可以看代码实现。
class Solution {
public:
string convert(string s, int numRows) {
//处理边界情况
if(numRows==1) return s;
int d = 2*numRows-2;
string ret;
for(int i=0; i<numRows; i++)
{
//处理第一行和最后一行
if(i==0||i==numRows-1)
{
int a= 0;
while(i+a*d<s.size())
{
ret.push_back(s[i+a*d]);
a++;
}
}
//处理中间行
else
{
int a= 0;
while(i+a*d<s.size())
{
ret.push_back(s[i+a*d]);
if(d-i+a*d<s.size())
ret.push_back(s[d-i+a*d]);
a++;
}
}
}
return ret;
}
};
例题三:外观数列

1、分析:这道题是需要我们从1到n,每一次都对前一个数组的元素情况进行概括,就比如前一个数组是[3,3,3,1,1,4,5,5],那我们要做的就是遍历这个数组得出有3个3,2个1,1个4和2个5,然后把这些数字3 3 2 1 1 4 2 5放入新数组,直到遍历了n次。那我们是不是可以借助双指针,由left和right来找到含有相同数字的连续区间,并计算个数。
2、代码细节:注意每次遍历前一个数组,right都不能超过前一个数组的大小;还要注意right越界时是没有计算[left,right-1]连续区间的个数的,所以后面还需要再单独计算一次。
class Solution {
public:
string countAndSay(int n) {
string v1 = "1";
//执行n-1次
for(int i = 2;i<=n;i++)
{
string tmp;
int left = 0,right=0;
while(right<v1.size())
{
if(v1[left]==v1[right]) right++;
else
{
tmp+= to_string(right-left)+v1[left];
left = right;
}
}
tmp+= to_string(right-left)+v1[left];
v1=tmp;
}
return v1;
}
};
例题四:数青蛙

1、分析:这道题我们单看题目和示例可能没有头绪,那现在举个例子借助一个hash表:croakOfFrogs={c r c o a k r o a k c r o a k},从下标0的 c 开始,走到 r 时是不是需要看一下前面有没有 c 的出现,如果出现我们可以让hash表 c 的个数--, r 的个数++;走到 o 时是不是需要看一下前面有没有 r 的出现,如果出现我们可以让hash表 r 的个数--, o 的个数++;也就是说遇到' r o a k '都得往前看,有没有它们对应的前缀字母,如果没有是不是就不符合条件直接返回-1了。对于c 就比较特殊,因为题目是要不同青蛙的最少数目,如果遍历到 c 时,k已经不为0了,可以认为已经有一只青蛙叫完"croak"了,那我是不是可以让它重新再来一次,也就是让hash表里 k 的个数--。
这里遍历完数组,也不能直接return k,还要检查“c r o a”的个数是不是都为0,为0字符串才有效。
2、代码实现:可以直接用if...else...把五个字母分开;也可以用数组模拟hash表,但是需要一个map表记录每个字母和下标对应关系,方便hash直接映射
class Solution {
public:
int minNumberOfFrogs(string croakOfFrogs) {
int c=0, r= 0, o = 0,a=0,k=0;
for(auto e:croakOfFrogs)
{
if(e=='c')
{
if(k>0)
{
k--;
c++;
}
else c++;
}
else if(e=='r')
{
if(c==0) return -1;
else
{
c--;
r++;
}
}
else if(e=='o')
{
if(r==0) return -1;
else
{
r--;
o++;
}
}
else if(e=='a')
{
if(o==0) return -1;
else
{
o--;
a++;
}
}
else if(e=='k')
{
if(a==0) return -1;
else
{
a--;
k++;
}
}
}
if(c||r||o||a) return -1;
return k;
}
};
class Solution {
public:
int minNumberOfFrogs(string croakOfFrogs) {
string t = "croak";
vector<int> hash(t.size());
unordered_map<char,int> map;
int i = 0;
//将五个字母和下标映射
for(auto e:t)
{
map[e]= i++;
}
for(auto e: croakOfFrogs)
{
if(e == 'c')
{
if(hash[t.size()-1] > 0)
{
hash[t.size()-1]--;
}
hash[0]++;
}
else
{
if(hash[map[e]-1] > 0)
{
hash[map[e]-1]--;
hash[map[e]]++;
}
else return -1;
}
}
for(int i = 0; i < t.size()-1; i++)
{
if(hash[i]) return -1;
}
return hash[t.size()-1];
}
};

201

被折叠的 条评论
为什么被折叠?



