问题:不用基本四则运算 + - * / 实现加法运算。
1.我的思路
我想到的一种办法是,使用二进制的位运算来避开使用基本四则运算。任何数字可以表示为二进制(废话,在内存里面,什么数不是二进制表示的?)。考虑 a + b,写成二制制形式:
011010010 + 101010110,两个数字的相同对应位分别进行计算,包括第 i 位的 ai,bi,和第 i - 1 位上的进位 ci-1,有以面两个东西需要求解:
1.当前位的数字即为:ai ^ bi ^ ci-1。
2.当前位的进位。为了求这个值,我们来看一下,只有 ai,bi,ci-1 这三个数字中,有二个或三个 1 时才会发生进位,即 ci 为1。为了得到 ci 的等式,我们列出值表如下:
ai bi ci-1 异或 与 或 情况说明
0 1 1 0 0 1 两个1,进位为1
1 1 1 1 1 1 三个1,进位为1
0 0 0 0 0 0 零个1,进位为0
0 0 1 1 0 1 一个1,进位为0
...其它重复的情况就不写出,ai bi ci-1 可以认为只有四种取值:三者中有一个1,两个1,三个1,零个1
由上面的表,可以得出结果,能够进位的,只有:“异或值”等于“与值”,且“或值”为1。因此,可以写出下面的等式:
ci = ((ai ^ bi ^ c-1) == (ai | bi | ci-1) &&(1== ai | bi | ci-1))。
这种思路的源代码如下:
#define MAX_BITS 32
typedef unsigned char bitInt;
/************************************************************************/
/*
根据当前位 a , b 以及上次的进位得到当前进位。
根据实际情况,列出下面的表格:
a b carry 异或 与 或 情况说明
0 1 1 0 0 1 两个1,进位为1
1 1 1 1 1 1 三个1,进位为1
0 0 0 0 0 0 零个1,进位为0
0 0 1 1 0 1 一个1,进位为0
...其它重复的情况就不写出,a b carry 可以认为只有四种取值:三者中有一个1,两个1,三个1,零个1
由上面的表,可以得出结果,能够进位的,只有:“异或值”等于“与值”,且“或值”为1。
这便是函数的实现。
*/
/************************************************************************/
int carry(bitInt a,bitInt b,unsigned int lastCurCarry)
{
return (((a ^ b ^ lastCurCarry) == (a & b & lastCurCarry)) && (1 == (a | b | lastCurCarry)));
}
int specialAdd(int a ,int b)
{
int result= 0;
unsigned int curCarry = 0;
bitInt curBitA,curBitB;
for (int i =0;i < MAX_BITS;++ i,a >>= 1,b >>= 1)
{
curBitA = a & 1;
curBitB = b & 1;
result |= ((curBitA ^ curBitB ^ curCarry) << i);
curCarry = carry(curBitA,curBitB,curCarry);
}
return result;
}
也许你想到了以下三点:
1.最高位(符号位)也使用上面的运算步骤,是否是正确的行为。
2.倒数第二位向最高位进位,并且最高位接受这个进位并参与计算,是否是正确的行为。
3.最高位计算时还有进位,也就是溢出了,而上面的运算步骤没有处理这个溢出,是否是正确的行为。
这些问题加起来其实是一个最本质的问题:为什么数字的补码形式直接运算是正确的运算?我们假定这是正确的,那么,上面的步骤必然也是正确的,因为我们完全是在模拟这个步骤。
如果你还是想知道为什么数字的补码运算是正确的运算,请参考我的另一篇文章:《补码的本质》。
2.拿来主义-何海涛的解法(在此)
我认为何海涛的这个算法比我的算法的层次要高,要更抽象,更注重整体,因而是更好的解法。我的算法是完全模拟运算,而何的解法虽然也是完全模拟,但层次比我的高,中间有些跳跃。来看一下他的解法。
15 + 185 可以看成以下三个计算:
1.不考虑进位
1 5 1 5
+ 8 5 8 5
_______
9 0 9 0
2.考虑所有的进位,将第一步中的结果,加上所有的进位;上面的计算中,所有的(或者称为总体的)进位包括,个位和百位上的进位,分别等于10,1000,那么,这个总体的进位等于 10 + 1000
因此,最终的结果等于 9090 + (10 + 1000)
3.递归完成第二步中新产生的相加式
这三个步骤对于所有的 a + b 都是成立的。有了这个思路,程序就好写了。关键在于,如何求那个总体的进位。其实这个也好求,放到二进制中去看,只有加数的两个二进制位(ai , bi )都是 1 时,才会有进位,因此,直接使用 (ai & bi) << (i+ 1) 即得到第 i 位上的进位。
而总体的进位即是对 (ai & bi) << (i+ 1) 进行求和,即总体进位c = (a1 & b1) << 2 + (a2 & b2) << 3 + (a3 & b3) << 4 + ....= (a & b) << 1,即直接使用 (a & b) << 1 即可得到总体进位。
上面的求解揭示了以下结论:
1.一位二进制 1 和 0 相加的第一步结果(不进位结果)是 1 ^ 0;多位二进制数 a 和 b 相加的第一步结果(不进位结果)是 a ^ b。
2.一位二进制 1 和 0 相加总体进位是 (1 & 0) << 1;多位二进制数 a 和 b 相加的第一步结果(不进位结果)是 (a & b)<<1。
据此,我们可以得到很大的一个启发:满足一位二进制的运算规则,都满足由二进制构成的数字的运算规则。
这里似乎是一个哲学的观点:(最小粒子)的规则 与(纯粹由这个最小粒子构成的系统)的规则是完全一致的。
给出这种实现的代码如下:
//以下代码片段引自何海涛博客:http://zhedahht.blog.163.com/blog/static/254111742011125100605/
int AddWithoutArithmetic(int num1, int num2)
{
if(num2 == 0) //李志浩注:由于引起递归的因素是进位(不停地由加数和进位(非0)引发求新进位的问题),因此,
//此处使用进位等于 0 作为递推出口。
{
return num1;
}
int sum = num1 ^ num2;
int carry = (num1 & num2) << 1;
return AddWithoutArithmetic(sum, carry);
}
3.拿来主义-163博友(superddr)之法
这位网友的方法很精妙,说破了很简单,但能想到这个方法的人实在独具慧眼!
他利用了“数组下标”天生含有的加法功能。如 d[1] = *(d + 1)这里就有加法了,如果要实现 a + b 只需要将 a 和 b 纳入到数组下标中去计算即可。
先把握一点,数组下标中的加法是内存地址的加法,所以 a 和 b 必须都要与地址挂勾。我们先设 a 为(类型 A 的数组s的首地址)的值,如 0x000800110,也即 A *a = 0x000800110,那么 s[b]就表示取数组 s 的第 b 个元素,这个元素的地址是 &(s[b]),它是由首地址 &(s[0]) (这个值等于 a)向内存增长方向增加 b * sizeof(A) 字节的内存地址。 也即 a + b * sizeof(A) = &(s[b]);如果我们令 sizeof(A ) 为 1,那么 &(s[b]) 就是 a + b 的结果,所以 A 类型可以选择为 char 类型。
下面是代码:
int add1(int a,int b)
{
char *c = (char *) a;
return int(&(c[b]));
}