递归学习笔记1

转发原帖

https://blog.csdn.net/caogenwangbaoqiang/article/details/80220019

转发内容

递归问题一般可以分为两类,一类是过程递归,就是说执行的过程有明显的递归性,典型的就是求阶乘,斐波拉契数列,矩阵染色。。。大多数问题可以归结为第一类;第二类是结构递归,比如二叉树的各序遍历,链表逆转,反向打印等问题。两类问题的设计考虑是有所不同的,如果采用同样的思路去考虑两类不同问题,一定得不到正确的代码。建议从过程递归设计开始学习。

一、过程递归性

不管是过程递归还是结构递归首先要明确的就是一定要抛弃程序设计的细节,如果在设计过程中扣住细节,试图弄清楚每一步执行过程,你就失败了。递归设计的设计者首先要明确的是你的递归函数的功能,比如阶乘int fun(int n),他的功能就是返回n的阶乘,设计过程中要时时记住自己递归函数的设计目的。其次就是递归程序的出口设计,这一点是比较灵活的,不同问题有不同的设计;最后就是一定要有规模的递减。在整个递归设计过程中,一定要严格注意和把握这几点,缺一不可。

案例1:阶乘

阶乘基本就是递归入门级案例,现在将用上面的思路来设计。

1、设计出函数原型,明确其功能;

//设计n的阶乘递归程序
int fuc(int n)//明确函数功能,返回n的阶乘结果
{
    //设计递归出口,出口明显根据n的变化来确定
    //而0!=1;1!=1,就可以用0或者1来结束递归
    if(n==0 || n==1)
        return 1;
    //注意不要做任何的细节处理,明确函数功能
    //函数功能是返回n的阶乘,那么直接return fuc(n)是不可以的,
    //因为没有实现有规模的递减
    //修改如下则做到了规模递减
    return n*fuc(n-1);
}

2、矩阵染色

上面的案例其实我们并没有明显的感觉到过程的递归性,但这个下面的案例我们可以感觉到过程是有明显的递归性的。这个案例是小米2016招聘的一个笔试题,同时在2016中兴捧月比赛初赛中出现了一个极其类似的题。题目描述如下:对于一个矩阵,例如:

                                                              0 1 1 0

                                                              2 1 2 1

                                                              1 1 2 1

                                                              0 1 1 0

这个矩阵代表了一个图像,现在要给这个图像的指定位置染色,函数原型如下void fillwithcolor(int i,int j,int c),表示在图像的i,j位置和i,j位置临近的同色区域染色为c,注意:对角元素不算临近,对于上图如果调用fillwithcolor(1,1,5)的话,将得到下面的矩阵。

                                                              0 5 5 0

                                                              2 5 2 1  

                                                              5 5 2 1

                                                              0 5 5 0

由于 对角不算临近,所以第二排的最后一个1没有被染色。

对于这个问题,函数原型已经给出,void fillwithcolor(int i,int j,int c)把i,j位置以及相邻近位置染色为c,我们试想,由于不考虑对角的位置,所以我们只需要考虑上下左右的位置,对于满足要求的上下左右的位置是不是成为了新的i,j位置呢。只需要在新的位置调用函数即可.

//设计一个区域染色程序
//函数的功能是给i,j位置及其临近位置染色c,m,n表示矩阵的行数和列数
void fillwithcolor(int **map_,int i,int j,int c,int m,int n)
{
    //出口设计:不让j和j跑到矩阵外面
    if(i>m-1 || j>n-1)
        return;
    int temp = map_[i][j]; //先保存初始色
    map_[i][j] = c; //染色为c
    //考虑往上走的情况
    if(i-1 >=0)//不能走到图片外面去
    {
        if(map_[i-1][j] == temp)
            fillwithcolor(map_,i-1,j,c,m,n);
    }
    //考虑往下走的情况
    if(i+1 <= m-1)
    {
        if(map_[i+1][j] == temp)
            fillwithcolor(map_,i+1,j,c,m,n);
    }
    //考虑往左走的情况
    if(j-1 >=0)
    {
        if(map_[i][j-1] == temp)
            fillwithcolor(map_,i,j-1,c,m,n);
    }
    //考虑向右走的情况
    if(j+1 <= n-1)
    {
        if(map_[i][j+1] == temp)
            fillwithcolor(map_,i,j+1,c,m,n);
    }
}

这个问题的过程是哟明显的递归性的,依次用同样的方法处理其上下左右的位置。

3、非波拉契数列

斐波拉契数列在这里不做介绍。

首先同样明确函数的目的

//设计斐波拉契数列函数
int fei(int n)//返回第n个斐波拉契数列数值
{
    //出口函数,斐波拉契数列第1个数列和第2个数列都为1
    if(n == 1|| n == 2)
        return 1;
    //同样做到规模有减小,不考虑任何细节,明确 递归函数的目的
    return fei(n-1)+fei(n-2);
}

