位运算2
紧接上回,这回我们来谈一下上次没有说完的3,4,如有错误请大佬指教
3.位运算的各种nb操作
4.位运算实现加减乘除
3.位运算各种nb操作
因为位运算的性质比较简单,所以我这里就不展开叙述了,这里准备了几个题目,让我们 体会一下位运算的精妙之处。
1.判断一个整数是否是2的幂
思路:可能许多人会说,这跟位运算有什么关系?我只要从小到大枚举2的幂,依次与所给数判断相等不相等不就行了吗?的确,这种方法是可行的,但这种方法的时间复杂度为O(logn),太高了。我们想要优化到O(1)的时间复杂度,这时我们就要用到位运算了。我们不妨 观察2的幂的二进制数,发现有一个共同的特点:那就是它们的二进制位数上有且只有有一个1!所以只要我们将目标数转换为二进制数,判断他是否只有一个1就行了。那如何判断它只有一个 1呢?这时我们就不得不提到我们位运算 的一个大明星“lowbit"函数,该函数的作用是获取一个数二进制数最右侧的1。它在树状数组中有着十分重要的作用,它的原理我这里就不展开讲述了,有兴趣的可以自行搜索一下。当我们获得了lowbit函数所得到的数x,将 x与 目标数进行判断,如果两者相等,那么目标数是2的幂,否则不是。
下面 我们看code
class Solution
{
public:
int lowbit(int x)
{
return x & (-x);
}
bool check(int num)
{
if (num>0&&num == lowbit(num))
return true;
return false;
}
};
2.返回>=n的最小的2的幂
思路:这一题不用位运算,用暴力的思想的话十分的简单,但用位运算的话可能就不是那么的好想了
首先我们让n-1,例如n=20,二进制数为010100,减1后为010011,将最左侧的1之后的数全部刷成1,即011111,再让这个数+1,得到100000,结果为32,即为目标数。其实我们仔细想一下,的确实这样的,如果一个数a它不是2的幂那它一定存在一个数x使2^x<a <2^(x+1), 那么a-1 >= 2^x的, 我们的目标就是2^(x+1),a-1的有效二进制位数与 2^x的位数是一样的,所以我们只需要将a-1的有效位上的数全部刷成1,然后再加1,就得到了目标数,n–的目的就是防止a为2的幂的特殊情况。
而我们如何将最左侧的1之后的数全刷成1呢?只需要将先将a|=a>>1;那么左侧最少有2个1,再a|=a>>2,那么左侧最少有4个1,因为int 只有32位,所以最多只需要>>16;
下面我们看code
class Solution
{
public:
int MinPowOfTwo(int num)
{
if (num <= 0)
return 1;
num--;
num |= num >> 1;
num |= num >> 2;
num |= num >> 4;
num |= num >> 8;
num |= num >> 16;
num++;
return num;
}
};
关于位运算,就介绍到这里,我说的都是一些很基本的内容,实际上位运算有更加牛逼的操作,那些操作有的令人直呼:wc。如果 想看的话,你可以去b站左程云大神的视频观摩学习,我的blog的内容也是根据左神的内容来写的,在这里不得不感谢一下左神,左神yyds!
4.位运算实现加减乘除
1.位运算实现加法
说到加法,我们 可以用小学学过的摆式子的方式来理解加法,也就是个位与个位相加,十位与十位相加······,谈到相加我们不禁想起了位运算中有一个无进位相加——异或,但实际上的相加为有进位相加,该怎么调整呢?其实我们只需要记录一下进位信息即可,最后加上进位信息。
下面我们看一下code
class Solution
{
public:
int add(int a, int b)
{
int ans = a;
while (b != 0)
{
//ans表示a与b无进位相加的结果
ans = a ^ b;
//表示进位信息
b = (a & b) << 1;
a = ans;
}
return ans;
}
2.位运算实现减法
实际上减法就是加法,只不过是加个减数的相反数罢了,而减数的相反数其实就是原数取反+1。所以a-b=a+(-b)=a+(~b+1)
所以code
class Solution
{
int sub(int a, int b)
{
//a-b也就是a+(-b),-b可以表示为~b+1
return add(a, add(~b, 1));
}
};
3.位运算实现乘法
位运算实现乘法也可以用摆式子的方式理解,例如67,我们将这两个数转化成二进制数01100111,我们从7的最右侧位开始看,10110,我们定义一个变量ans接收计算过的值,所以ans+=0110,然后我们再用7倒数第二位的10110,这时最右侧的1就已经没用了,所以再计算前将0111右移一位,则之前倒数第二位1变成了最右侧的1,拿这个1*0110,并且此时我们要将所得的计算结果左移1位然后ans+=01100,就这样计算,直到7为0;而ans就是最终的结果
但是以上只能针对于非负数相乘才成立,因为如果是负数相乘的话,其最高位为1,将其右移的话最高位会补1而不是补0,这一点如果是java的同学可以用无符号>>>来弥补,但是c++的语法里没有>>>,这是只能我们手写无符号右移函数
下面我们直接上代码,通过代码来理解
class Solution
{
public:
int f(int x, int cnt)
{
/*
如果x为负数x>>cnt位后左侧就会出现cnt个1,我们需要
将这cnt个1变为0,我们只需要将(1<<32-cnt)-1后与x>>cnt&一下即可
*/
return (x >> cnt) & (sub(1 << sub(32,cnt), 1));
}
int mul(int a, int b)
{
int ans = 0,cnt=1,res=b;
if (a == 0 || b == 0)
return 0;
while (b != 0)
{
int temp = b & 1;//获取b的最后一位
if (temp)
{
ans += a;
}
a <<= 1;
b = f(res, cnt++);//b进行无符号右移
//f为无符号右移函数
}
return ans;
}
};
4.位运算实现除法
除法的实现可能就要复杂一点,我们先从例子入手,例如45/4,跟之前的加减乘法不一样,我们不需要先将两个数转换成二进制数,我们只需要判断被除数与除数2的幂的大小即可,也就是45与42的幂的大小,2先从2的30次方开始,即比较452^30与45的大小,如果前者大,指数-1,直到比到45>= 4(2^i),此时i=3,
所以二进制第三位上为1(注意这里的第三位实际上是第四位),然后让45-4*
2^3=13,i从2继续开始判断,直到判断到i=0;最终得到的数是1011,转换成10进制数为11,答案正确。
但仔细想一想这是有着十分严重的问题的,因为你开始让452^30次方,这个数在int范围内是溢出的,直接把这个数溢出成负数了,那还判断个毛线呀。这时就有一个很巧妙的方法,原式是判断a>=b(2 <<i),那我们可以 两边同时除以2 ^i,原式就变成了a>>i >=b;这样就避免了溢出风险。下面我们看一下code
class Solution
{
public:
int add(int x, int y)
{
//无进位相加再加上进位信息
int ans = x;
while (y != 0)
{
ans = x ^ y;
y = (x & y)<<1;
x = ans;
}
return ans;
}
int neg(int x)//对所传入的数取反
{
return add(~x, 1);
}
int sub(int x, int y)
{
//将减法看成加法
return add(x, neg(y));
}
int div(int x, int y)
{
//先将这两个数转换成两个非负数
int a = x > 0 ? x : neg(x);
int b = y > 0 ? y : neg(y);
int ans = 0;
for (int i = 30; i >= 0; i=sub(i, 1))
{
if ((a >> i) >= b)
{
ans |= 1 << i;
a = sub(a, b << i);
}
}
return (x > 0) ^ (y > 0) ? neg(ans):ans;
}
};
但是上述的写法仍然存在一点问题,就是如果说a,b中存在整数最小值的话,那么该做法就是错误的,因为如果存在整数最小的话,int的范围是-2^31~ 2 ^31-1,不存在 2 ^31这个数,有的同学就说了那我就转换成long long 类型不就行了吗,的确可以,但这是 一种治标不治本的做法,如果所给的数是long long 最小呢,所以我们要从根本上解决这个问题
我们分析一下当a,b两者中至少有一个整数最小时的情况
1.当a=b=min时,直接返回1;
2.当a=min,b=1或-1时,结果为min或max+1(整型不支持转换成字符串型)
3.当a!=min,b=min时,结果为0;
4.当a=min, b!=min时,
这时当b>0时,a/b=((a+b)/b)-1;这样a+b,b就可以调用div函数
当b<0时,a/b=((a-b)/b)+1; 这样就可以调用div函数了
这段code我就不发了,大家可以练习一下
最后位运算就剩位图的实现了,欲知后事如何,且听下回分解。
953

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



