数据结构之递归(Recursion)------分而治之

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shentanweilan9/article/details/54016343

1.递归的定义
在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。
直接递归:函数调用自身。
间接递归:过程或函数p调用过程或函数q,而q又调用p。
尾递归:一个递归过程或递归函数中递归调用语句是最后一条执行语句。尾递归只是一个变形的循环,可以很容易用循环来代替。在含有循环结构的语言中,不推荐使用尾部递归。

例如阶乘函数的定义: 在该函数fun(n)求解过程中,直接调用fun(n-1)(语句4)自身,所以它是一个直接递归函数。又由于递归调用是最后一条语句,所以它又属于尾递归。

    public int fun(int n) {          
        if (n == 1) return 1;
        else return fun(n - 1) * n;         
    }
  public int fun(int n) {
        int result = 1;
        for (int i = 1; i <= n; i++)
            result *= i;
        return result;
    }

2.递归是如何实现的
2.1递归调用不是表面上的函数自身调用,而是一个函数的实例调用同一个函数的另一个实例(是两个不同的实例)
2.2函数调用的过程中处理要素包括:函数控制权的转接工作(利用函数入口地址和返回地址),局部变量的空间分配工作,实参和形参的传递,函数的返回结果传递。

3.何时使用递归
1. 定义是递归的(数学公式、数列等的定义)
2. 数据结构是递归的 (单链表就是一种递归数据结构 sum函数)
3. 问题的求解方法是递归的
eg:1.有三根杆子A,B,C。A杆上有若干碟子
   2.每次移动一块碟子,小的只能叠在大的上面
   3.把所有碟子从A杆全部移到C杆上
解:题中只给了三座塔,我们利用C塔将圆盘堆在B塔。首先将A塔的1号圆盘放在B塔,A塔的2号圆盘放在C塔,再把放在B塔的1号圆盘放在C塔,此时C塔拥有两个圆盘按要求自下而上从小到大排列。接下来将A塔的3号圆盘放在B塔,将C塔的1号圆盘放在B塔,把C塔德2号圆盘放在A塔,再把B塔的1号圆盘放在A塔,此时C塔空,1号2号按要求排在A塔,B塔只有3号圆盘。此时把B塔3号圆盘放在C塔,把A塔德1号放在B塔吗,把A塔德2号房在C塔,再把B塔德1号放在C塔,此时B塔空,C塔按要求排有123号圆盘。这次把A塔的4号圆盘放在B塔,这次就比较麻烦了先把C塔的1号放在A塔,C塔的2号房在B塔,再把A塔德1号放在B塔,把C塔德3号放在A塔,再把B塔的1号放在C塔,把B塔德2号放在A塔,再把C塔德1号放在A塔,此时C塔空,B塔只有4号圆盘,A塔按要求房有123到N号圆盘,缺4号圆盘。现在把B塔的4号圆盘房在C塔,现在推回去,把A塔德1号房在C塔,A塔的2号房在B塔,再把C塔的1号放在B塔,把A塔德3号房再C塔,此时刚好是3号压4号于C塔,再把,B塔的1号房在A塔,把C塔的2号放在C塔,把A塔的1号放在C塔,这下刚好推回来,此时B塔空,A塔最上面是5号圆盘,C塔按要求放有1234号圆盘。 按这样的递推方法,将n-1个圆盘按要求放在C塔,第n个圆盘放在B塔,现在A塔空。n号圆盘是最大的圆盘,按问题要求我们终于把n号最大的圆盘放在了B塔,这下借助已空的A塔联合BC塔推回来,就可以把n个圆盘按要求放在B塔。

public class Recursion {

    public void move(int n, char a, char b, char c) {
        if (n == 1)
            System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
        else {
            move(n - 1, a, c, b);
            System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
            move(n - 1, b, a, c);
        }
    }

    public static void main(String[] args) {
        new Recursion().move(5,'A','B','C');
    }
}

eg:八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。———以上节选自百度百科。

