目录
《算法设计手册》(The Algorithm Design Manual)是本比较经典的算法书了。如果说《算法导论》偏向于数学,那么《算法设计手册》更偏向于工程应用(至于《计算机程序设计艺术》,目前我是没时间通读,只是偶尔当工具书查查,就不提了)。前者的课后题中的面试题部分挺潮的,如果在google上搜索一下,发现很多都是名企考过的,或许是因为第二版出版时间比较近的缘故?我不大相信是作者自己出的然后被大公司拿去面试的,而是作者收录的考过的面试题。有了这一层筛选,这些面试题质量有保证啊。
由于看的是英文版,大部分题都是我翻译过来的,个人英文水平有限,虽然有的不好理解的地方参照一些解答来帮助理解,并咨询了在国外留学的朋友,可能仍有些措辞不准确或有误的地方,恳求谅解并欢迎提出。同时,有的题目用到的比较冷僻的知识可能会和正文有关系,这种情况会明确注明。
虽然官方网站上有个wiki answer提供了大多数答案,不过有的不很合适,我写的和整理的都是比较好的解答。
原作者的wiki answer页面http://www2.algorithm.cs.sunysb.edu:8080/mediawiki/index.php已经失效。
第1章:
不用*和/计算整数除法。请找出最快的方式。
解答:
虽然初始化一个计数变量,每当被除数减去除数的一次就自增一直到被除数小于除数这个暴力解法可行,但显然很慢。这是wiki answer答案,但它在很多情况下都不快,比如100/1。其执行的次数正好和相除的结果相同,用m表示除数,n表示被除数,时间复杂度是O(m/n)。
// Note: This only works for positive values! int divide(int numerator, int denominator) { int quotient = 0; while(numerator >= denominator) { numerator -= denominator; quotient++; } return quotient; }
下面看看另一种解法。
一般限制使用*和/时,很容易考虑使用位移运算来替代,因为对于无符号数,左移一位(在不溢出时)相当于乘以2,右移一位相当于除以2。如果在纸上进行除法的笔算,是只用到了乘法和减法的。但是一般的十进制整数除法和位运算有什么关系呢?为了将两者建立联系,必须把十进制数转化成二进制数,观察除法的进行情况来找规律。比如100/7,写成二进制来进行笔算,计算过程如下图:
这样就简单了,从这个式子可以看出,二进制除法笔算只涉及了减法和隐含的移位与大小比较,原先的乘法已经被移位所代替。因此,具体的编码,就是把用笔算除法的过程转化成代码而已。
不过,一般考虑使用除法的环境,必然要考虑除数是否为0。除数为0时这个除法是非法的,不能继续进行,需要报错。
既然提到了编码,如果使用C语言来完成,要注意的是:在C标准中,带符号数右移的结果在C语言里是实现相关的,具体结果取决于实现,而不一定是用符号位补、用1补或者用0补最高位。为了避免这个陷阱,建议先确定结果——也就是商的符号,然后把被除数和除数都转化为无符号数,这样位移时就不会出错。
但是,这又涉及了有带符号数与无符号数的转换,它们二者的表示范围的问题是不同的。好在被除数和除数从带符号数转化为无符号数时并不会丢失数据,而且商的绝对值必然小于被除数的绝对值(因为除数是整数,为0时报错,大于等于1时才继续进行),这时把商转化回带符号数时也不会丢失数据,可以放心的进行。不过这一点最好在面试时告诉面试官你已经注意到了这个问题,肯定会为你的印象加分。
int division(int m,int n) { //calculate m/n without * and / unsigned int rest,divisor, op,result = 0; int flag; int bits = 0; //bits用于记录商的1在哪一位 assert(n!=0); if((m<0 && n>0) || ( m>0 && n<0 )) flag = -1; else flag = 1; rest = m>0?m:-m; divisor = n>0?n:-n; if(rest < divisor) return 0; op = divisor; /* 2013.8.30 */ /*经过博客园园友infinityu的提醒重写 */ while(op<=rest) { bits++; op=op<<1; } op=op>>1; bits--; while(op>=divisor) { if(rest>=op) { rest-=op; result += 1<<bits; } op = op>>1; bits--; } /* 重写部分结束 */ return flag * result; }
由于需要把被除数转化为二进制进行计算,最多做了其二进制表示位数次的减法,因此对于被除数m,算法复杂度为O(logm)。
稍作修改,把最后的小于除数divisor的result取出就是余数,这样就能把除法运算改写为取模运算%了。如果把参数表修改为传递结果地址,同时获得商和余数也是可以的。
可见,这一道面试题考到了算法优化、除法除数为0这个常见错误、将除法从十进制引申到二进制、二进制的位运算、语言特性中的无符号数和带符号数的位移、无符号数和带符号数的相互转换,你还可以更进一步探讨算法复杂度、以及算法的扩展性,确实很能考察被面试者对算法的掌握情况。
p.s.经过园友infinityu的提示,发现源代码中有bug,重写之后已经对1~1000之间所有整数相互相除的测试。为了便于记录商的1应该在哪一位,使用变量bits来指示。
25匹马,一次最多5匹马比赛,如何用最少的比赛次数找到最快的前三匹马?(假设所有马的速度在每场比赛的发挥都一样且各匹马之间不相同)
解答:
老生常谈的问题,关键是找出每次的正确候选以及尽量利用上次比赛获得的信息。
先分5组A、B、C、D、E,组内比赛,假设A1为A组第一。一共5场。
将A1~E1进行比赛,不妨设第一是A1,那么最快就是A1。
第二快只能在A2、B1~E1中出现。同时,这时知道了B1~E1的速度,不妨B1>C1>D1>E1,这样D1、E1以及整个D组和E组可以被排除出第二和第三的候选。同时,C2必然不可能是第三快。这时候选为A2、A3、B1、B2、C1,比赛一次,前两名即为第二和第三。
综上,一共比赛了7次。
=============================================================
1-30~1.34是几个估算题,当年google确实考过其中的题目。不过这里不做解答了。
关于估算题目的思路和解法,可以参考《编程珠玑》《编程珠玑(续)》和我写的相关文章:[珠玑之椟]估算的应用与Little定律
1.30
世界上有多少个钢琴调音师?
1.31
美国有多少个加油站?
1.32
曲棍球场上的雪有多重?
1.33
美国公路一共有多长?
1.34
平均来看,你翻开一本曼哈顿电话簿时,你需要随机翻开多少次才能找到一个给定的名字?