如果面试题要求在二维数组(可能具体表现为迷宫或者棋盘等)上搜索路径,那么可以尝试用回溯法。通常回溯法很适合用递归的代码实现。只有当面试官限定不可以用递归实现的时候,可以在考虑用栈来模拟递归的过程。
如果面试题是求某个问题的最优解,并且该问题可以分为多个子问题,那么可以尝试用动态规划。在用自上而下的递归思路去分析动态规划问题的时候,就会发现子问题之间存在重叠的更小的子问题。为了避免不必要的重复计算,可以用自上而下的循环代码来实现,也就是把子问题的最优解先算出来并用数组(一般是一维或者二维数组)保存下来,接下来基于子问题的解计算大问题的解。
如果在告诉面试官动态规划的思路之后,面试官还在提醒说在分解子问题的时候是不是存在某个特殊的选择,如果采用这个特殊的选择将一定得到最优解,那么,通常面试官这样的提示意味着该面试题可能使用于贪婪算法。当然,面试官也会要求应聘者证明贪婪选择的确最终能够得到最优解。
如果我们需要重复地多次计算相同的问题,则通常可以选择用递归或者循环两种不同的方法。递归是在一个函数的内部调用这个函数自身。而循环则是通过设置计算的初始值及终止条件,在一个范围内重复运算。通常基于递归实现的代码比基于循环实现的代码要简洁很多,更加容易实现。在面试的时候,如果面试官没有特别的要求,可以尽量多采用递归的方法编程。
递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数调用自身,而函数调用是由时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。因此递归的效率不如循环。
另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或者多个小问题。如果多个小问题存在相互重叠的部分,就存在重复的计算。
除效率之外,递归还有可能引起更严重的问题:调用栈溢出。在前面的分析中提到需要为每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。
面试题10:斐波那契数列
题目一:求斐波那契数列的第n项。
写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。斐波那契数列的定义如下:
解答:很多C语言教科书在讲述递归函数的时候,都会用斐波那契数列作为例子,因此很多应聘者对这道题的递归解法都很熟悉。他们看到这道题的时候心中会忍不住一阵窃喜,因为他们能很快写出如下代码:
int Fibonacci(int n)
{
if(0 == n)
{
return 0;
}
if(1 == n)
{
return 1;
}
return Fibonacci(n-1) + Fibonacci(n-2);
}
我们的教科书上反复用这个问题来讲解递归函数,并不能说明递归的解法最适合这道题目。面试官会提示我们上述递归的解法有很严重的效率问题并要求我们分析原因。
我们以求解f(10)为例来分析递归的求解过程。想求得f(10),需要先求得f(9)和f(8)。同样,想求得f(9),需要先求得f(8)和f(7)……我们可以用树形结构来表示这种依赖关系,如下图所示。
我们不难发现,在这棵树中有很多节点是重复的,而且重复的节点树会随着n的增大而急剧增加,这意味着计算量会随着n的增大而增大。 事实上,用递归方法计算的时间复杂度是以n的指数的方式递增的。
其实改进的代码并不复杂。上述递归代码之所以慢,是因为重复的计算太多,我们只要想办法避免重复计算就行了。比如我们可以把已经得到的数列中间项保存起来,在下次需要计算的时候我们先查找一下,如果前面已经计算过就不用再重复计算了。
更简单的办法是从下往上计算,首先根据f(0)和f(1)算出f(2),再根据f(1)和f(2)算出f(3)……以此类推就可以算出第n项了。很容易理解,这种思路的时间复杂度是O(n)。实现代码如下:
int Fibonacci(int n)
{
if(0 == n)
{
return 0;
}
int f1 = 0;
int f2 = 1;
int f3 = 1;
for(int i = 2;i < n;i++)
{
f1 = f2;
f2 = f3;
f3 = f1 + f2;
}
return f3;
}
还有一种比这更快的O(logn)算法。这种算法需要用到一个很生僻的数学公式,也很少有面试官会要求掌握。这里就不做介绍了。
题目二:青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解答:首先考虑最简单的情况。如果只有1级台阶,那显然只有一种跳法。如果有2级台阶,那就有两种跳法:一种是分两次跳,每次跳1级;另一种就是一次跳2级。
接着再来讨论一般情况。我们把n级台阶时的跳法看成n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);二是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。因此,n级台阶的不同跳法的总数f(n)=f(n-1)+f(n-1)。分析到这里,我们不难看出这实际上就是斐波那契数列了。
本题扩展:在青蛙跳台阶的问题中,如果把条件改成:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶……它也可以跳上n级,此时该青蛙跳上一个n级的台阶总共有多少种跳法?我们用数学归纳法可以证明f(n)=2^(n-1)。