递归的基本原理

递归的基本原理

2017年03月09日 20:35:52 JimmieZou 阅读数:6592 标签: C-C++递归八皇后n皇后全排列 更多

个人分类: C/C++《算法笔记》学习笔记

版权声明:转载请注明出处(http://blog.csdn.net/daniel960601),蟹蟹~ https://blog.csdn.net/Daniel960601/article/details/60964255

看《算法笔记》到递归了,遇到稍稍复杂一点的递归就会昏头。查阅资料发现大神们推崇《The Little Schemer》和《SCIP计算机程序的构造和解释》这两本书,第一本貌似不太厚,但是只有全英版本,目前正在准备一件比较重要的事情,来不及看这两本超级经典了,有时间了真是一定要看啊。

今天中午午休时拿出了《C Primer Plus》,带着一点点希望,想从这里面找到一点“灵感”,之前也特意看过这本书的递归这一小节,也还是没能达到理想的效果,这次读完有了一些收获,记之。

递归的定义

C/C++允许函数调用其本身,这种调用过程被称为递归(recursion)。C和C++在一点上有不同,那就是C++中不循序main函数递归调用。(《C++ Primer Plus》)。

递归的使用

为了方便说明,我们看下面这个例子。

#include<stdio.h>

void f(int n){
    printf("Level %d : n location %p",n, &n); /* 1 */
    if(n < 4){
        f(n+1);
    }
    printf("Level %d : n location %p",n, &n); /* 2 */
}

int main(){
    f(1);       
    return 0;
}

运行结果:

Level 1 : n location 62fe30
Level 2 : n location 62fe00
Level 3 : n location 62fdd0
Level 4 : n location 62fda0
Level 4 : n location 62fda0
Level 3 : n location 62fdd0
Level 2 : n location 62fe00
Level 1 : n location 62fe30

程序中不仅显示了变量n的值,还输出了n的地址。 
在上面的程序中,main()函数调用f()函数称为“第一级递归”,然后f()函数调用其本身称为“第二级递归”。第二级递归调用第三极递归,以此类推。以上程序共有四级递归。

下面来分析一下程序中递归的具体工作过程: 
输出一共八句,前四句很好理解,后四句的理解体现了递归的根本当某一级递归结束,则该级函数马上将程序的控制权交给该函数的调用函数。 比如说第四级调用,输出了语句#1之后,判断n < 4时是不成立的,所以不再继续递归调用,而是输出语句#2,输完语句#2之后,第四级调用就结束了,此时它就会将控制权交给第三级调用,第三级调用函数中前一个执行过的语句是在if语句中进行第四级调用,因此,当第三级调用获得第四级给它的控制权时,它继续执行后面的代码,即执行打印语句#2,这就输出了第6句话。当第三级调用结束后,第二级调用函数开始继续执行,即输出了第7句话。以此类推。

通过输出语句中n的地址,我们可以得出结论:每一级的递归都是用它自己的私有变量n。关键点在于调用Level1地址和返回时的Level1地址是相同的。

递归的基本原理

第一:每一级的函数调用都有它自己的变量。 
第二:每一次函数调用都会有一次返回,并且是某一级递归返回到调用它的那一级,而不是直接返回到main()函数中的初始调用部分。 
第三:递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。例如在上面的程序中,打印语句#1位于递归调用语句之前,它按照递归调用的顺序被执行了4次,即依次为第一级、第二级、第三级、第四级。 
第四:递归函数中,位于递归调用后的语句的执行顺序和各个被调函数的顺序相反。例如上面程序中,打印语句#2位于递归调用语句之后,其执行顺序依次是:第四级、第三级、第二级、第一级。(递归调用的这种特性在解决涉及到反向顺序的编程问题中很有用,下文会说到) 
第五:虽然每一级递归都有自己的变量,但是函数代码不会复制。 
第六:递归函数中必须包含终止递归的语句。通常递归函数会使用一个if条件语句或其他类似语句一边当函数参数达到某个特定值时结束递归调用,如上面程序的if(n > 4)。

递归和反向计算

这里讨论一下使用递归处理反向问题,这类问题使用递归比使用循环更加方便。通过一个例子来体会:编写一个函数将一个十进制整数准换成二进制。

关于进制准换的一般思路,可以移步这里

上面博客是用循环实现的,用循环实现需要额外的空间来保存转换后的每一位,根据“递归的基本原理”第四条可以得知用递归来求不需要自己申请额外空间,只要我们把转换后的位的输出语句写在递归式之后,就能逆序输出。

用递归实现算法时,有两个因素是至关重要的:递归式递归边界。 
在这个例子中,根据上面给出的链接方法,可以很容易的得出递归式:to_binary(n/2)。 
那么递归边界是多少呢?是n<2。因为当n>=2时,就需要一位二进制位来表示。所以只有被2除的结果小于2时才停止计算。每次除以二就可以得到一位,直到得到最后一位为止。

#include<stdio.h>

void to_binary(int n){
    int r = n%2;
    if(n >= 2){
        to_binary(n/2);
    }
    printf("%d",r);
    return ;

}

int main(){

    int n;
    while(scanf("%d",&n) != EOF){
        to_binary(n);
        printf("\n");
    }

    return 0;
}

若想跟踪(大多数时候的比较不简单)递归,一定要时刻记住这一点:当某一级递归结束,则该级函数马上将程序的控制权交给该函数的调用函数。

下面再记录两个例子,加深理解。

例子

1. 求1~n的全排列

求1~n的全排列,我们可以看成求: 
以 1 开头的全排列 
以 2 开头的全排列 
以 3 开头的全排列 
…… 
以 n 开头的全排列

显然需要遍历1~n的每一位,不放设数组p[]用来保存当前排列,设一个散列数组hashTable[],hashTable[x]==true表示x已经在p中。

下面对于p的第一位需要填入1~n的所有数字,并且 已经在p的第一位确定的情况下,填剩下的n-1位。填每一位时都需要遍历1~n这些数字在不在已经填好的位置上,即加入现在需要填入第index位,那么就需要查看填入的x是不是在1~index-1这些位置上已经放置(即hashTable[x]==false),如果没有放置,则可将x放到index位置;若已经放置,则需要看x的下一位。并且当处理完index位之后(代表这一级的递归完毕,需要返回),需要将填入的位恢复为false,以便下一级或返回后的上一级填数字。

#include<stdio.h>

bool hashTable[100] = {false};

//处理当前序列的第index位
void gP(int p[], int n, int index){
//  printf("Level %d : index at %p\n",index,&index);
    if(index == n+1){ //到了递归边界,此时已经一个序列 
        for(int i = 1; i <= n; i++){ //输出该序列 
            printf("%d ",p[i]);
        } 
        printf("\n");
        return ;
    }

    //没到递归边界,处理1~n位,每一位都需要枚举到1~n 
    for(int x = 1; x <= n; x++){ //p的第一位以及p的每一位都需要枚举1~n的所有数(强调第一位是为了便于理解算法思想) 
        if(hashTable[x] == false){ //x没被使用 
            p[index] = x; //则将x放到当前序列index位置上 
            hashTable[x] = true; //标记x已经被使用
            gP(p,n,index+1); //处理当前序列的下一位置
            hashTable[x] = false; //处理完当前序列,需要将相应的位置为未被使用 
        }
    } 
} 

int main(){

    int n;
    scanf("%d",&n);
    int p[100];

    gP(p,n,1);

    return 0;
}

由于gP()的第1,2个参数固定,将gP(p,n,1)的调用简化记做gp(1),同理gP(p,n,m)记做gp(m)。 
下面对n=3时的递归过程的一部分做一个简单分析: 
第一级:gP(1):p[1] = 1, x = 1,hashTable[1]=true,之后调用第二级(gp(2)) 
第二级:gp(2):p[2] = 2(不能赋1,因为hashTable[1]==true),x = 2,之后调用第三级(gp(3)) 
第三级:gp(3):p[3] = 3(不能为1,2,理由同上),x = 3,之后调用第四级(gp(4)) 
第四级:gp(4),满足if条件(index == 3+1),所以得到了一个排列,将其输出(输出1 2 3),之后第四级返回 
第四级返回:第三级接着执行其后续代码,即hashTable[3]=false,x++后不满足for循环的x<=n的条件,故第三级执行完毕,返回 
第三级返回:第二级接着执行其后续代码,即hashTable[2]=false,x++后得到x=3,那么可以接着执行for循环,则p[2] = 3,hashTable[3] = true, 此时又调用gp(3),这时的层级升了一个跨度,但是分析方法不变,就不再继续,相信读者已经明白。此次达到递归边界时应该输出的序列是1 3 2。

2. 八皇后问题

再记录一下八皇后这个经典的问题,如果不了解什么是八皇后问题。 
八皇后问题是指在8*8的棋盘上放置8个皇后,要使得这8个皇后不在同一行、不在同一列、不在同一对角线上,求合理的方案数目。 这个问题还可以拓展为n皇后问题,即把上面描述中的8都换成n即是n皇后问题的描述。

很容易想到的一种方法是用组合数枚举,则有C(n, n*n)的枚举量,当n=8时就是54 505 232次枚举,对于较大一些的n时无法承受这样的枚举次数的。

那么需要换个思路:由于每行和每列都只能放置一个皇后,那么如果把n列皇后所在的行号依次写出,就是一个1~n的排列,这就可以用上面的全排列的思想了。 不过只是将上面的输出全排列换成校验这种放置方案是否合理即可。

#include<stdio.h>
#include<math.h>

int count = 0; //记录合法的放置方法的数目 
bool hashTable[100] = {false};

void gP(int p[], int n, int index){
    if(index == n+1){
        bool flag = true; //flag=true表示是一个合法方案 
        for(int i = 1; i < n; i++){
            for(int j = i+1; j <= n; j++){
                if(abs(i-j) == abs(p[i]-p[j])){ //判断两个皇后是不是在同一对角线上 
                    flag = false; //不是合法方案 
                }
            }
        }
        if(flag){ //是合法方案 
            count++; //合法方案数目+1 
        }
        return ;
    }
    for(int x = 1; x <= n; x++){
        if(hashTable[x] == false){
            p[index] = x;
            hashTable[x] = true;
            gP(p,n,index+1);
            hashTable[x] = false;
        }
    }
}

int main(){

    int n;
    scanf("%d",&n);

    int p[100];
    gP(p,n,1);

    printf("%d\n",count);

    return 0;
}

以上代码注释较少,可以参见“全排列”代码理解。

这种方法枚举了所有的情况,然后判断每一种情况是否合理,这中做法是非常朴素的,不适用优化算法,直接使用朴素法来解决问题的做法称为暴力法

事实上,通过思考可以发现,当已经放置了一部分皇后后(对应于生成了一个排列的一部分),已经出现了不合法的放置位置(已经有皇后在同一对角线上),剩下的皇后无论如何放置,这种放置方案都是不合法的,没有必要在往下递归了,直接返回上层即可。如下图,只要1,3两个放置完毕,就出现了不合法。 


5皇后的不合法放置 

一般来说,如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层。一般把这种方法称为回溯法

下面的代码是采用了回溯法的代码,可以体会一下它与上面代码的区别。

#include<stdio.h>
#include<math.h>

int count = 0; //记录合法的放置方法的数目 
bool hashTable[100] = {false};

void gP(int p[], int n, int index){
    if(index == n+1){
        count++;
        return ;
    }
    for(int x = 1; x <= n; x++){
        if(hashTable[x] == false){

            bool flag = true; 
            for(int i = 1; i < index; i++){ //遍历之前的皇后和x皇后的位置关系 
                if(abs(i-index) == abs(p[i]-x)){ //x和之前的皇后i在同一对角线上 
                    flag = false; //不合理
                    break; 
                }
            }

            if(flag){ //x位置合理,可以放置 
                p[index] = x;
                hashTable[x] = true;
                gP(p,n,index+1);
                hashTable[x] = false;
            }
        }
    }
}

int main(){

    int n;
    scanf("%d",&n);

    int p[100];
    gP(p,n,1);

    printf("%d\n",count);

    return 0;
}

猜你喜欢

转载自blog.csdn.net/u010412301/article/details/86539230
今日推荐