4、全排列问题

全排列递归程序设计是一个很好的理解递归设计的例子,有一定的难度。但是是很典型的递归程序设计案例,其过程有明显的过程递归性,下面将用上面的步骤来设计。

1、明确函数功能

我们假定传入的是一个数组,长度为n,递归函数初步设计成这样void permutation(int * arr,int n),然后我们要明确的是全排列的具体递归过程,例如我们考虑1 2 3的全排列。

首先将1固定在排列首,然后求2 3的全排列,然后将2固定在排列首,求13的全排列,最后将3固定在排列首,求12的全排列。这是第一层递归,将1固定在排列首时候,对于2,3两个数构成的全全排列依然要重复上面的过程,即把2固定在首和把3固定在首,明显是一个递归的过程。如果我们所求的数组较长,加入1,2, 3,.....,n个数,我们就会依次求取2~n的排列,3~n的全排列,i~n的全排列,所以我们在设计函数的时候,需要加上一个参数m,代表m到n的全排列,所以函数的定义就如下void permutation(int *arr,int m,int n)函数的功能是求取数组第m个数到第n个数的全排列。

2、由于是求全排列,所以不建议用一个二维数组去保存所有的排列,打印出所有的全排列即可,为了便于理解,我们可以先不考虑出口,先来写代码。

//输出一个数组的全排列情况
void permutation(int *arr, int m, int n)
{
    //出口设计:当m到了n-1的位置时候,表明以某个数为首的全排列计算完成
    //即可打印出来
    if(m == n-1){
        for (int j = 0;j < n;j++)
            cout << arr[j]<<" ";
        cout << endl;
    }

    //功能顺序
    for(int i=m; i<n; i++){
        //m从0开始,表示第一个位置上的数,依次完成和m之后的数交换
        swap(arr[i], arr[m]);
        //求除了第1,2,3.。,n个之后的数的全排列,m+1也存在规模的递减
        permutation(arr, m+1, n);
        //需要换回来
        swap(arr[i], arr[m]);
    }
}

对于过程递归来说,在函数的设计过程中只要觉得这个位置所需要实现的功能就是这个函数的功能,就可以理所当然的递归调用自己。不要考虑任何的细节,满足条件即可。

二,结构上具有递归性

递归还有一种设计思路是按照本身结构上具有递归性,用上面的思路解释不通。例如逆向打印,链表逆转,二叉树遍历等,很多带有逆向操作的都可以递归,为什么呢?因为递归是按层执行,每一层函数调用产生的变量,结果等都会压入函数栈中,知道遇到出口,才依次弹栈,所以我们利用其弹栈的特性,在逐渐弹的过程中,去执行我们需要的代码,就可以实现逆向效果。

 

案例1:用递归逆向打印一个数组

逆向打印数组其实简单的不要不要的了,但是考虑过用递归去打印吗?逆向打印我们在过程上完全感觉不到存在什么递归性,但是由于这种线性结构,使得其具有递归结构性,所以也是可以逆向打印的。

递归逆向打印的设计思路就是让递归一直下去,知道达到最后的位置,然后设定为出口条件,此时就会依次弹出,在弹出的位置依次打印每一层的值。

和过程递归相同的是同样要时刻明确自己函数的功能。

//逆向打印数组
void reprint(int *arr,int i,int n)//用i描述当前位置
{
    //出口设计:当i增加到n-1时,表示已经到了数组最后一个元素
    if(i == n-1){
        return;
    }
    /*这里在不停的递归,知道当i == n的时候就开始弹他会弹到上一层调用的地方,并且开始执行调用函数下面的语句*/
    reprint(arr, i+1, n);
    /*弹出后就会到这个位置来开始执行下面的语句,所以我们只需要在这里打印即可*/
    cout << arr[i];
}

案例2:单向链表逆转

单向链表的逆转也是用递归来实现了,有了上面的案例一,是否会有些启发呢。单向链表只能从后面开始逆转,从前面开始逆转的话,由于没有前指针,所以会断掉,只能从后开始逆转。所以可以考虑用递归先将节点位置定位到最后,然后利用其弹栈的过程实现逆转。

//单向链表逆转
void reverseList(ListNode * node)
{
    //出口设计:到最后的时候node肯定为空,就相当于遇到了出口
    if(node && node->next){
        //规模缩小,直到扫描到最后一个链表
        reverseList(node->next);
        //反转操作
        node->next->next = node;
    }
}

案例3:二叉树中序遍历

//二叉树中序遍历
void MTravel(List BT)
{
    //出口设计:当当前结点为空,就是出口
    if(BT){
        MTravel(BT->Left);
        cout << BT->Data;
        MTravel(BT->Right);
    }
}

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值