实现加减乘除的必要说明
在进行接下来的操作之前,我们先搞清楚计算机是怎么储存10进制的数字。计算机不像我们人,它只能识别2进制的数字,即只能识别0,1,所有的10进制以及一系列的复杂操作,都是0,1组合起来,在计算机的某些硬件上通过电路表现计算机能看的懂得2进制语言。
对于计算机来说,它能直接看得懂的操作符号有(这里列出常见的位运算符号):
&(与) | 都为1才1;有0为0 |
|(或) | 都为0才0;有1为0 |
!(非) | 1变0;0变1 |
^(异或) | 相同为0;不同为1 |
这些符号的意思就不解释了,属于最基本的内容。另外,这里最好把^(异或)理解为——无进位的加法最好,或者说以后就把^理解为无进位的加法。
OK了解了这些之后,我们还要明白计算机对于存储的数据不可能是无限大的,以int整型为例:
它的范围为:-2^31 —— 2^31-1 ,即:-2147483648 ——2147483647。当超过最大值或者最小值后,数据会溢出。对于非负:0—— 2^31-1 ;对于负数-2^31——-1。🆗了解了这么多就可以开始我们主要的表演了——位运算实现加减乘除。
位运算的加法
我们在进行10进制的加法运算的时候,逢10进1,同样的我们2进制也可以这么模拟10进制来进行加法运算。
上面我们介绍了可以把^运算理解成不进位的加法,那么是不是我们再用另一个变量处理进位后的结果,再把两者结合在一起是不是就实现了模拟加法运算呢?很对,我们位运算的加法就是这么个大体思想。
首先:我们用变量answer处理两个数相加不进位的结果,异或^实现;
然后:再处理他们的进位的数据;
最后:整合;
以42,2为例:
首先他们的状态是这样:
第一步:一开始用answer处理两个数异或后的结果——注意这只是处理每个位上没进位后的结果
从图片看出(从右往左,从0位开始)a的第一位和b的第一位,应该进位。
第二步:也就是说a的第二位与b的第二位加起来结果应该为1,可以把a&b得到的结果再向左移动一位,就完成了进位操作(与运算只关心是否都为1,这正好也是进位的条件)。
最后再把answer ^ 第二步得到的结果 = 新结果,如此循环往复,直到进位的值为0。
int add(int a,int b)
{
int answer=a;
//如果b==0则不需要进行加法
while (b!=0)
{
answer=a ^ b; //处理没有进位的加法
b = (a & b) << 1; //处理进位信息
a = answer; //接着下一轮循环
}
return answer;
}
位运算的减法
减法就很好说了,减法就是加其的相反数,而一个数的相反数可以先取反再加一得到。
-a=~a+1;
//相反数
int neg(int a)
{
return add(~a,1);
}
//位运算减法
int sub(int a,int b)
{
return add(a,neg(b));
}
位运算的乘法
乘法也是模拟我们10进制的竖式乘法。
这个是10进制的数相乘得到的结果;其实再细分可以有如下步骤:一个乘数向左移动;一个乘数向右移动;直到向右移动的数为零则结束移动,并把所有得到的结果相加则为答案。
模拟2进制乘法也一样,而且更简单,因为不用担心两数相乘会进位的情况(1*1是最大的可能) ,最后也和10进制一样,只要有个数字为0,结束移动,并且把得到的数字相加(用前面add函数)。
int mutl(int a,int b)
{
if (a<0 && b<0) {a=neg(a);b=neg(b);} //如果都为负数,则都转化位正数
if (a<0) {a=neg(a);} //有一个为正数,则把另一个转化为正数(>>表示带符号移动)
if (b<0) {b=neg(b);}
int answer=0;
while (b!=0)
{
if((b & 1)!=0)
{ //把相乘不是0的结果加起来
answer=add(answer,a); //当发现向右移动的那个数最低位!=0,表明相乘的数不为0
//就把得到的结果加起来
}
a = a<<1; //一个数向左移动
b = b>>1; //一个数向右移动
}
return answer;
}
另外说明一下,循环中的代码都是考虑了正数相乘的结果,所以得先把负数转化为正数。(如果是java可以再b = b>>>1表示无符号右移,就不需要考虑正负的情况。>>表示带符号右移,如果是负数右移的话,会在前面补1,改变了数的性质)
位运算的除法
同样的第一步把负数先转化为正数。以42,10为例,42=10*(2^2)+2,即42/10==4;而2^2==0100
再大一点的数,781,3;781=3*(2^2+2^8)+1 而2^2+2^8=0001 0000 0100,那么可以这样:定义一个标记变量answer=0;把被除数分别向右移动30——0位(为什么不是31——1位,因为最高位管符号),再看在向右移动的过程中:如果有移动后的数字>=除数(说明除数是被除数的一个组成),则把answer相应的位数标为1(用answer ^ (1 << i)实现标记),且把被除数减去移动后的数字作为新的被除数,如此循环下去,直到除数大于被除数,则退出循环;若移动后的数字>除数,则继续往后移动,一直找到循环结束还没有找到移动后的数字>=除数,则退出循环。
这里比较难以理解的是answer第i位置1与a=a-b<<i。慢慢分析,a=a-b<<i的意思就是把被除数减去移动后的数字作为新的被除数。answer位置为1,来细说。
这是answer在这个过程中要完成的事情,至于说answer是如何把0变1的,那就又用到了^运算。
当发现满足a>=b的条件时,此时answer第i位为1,则可以用answer ^ (1 << i) 来实现。这就实现了answer的标记。
至此是不是以为位运算圆满结束了,并没有,在考虑除法的时候还有边界情况没有考虑,或者这么说,我们上面实现的位运算除法只是满足与非负数与负数的运算,负数那部分并没有包括INT_MIN情况,即int类型的最小值情况。
所以除法还得加上一些边界。
int divide(int a,int b)
{
if (a==INT_MIN && b==-1) return INT_MAX;
if (a==INT_MIN && b==INT_MIN) return 1;
if (a!=INT_MIN && b==INT_MIN) return 0;
if (a!=INT_MIN && abs(a)<abs(b)) return 0;
if (a>0)
{
return divi(a,b);
}
if (a<0)
{
a = b>0 ? a+b : a-b;
}
int offset=b > 0 ? -1 : 1;
int ans=divi(a,b);
return ans+offset;
}
int divi(int a,int b)
{
int x = a<0 ? neg(a) : a;
int y = b<0 ? neg(b) : b;
int answer=0;
for (int i=30;i>=0;i=sub(i,1))
{
if ((x >> i)>=y)
{
answer = answer | (1<<i);
x = sub(x,y<<i);
}
}
//异或相同为0 不同为1
//a<0 b<0 ->异或为0->返回正answer
//a<0 b>=0 ->异或为1->返回负answer
//a>=0 b<0 ->异或为1->返回负answer
return a<0 ^ b<0 ? neg(answer) : answer;
}
分析一下里面比较难懂的代码:
if (a==INT_MIN && b==-1) return INT_MAX;
if (a==INT_MIN && b==INT_MIN) return 1;
if (a!=INT_MIN && b==INT_MIN) return 0;
if (a!=INT_MIN && abs(a)<abs(b)) return 0;
if (a<0)
{
a = b>0 ? a+b : a-b;
}
int offset=b > 0 ? -1 : 1;
int ans=divi(a,b);
return ans+offset;
前面四个判断:
第一个if a==最小值,b-==-1 两者相除返回最大值 (注意最小值的绝对值与最大值的绝对值不一样,具体原因开头有答案)
第二个if a==最小值,b==最小值 两者相除返回1
第三个if a!=最小值,b==最小值 两者相除返回0
第四个 if a!=最小值的前提下,当a的绝对值小于b,两者相除返回0
后面那个 if(a<0) 其实更加准确的说法是:if(a<0 && (b!=-1 && b!=INT_MIN))
if里面的 语句是当a=最小值,b<0,如果此时再直接调用除法函数,会有溢出的情况(INT_MIN没有其相反数与他对应),所以为了处理这种情况,可以把被除数加上除数的相反值,让其远离最小边界,就是a = b>0 ? a+b : a-b;做的工作。
当你改变了被除数的大小,又要得到正确结果,你最后得变回来,之前是加上除数远离,那么这个时候就得减回来;同理减去除数远离,就得加回来。就是int offset=b > 0 ? -1 : 1;的工作。
最后把变换后的a,b 进行除法运算,即可得出结果。
完整代码
#include <iostream>
using namespace std;
//相反数
int neg(int a);
//位运算加法
int add(int a,int b);
//位运算减法
int sub(int a,int b);
//位运算乘法
int mutl(int a,int b);
//位运算除法
int divi(int a,int b);
int divide(int a,int b);
int main()
{
cout<<divide(INT_MIN,INT_MIN)<<endl;
cout<<divide(8988,INT_MIN)<<endl;
cout<<add(100,2627378)<<endl;
cout<<sub(929293939,233434)<<endl;
cout<<mutl(47838,283)<<endl;
cout<<divide(28389349,2)<<endl;
cout<<divide(INT_MIN,-1)<<endl;
cout<<divide(-28,4)<<endl;
cout<<divide(-123,-3)<<endl;
}
int neg(int a)
{
return add(~a,1);
}
int add(int a,int b)
{
int answer=a;
while (b!=0)
{
answer=a ^ b;
b = (a & b) << 1;
a = answer;
}
return answer;
}
int sub(int a,int b)
{
return add(a,neg(b));
}
int mutl(int a,int b)
{
if (a<0 && b<0) {a=neg(a);b=neg(b);}
if (a<0) {a=neg(a);}
if (b<0) {b=neg(b);}
int answer=0;
while (b!=0)
{
if((b & 1)!=0)
{
answer=add(answer,a);
}
a = a<<1;
b = b>>1;
}
return answer;
}
int divide(int a,int b)
{
if (a==INT_MIN && b==-1) return INT_MAX;
if (a==INT_MIN && b==INT_MIN) return 1;
if (a!=INT_MIN && b==INT_MIN) return 0;
if (a!=INT_MIN && abs(a)<abs(b)) return 0;
if (a>0)
{
return divi(a,b);
}
if (a<0)
{
a = b>0 ? a+b : a-b;
}
int offset=b > 0 ? -1 : 1;
int ans=divi(a,b);
return ans+offset;
}
int divi(int a,int b)
{
int x = a<0 ? neg(a) : a;
int y = b<0 ? neg(b) : b;
int answer=0;
for (int i=30;i>=0;i=sub(i,1))
{
if ((x >> i)>=y)
{
answer = answer | (1<<i);
x = sub(x,y<<i);
}
}
return a<0 ^ b<0 ? neg(answer) : answer;
}
附上运行图
力扣:两数相除https://leetcode.cn/problems/divide-two-integers/
解题代码:
class Solution
{
public:
int divide(int a,int b)
{
if (a==INT_MIN && b==-1) return INT_MAX;
if (a==INT_MIN && b==INT_MIN) return 1;
if (a!=INT_MIN && b==INT_MIN) return 0;
if (a!=INT_MIN && abs(a)<abs(b)) return 0;
if (a>0)
{
return divi(a,b);
}
if (a<0)
{
a = b>0 ? a+b : a-b;
}
int offset=b > 0 ? -1 : 1;
int ans=divi(a,b);
return ans+offset;
}
int divi(int a,int b)
{
int x = a<0 ? -a : a;
int y = b<0 ? -b : b;
int answer=0;
for (int i=30;i>=0;i--)
{
if ((x>>i) >= y)
{
answer = answer | (1 << i);
x = x - (y<<i);
}
}
return (a<0) ^ (b<0) ? -answer : answer;
}
};