递归与回溯算法

来源:互联网 发布:天天向上网络作家专场 编辑:程序博客网 时间:2024/05/01 23:18

长久以来一直对递归一知半解。最近看面试方面的书,在数据结构中有很多算法是和递归有关的,另外一些经典问题:打靶问题、8皇后问题、0-1背包问题也和递归有关,在有些算法中递归甚至是必不可少的,因此很有必要把递归算法再重新学一下。

      首先说一下递归和迭代的区别。迭代是反复的意思,有时候也指重复执行和反复执行。迭代一般是通过循环执行一组指令,并且在每次执行后都会从变量的原值推出它的新值。迭代往往需要一个迭代表达式,如f(n+1)=f(n)+f(n-1)。大家可以看出递归中的经典案例N!也可以通过迭代来做,而且更高效(因为迭代相比递归一般要高效很多)。

      下面就来谈递归。递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。文[1]中对于递归讲解比较细致,大家可以参考。下面展示两段代码[1]:

返回一个二叉树的深度:

01int depth(Tree t){
02      if(!t) return 0;
03 
04    else {
05 
06        int a=depth(t.right);
07 
08        int b=depth(t.left);
09 
10        return (a>b)?(a+1):(b+1);
11 
12    }
13 
14}

判断一个二叉树是否平衡:

01int isB(Tree t){
02      if(!t) return 0;
03 
04    int left=isB(t.left);
05 
06    int right=isB(t.right);
07 
08    if( left >=0 && right >=0 && left - right <= 1 || left -right >=-1)
09 
10        return (left<right)? (right +1) : (left + 1);
11 
12    else return -1;
13 
14}

    第一个算法还是比较好理解的,但第二个就不那么好理解了。第一个算法的思想是:如果这个树是空,则返回0;否则先求左边树的深度,再求右边数的深度,然后对这两个值进行比较哪个大就取哪个值+1。而第二个算法,首先应该明白isB函数的功能,它对于空树返回0,对于平衡树返回树的深度,对于不平衡树返回-1。明白了函数的功能再看代码就明白多了,只要有一个函数返回了-1,则整个函数就会返回-1。(具体过程只要认真看下就明白了)

    对于递归,最好的理解方式便是从函数的功能意义的层面来理解。了解一个问题如何被分解为它的子问题,这样对于递归函数代码也就理解了。这里有一个误区(我也曾深陷其中),就是通过分析堆栈,分析一个一个函数的调用过程、输出结果来分析递归的算法。这是十分要不得的,这样只会把自己弄晕,其实递归本质上也是函数的调用,调用的函数是自己或者不是自己其实没什么区别。在函数调用时总会把一些临时信息保存到堆栈,堆栈只是为了函数能正确的返回,仅此而已。我们只要知道递归会导致大量的函数调用,大量的堆栈操作就可以了。

    下面介绍和递归密切相关的算法:回溯算法。回溯算法是一种搜索算法,它可以通过对问题从一个状态出发,搜索从该状态出发的所有状态,当一条路走到尽头时,再后退一步继续搜索,直到所有路径都试探过。回溯算法通常由递归来实现,因为递归结束时会返回调用处(上一步),回溯结束时也是返回上一步,两者可以很好的契合。文[2]中对于回溯的讲解挺详细的,大家可以参考下。这里引用文中的一段话,我觉得写的挺好的:“通过把递归结束的条件设置到搜索的最后一步,就可以借用递归的特性来回溯了。因为合法的递归调用总是要回到它的上一层调用的,那么在回溯搜索中,回到上一层调用就是回到了前一个步骤。当在当前步骤找不到一种符合条件情况时,那么后续情况也就不用考虑了,所以就让递归调用返回上一层(也就是回到前一个步骤)找一个以前没有尝试过的情况继续进行。当然有时候为了能够正常的进行继续搜索,需要恢复以前的调用环境。”。下面通过一个例子来介绍递归回溯的实现

楼梯问题:假设一个楼梯共有10级台阶,一个人可以选择一次走1级或2级台阶,请问总共有多少种不同走法?

    这个问题是之前一个师兄跟我说的,当时考虑了好久也不得其解,现在通过递归回溯算法来解决它。具体代码如下:

01#include <iostream>
02 
03using namespace std;
04 
05const int STAIRS=10;
06  const int MAX_STEP=2;
07 
08int result_number=1;
09  int steps[STAIRS];
10 
11void print_result(){
12      cout<<"This is the "<<result_number++<<" result: "<<endl;
13 
14    for(int i=0;i<STAIRS;i++){
15 
16        if(steps[i]==0){
17 
18            break;
19 
20        }
21 
22        cout<<steps[i]<<" ";
23 
24    }
25 
26    cout<<endl;
27 
28}
29 
30void do_step(int stairs,int step_number){
31      if(stairs==1){
32 
33        steps[step_number]=1;
34 
35        print_result();
36 
37        steps[step_number]=0;
38 
39        steps[step_number-1]=0;
40 
41        return;
42 
43    }
44 
45    else if(stairs==0){
46 
47        print_result();
48 
49        steps[step_number-1]=0;
50 
51        return;
52 
53    }
54 
55    else{
56 
57        for(int i=1;i<=MAX_STEP;i++){
58 
59                  if(stairs-i<0){
60 
61            break;
62 
63            }
64 
65            steps[step_number]=i;
66 
67            do_step(stairs-i,step_number+1);
68 
69        }
70 
71     }
72 
73}
74 
75int main(){
76      cout<<"the program started..."<<endl;
77 
78    do_step(STAIRS,0);
79 
80    cout<<"there are "<<result_number-1<<" results."<<endl;
81 
82}

     do_step函数的功能是:当执行时如果参数stairs为1,或0,则直接输出结果,并且把steps[]恢复到上一步的状态,退出函数。如果stairs大于1,则尝试分别走1步和2步,此后问题转化为原问题的子问题。明白了函数的功能就很容易看懂代码了。该函数中出现了for循环嵌套 递归的情况,大家只要认为是多次执行其中的代码就可以了(那个我们不理解的复杂的函数堆栈调用关系可以正确无误的实现我们要求的函数调用,呵呵)。程序会列出每个结果,总的可行走法共89种。

    大家了解了回溯算法后就可以解决一大类问题了,包括上面说的打靶问题、8皇后问题等。大家也可以尝试通过回溯算法来解决“数独”问题,通过搜索来查找可行的数独解(也算是个小小的挑战吧,^_^)。 

参考资料:

[1] 漫谈递归思想 http://www.cnblogs.com/BLoodMaster/archive/2010/03/23/1692641.html

[2] 递归回溯总结 http://hi.baidu.com/sulipol/blog/item/32411851bb9724551138c2eb.html

[3] 《程序员面试宝典》第8章 循环、递归与概率

原创粉丝点击