接前面一篇博客,这又是某个公司的奇葩面试题(都说了到底是哪家公司才会出这种没营养的面试题)。不过吐槽归吐槽,这个题目还是有点学问的,比前面那个 不使用比较运算符如何比较两个数的大小 强多了(但是还是能看出面试官是在存心刁难人)。
原题是“只给加法,如何实现减乘除”,我寻思着,既然减乘除都不给了,那就加大点难度,加法也别给了吧,今天就手动去实现加减乘除。这里只实现int类型的加减乘除。
一说到这种“不给xxx如何实现xxx的问题”,那第一个想到的就是位运算,因此本篇博客的各种运算也是基于位运算的。关于位运算的知识点,请阅读本博客的看官们自行百度学习。。。。
我们先举个加法的例子:8+9=17,可以转换成10+7,即:二者相加不考虑进位的值,和二者相加只考虑进位的值相加。我们再通过二进制来直观的看一下。
0000 1000 (8)
0000 1001 (9)
0001 0001 (17)
下面描述一下思路(可能有点绕)。
首先我们看,直接二进制相加的结果,0+0=0,1+0=1,1+1=10。好像能看出点什么。前两个的运算规则复合“异或运算”,而后者则复合与运算并左移1位。
到现在思路就清楚了:a^b的结果是不考虑进位的结果,而a&b<<1是只考虑进位的结果。把二者相加即可。如果相加后,可能还存在进位,那就让这两个数字继续相加,一直到进位为0为止。这里使用递归去实现,感兴趣的可以用循环实现,性能比递归要高。
public int add(int a, int b) {
// 得到原位和
int xor = a ^ b;
// 得到进位和
int forWoad = (a & b) << 1;
return forWoad == 0 ? xor : add(xor, forWoad);
}
到这里,加法就实现完毕了
计算机中的负数实现,是将正数按位取反获取反码,之后+1获得补码,这个结果就是某个正数所对应的负数。(这个在计算机组成原理、操作系统、计算机导论、离散数学等课本中都有,不记得的请翻一下大学课本。)。
负数的实现其实还是比较简单的,按位取反之后+1即可。
public int negative(int num) {
return add(~num, 1);
}
实现了负数之后,我们第一步实现的加法就可以和负数进行运算了,而减法也就变得简单起来。
减法的实现如4-2等价于4+(-2),我们直接使用加法和负数就可以实现。
public int minus(int a, int b) {
return add(a, negative(b));
}
接下来要实现乘法和除法。乘法和除法可能会有正数和负数相互计算的情况,因此我们实现乘除之前,需要先实现绝对值计算的功能,将运算数字转换成绝对值进行乘除,之后判断是否需要加上负号即可。
public int abs(int num) {
if (num < 0) {
num = minus(0, num);
}
return num;
}
乘法的实现,如11*10,乘法的流程如下面所示。
0000 1011 (11)
0000 1010 (10)
================
0001 0110 (1011<<1,相当于乘以0010)
0101 1000 (1011<<3,相当于乘以1000)
可以看到,二进制乘法的原理是:从乘数的低位到高位,遇到1并且这个1在乘数的右边起第i(i从0开始)位,那么就把被乘数左移i位得到temp_i,直到乘数中的1遍历完毕后,把根据各位1而得到的被乘数的左移值全部相加即得到乘法结果。
而至于存在负数的运算,可以先获取负数的个数,再将两个数字转换成绝对值计算,最后判断当负数是1个时,计算结果就是负数,其他情况则是正数。
public int multi(int a, int b) {
// 先获取负数的个数
int negativeCount = negativeCount(a, b);
// 负数转正数进行计算
a = abs(a);
b = abs(b);
int i = 0;
int res = 0;
// 乘数为0则结束
while (b != 0) {
// 处理乘数当前位
if ((b & 1) == 1) {
res = add(res, a << i);
}
b = b >> 1;
i = add(i, 1);
}
if (negativeCount == 1) {
// 转为负数
res = negative(res);
}
return res;
}
negativeCount方法
public int negativeCount(int a, int b) {
int count = 0;
if (a < 0) {
count = add(count, 1);
}
if (b < 0) {
count = add(count, 1);
}
return count;
}
乘法事实上有个简单的实现。乘法就是个连加的过程,如5*4就是4个5相加,这个虽然性能比较低,但是操作起来简单,感兴趣的朋友可以自己去实现。
除法没有什么简单的二进制实现方案,实际计算机中的除法也是通过连减去计算的。a/b的意义就是求a可以由多少个b组成,因此除法可以求a能减去多少个b。至于负数的情况,和乘法相同,不再介绍。
public int sub(int a, int b) {
// 先获取负数的个数
int negativeCount = negativeCount(a, b);
// 负数转正数进行计算
a = abs(a);
b = abs(b);
int res;
if (a < b) {
return 0;
} else {
res = add(sub(minus(a, b), b), 1);
}
if (negativeCount == 1) {
// 转为负数
res = negative(res);
}
return res;
}
取模运算的思路和除法一样,也是个连减的过程,一直减到我们减不了为止,剩下的值就是我们要的结果。
public int mode(int a, int b) {
// 先获取负数的个数
int negativeCount = negativeCount(a, b);
// 负数转正数进行计算
a = abs(a);
b = abs(b);
int res;
if (a < b) {
res = a;
} else {
res = sub(minus(a, b), b);
}
if (negativeCount == 1) {
// 转为负数
res = negative(res);
}
return res;
}
总的来说,加减乘除的实现还是比较简单的,只是对于初学者来说比较难想。熟悉了这类“不给xxx如何实现xxx”的题目之后,就能第一时间想到位运算了,通过位运算去实现运算符规则,实现起来就没有什么难度了。
最后还是需要点评一下这道题。这个问题相比上次的问题,稍微的有那么点水平,但是还是不难看出面试官在刁难人。程序员需要懂的原理,应该是开发中的各种框架原理,如HashMap、Spring、Mybatis等,理解了原理才能更好的优化、扩展,以便于提高性能。而所谓的加减乘除原理并没有这些重要,往往在上大学的时候也就了解过了。加减乘除原理的理解对性能优化帮助并不大,即使位运算性能比减法和除法高,但是这点性能损耗,在我们服务器动辄4g8g的情况下是没有任何区别的。所以说面试的时候别问这种刁难人的问题啊,你就是造造火箭问问Spring也好!