《算法设计手册》面试题解答 第一章:算法设计简介

目录

 

系列简介:

  《算法设计手册》(The Algorithm Design Manual)是本比较经典的算法书了。如果说《算法导论》偏向于数学,那么《算法设计手册》更偏向于工程应用(至于《计算机程序设计艺术》,目前我是没时间通读,只是偶尔当工具书查查,就不提了)。前者的课后题中的面试题部分挺潮的,如果在google上搜索一下,发现很多都是名企考过的,或许是因为第二版出版时间比较近的缘故?我不大相信是作者自己出的然后被大公司拿去面试的,而是作者收录的考过的面试题。有了这一层筛选,这些面试题质量有保证啊。

  由于看的是英文版,大部分题都是我翻译过来的,个人英文水平有限,有的不好理解的地方尽量参照相关解答来理解,并咨询了在国外留学的朋友,可能仍有些措辞不准确或有误的地方,恳求谅解并欢迎提出。同时,有的题目用到的比较冷僻的知识可能会和正文有关系,这种情况会明确注明。

  虽然官方网站上有个wiki answer提供了大多数答案,不过有的不很合适,我写的和整理的都是比较好的解答。

  原作者的wiki answer页面http://www2.algorithm.cs.sunysb.edu:8080/mediawiki/index.php已经失效。

  另附上在线勘误表:http://www.cs.sunysb.edu/~skiena/algorist/book/errata

 

第1章:

1-28

  不用*和/计算整数除法。请找出最快的方式。

解答:

  虽然初始化一个计数变量,每当被除数减去除数的一次就自增一直到被除数小于除数这个暴力解法可行,但显然很慢。这是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来指示。

 

1-29:

  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,比赛一次,前两名即为第二和第三。

  (注意:这里分析时没有"充分"利用所有已知信息。更进一步利用已知信息的方式请看扩展1。

  综上,一共比赛了7次。

 

扩展1:

  64匹马,每次最多8匹比赛,要求用最少场次获得前4名。其他条件同上题。

 解答:

  按照上题的分析方式并不能得到最少比赛次数,下面看看如何充分利用已知信息来达到最少比赛次数。

  首先分8组A~H决出各组顺序,共需8场,并且组内顺序排列为A1>A2>...>A8。

  第一名在A1~H1中决出,不妨设为A1>B1>...>H1,需要1场比赛。

  此时第二名只能是A2和B1其中之一(不同于上题分析,C1~H1其实可以直接抛弃),但决出第二名只用两个赛道太浪费了。为此进一步分析,如果A2>B1,那么第3名只能是A3、B1之一;如果B1>A2,那么第3名只能是A2、B2、C1。这两种情况都只涉及5匹马仍然不满8匹。用这种思路进行全面分析,表示为树状并把叶子处需要比较的马的编号进行标注:

  角逐第2名时A2>B1

    第3名候选A3,B1

    角逐第3名时A3>B1

      第4名候选A4,B1     ————(A2,A3,A4,B1)

    角逐第3名时A3<B1

      第4名候选A3,B2,C1    ————(A2,A3,B1,C1)

  角逐第2名时A2<B1

    第3名候选A2,B2,C1

    角逐第3名时A2>B2>C1

      第4名候选B2,A3     ————(A2,A3,B1,B2,C1)

    角逐第3名时A2>C1>B2

      第4名候选C1,A3     ————(A2,A3,B1,B2,C1)

    角逐第3名时B2>A2>C1

      第4名候选A2,B3     ————(A2,B1,B2,B3,C1)

    角逐第3名时B2>C1>A2

      第4名候选B3,C1     ————(A2,B1,B2,B3,C1)

    角逐第3名时C1>A2>B2

      第4名候选A2,C2,D1    ————(A2,B1,B2,C1,C2,D1)

    角逐第3名时C1>B2>A2

      第4名候选C2,B2,D1    ————(A2,B1,B2,C1,C2,D1)

  可见,如果能够一次比赛获得A2,A3,A4,B1,B2,B3,C1,C2,D1的完整排列次序才能知道前2~4名。很可惜一共是9匹马,不可能一场比赛必然获得结果。那么求最优方案,就是将以上各分支出现最晚出现的马去掉,即C2或D1,进行一场比赛。运气好的话这一轮可以决出前2~4,一共比赛10轮,运气不好的话还需要加赛一轮。而去掉C2或D1能够保证只比赛10轮的概率最大。

 

扩展2:

  25匹马,5个赛道,决出前5。

解答:

  分析和“扩展1”类似,留给读者自己完成。如果想校对答案,可以查阅:http://blog.csdn.net/hackbuteer1/article/details/7481342

 

=============================================================

  1-30~1.34是几个估算题,当年google确实考过其中的题目。不过这里不做解答了。

  关于估算题目的思路和解法,可以参考《编程珠玑》《编程珠玑(续)》和我写的相关文章:[珠玑之椟]估算的应用与Little定律

  另外,“不用再去算一辆校车上可以装多少个高尔夫球了。因为 Google 已承认,那些用于测试求职者的智力题/脑筋急转弯(全世界有多少钢琴调音师?为什么井盖是圆的?),就无法预测出求职者是否会成为一位好员工。 “现在信赖更常规方法去面试潜在员工”。相关链接

1.30

  世界上有多少个钢琴调音师?

1.31

  美国有多少个加油站?

1.32

  曲棍球场上的雪有多重?

1.33

  美国公路一共有多长?

1.34

  平均来看,你翻开一本曼哈顿电话簿时,你需要随机翻开多少次才能找到一个给定的名字?

你可能感兴趣的:(面试题)