解:从第一行第一列开始逐行摆放皇后依题意每行只能有一个皇后,遂逐行摆放,每行一个皇后即可摆放后立即调用一个验证函数(传递整个棋盘的数据),验证合理性,安全则摆放下一个,不安全则尝试摆放这一行的下一个位置,直至摆到棋盘边当这一行所有位置都无法保证皇后安全时,需要回退到上一行,清除上一行的摆放记录,并且在上一行尝试摆放下一位置的皇后(回溯算法的核心)当摆放到最后一行,并且调用验证函数确定安全后,累积数自增1,表示有一个解成功算出,验证函数中,需要扫描当前摆放皇后的左上,中上,右上方向是否有其他皇后,有的话存在危险,没有则表示安全,并不需要考虑当前位置棋盘下方的安全性,因为下面的皇后还没有摆放。

public class Recursion {

    private int[][] arry = new int[8][8];    //棋盘,放皇后
    private int map = 0;     //存储方案结果

    public void find(int i) {    //寻找皇后节点
        if (i > 7) {    //八皇后解
            map++;
            print();
            return;

        }

        for (int m = 0; m < 8; m++) {       //深度优先,递归算法
            if (rule(arry, i, m)) {
                arry[i][m] = 1;
                find(i + 1);
                arry[i][m] = 0;
            }
        }

    }

    public boolean rule(int arry[][], int k, int j) {    //判断节点是否合适
        for (int i = 0; i < 8; i++) {       //行列冲突
            if (arry[i][j] == 1)
                return false;
        }
        for (int i = k - 1, m = j - 1; i >= 0 && m >= 0; i--, m--) {    //左对角线
            if (arry[i][m] == 1)
                return false;
        }
        for (int i = k - 1, m = j + 1; i >= 0 && m <= 7; i--, m++) {    //右对角线
            if (arry[i][m] == 1)
                return false;
        }
        return true;
    }

    public void print() {      //打印方法结果
        System.out.print("方案" + map + ":");
        for (int i = 0; i < 8; i++) {
            for (int m = 0; m < 8; m++) {
                if (arry[i][m] == 1) {
                    System.out.print("皇后" + (i + 1) + "在第" + i + "行,第" + m + "列\t");

                }
            }
        }
        System.out.println();
    }

    public static void main(String[] args) {
//        new Recursion().move(5, 'A', 'B', 'C');
        new Recursion().find(0);
    }

}

eg:斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……在数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。

public class Fibonacci {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input this fibonacci n:");
        int n = scanner.nextInt(); // 假设输入为大于零的整数  
        System.out.println(fibonacci(6) + ":" + fibonacciNormal(6));
        int sum = 0;
        for(int i = 1; i <= n; i++){
            sum += fibonacci(i);
        }
        System.out.println(sum);
    }

    // 递归实现方式  
    public static int fibonacci(int n){
        if(n <= 2){
            return 1;
        }else{
            return fibonacci(n-1) + fibonacci(n-2);
        }
    }

    // 递推实现方式  
    public static int fibonacciNormal(int n){
        if(n <= 2){
            return 1;
        }
        int n1 = 1, n2 = 1, sn = 0;
        for(int i = 0; i < n - 2; i ++){
            sn = n1 + n2;
            n1 = n2;
            n2 = sn;
        }
        return sn;
    }
}

4.递归算法的设计 之 分而治之
递归的求解的过程均有这样的特征:先将整个问题划分为若干个子问题,通过分别求解子问题,最后获得整个问题的解。而这些子问题具有与原问题相同的求解方法,于是可以再将它们划分成若干个子问题,分别求解,如此反复进行,直到不能再划分成子问题,或已经可以求解为止。这种自上而下将问题分解、求解,再自上而下引用、合并,求出最后解答的过程称为递归求解过程。这是一种分而治之的算法设计方法。
递归设计的步骤如下:
(1)对原问题f(s)进行分析,假设出合理的“较小问题”f(s’)(与数学归纳法中假设n=k-1时等式成立相似);
(2)假设f(s’)是可解的,在此基础上确定f(s)的解,即给出f(s)与f(s’)之间的关系(与数学归纳法中求证n=k时等式成立的过程相似);
(3)确定一个特定情况(如f(1)或f(0))的解,由此作为递归出口(与数学归纳法中求证n=1时等式成立相似)。

5.递归算法到非递归算法的转换
递归算法的时间效率通常比较差。因此,对求解某些问题时,我们希望用递归算法分析问题,用非递归算法具体求解问题。这就需要把递归算法转换为非递归算法。
把递归算法转化为非递归算法有如下三种基本方法:
(1)对于尾递归和单向递归的算法,可用循环结构的算法替代。
(2)自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
(3)利用栈保存参数,由于栈的后进先出特性吻合递归算法的执行过程,因而可以用非递归算法替代递归算法。

第(1)种和第(2)种情况的递归算法转化为非递归算法的问题,前者是一种是直接转化法,不需要使用栈,后者是间接转化法,需要使用栈。第(3)种情况也需要使用栈,但因具体情况而异,

尾递归是递归调用语句只有一个,而且是处于算法的最后。 这个特别容易转换。典型的例子是 求阶乘算法。
单向递归是指递归函数中虽然有一处以上的递归调用语句,但各次递归调用语句的参数只和主调用函数有关,相互之间参数无关,并且这些递归调用语句,也与尾递归一样处于算法的最后。单向递归的典例就是Fibonacci数列。

一般对于尾递归和单向递归可采用循环消除。(原因是,递归返回时,正好是算法的末尾,相当于保存的返回信息和返回值根本不需要被保存)采用循环结构消除递归没有通用的转换算法,对于具体问题要深入分析对应的递归结构,设计有效的循环语句进行递归到非递归的转换。
模拟系统运行时的栈消除递归(从现在开始培养解决问题运用递归的分而治之的思维并培养消除递归的能力) 对于不属于尾递归和单向递归的递归算法,很难转化为与之等价的循环算法。但所有的递归程序都可以转化为与之等价的非递归程序。
直接使用栈保存中间结果,从而将递归算法转化为非递归。

在设计栈时,除了保存递归函数的参数外,还增加了一个标志成员(tag),对于某个递归小问题f(s`),其值为1表示对应的递归问题尚未求出,需进一步分解转换,值为0表示对应递归问题已求出,需通过该结果求解大问题f(s).
直接使用栈保存中间结果,从而将递归算法转化为非递归。

在设计栈时,除了保存递归函数的参数外,还增加了一个标志成员(tag),对于某个递归小问题f(s`),其值为1表示对应的递归问题尚未求出,需进一步分解转换,值为0表示对应递归问题已求出,需通过该结果求解大问题f(s).

直接使用栈保存中间结果,从而将递归算法转化为非递归。

在设计栈时,除了保存递归函数的参数外,还增加了一个标志成员(tag),对于某个递归小问题f(s`),其值为1表示对应的递归问题尚未求出,需进一步分解转换,值为0表示对应递归问题已求出,需通过该结果求解大问题f(s).

为方便讨论,将递归模型分为等值关系和等价关系两种。
等值关系:“大问题”的函数值等于“小问题”的函数值的某种运算结果,例如求n!对应的递归模型就是等值关系。
对于“等值关系”通用解决方案:
1、设计压栈数据元素
2、首先将初始压栈,tag初始为0(还可跟据需要,加入返回值等),如果有按条件返回的多个分支递归,可设置一个trans标记,然后压栈的时,转到对应的分支
3、当栈不为空时,循环(每次最栈顶元素判断是压栈过程还是解栈过程)
压栈存储参数,回归时期解决具体问题。

等价关系:
等价关系是指“大问题”的求解过程转化为“小问题”求解而得到的,它们之间不是值的相等关系,而是解的等价关系 。 例如,求梵塔问题对应的递归模型就是等价关系,也就是说,Hanoi(n,x,y,z)与Hanoi(n-1,x,z,y)、move(n,x,z)和Hanoi(n-1,y,x,z)是等价的。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页