程序设计:递归简论

        一般程序设计有三大结构——顺序、选择和循环,循环结构是程序结构中最重要的组成部分,没有它就难以实现程序的自动化,只能在有限的若干选择和执行后就终止了。递归可以看作一种循环,其底层是利用栈的实现,因此比一般的迭代循环(for循环、while循环和do-while循环)多花费一定的空间成本,但是有时候写递归函数能让程序的逻辑更加简明。

         例如下面是两个计算阶乘的方法,一个使用循环语句迭代,另一个是使用递归,而递归的代码较为简明些。

介绍

public static int fac(int n){
        if(n<=1)return 1;
        return n*fac(n-1);
}
public static int fac2(int n){
        int res=1;
        if(n==0)return 1;
        for(int i=1;i<=n;i++){
            res*=i;
        }
        return res;
}

        这两个代码都是正确的,都是为了算出非负整数n的阶乘数,在第二段代码中,阶乘计算的具体流程已被写得一清二楚,每次都和自动增加的i相乘,最终共乘了1,2,3,...,n,于是res便等于了1*2*3*...*n。对于第一段代码,为了计算n的阶乘,需要计算n-1的阶乘,然后再用n乘以n-1的阶乘就是n的阶乘了。而计算n-1的阶乘有是与前面同样的方法,即递归调用了方法本身。当n<=1,时,n的阶乘就是1,这是其中一行代码。如果假设所编写的方法fac(int n)能够实现输入一个非负对整数n返回一个n地的阶乘的功能,那么,fac(n-1)就被正确地计算了出来,并乘以n,最终n的阶乘被正确地计算出来了。上面的这段描述类似于数学归纳法的证明过程。实际上,设计递归代码时就应当这样考虑。

        另外,递归的过程是一个递推的过程,它将原问题的规模减小一分,去解决规模较为小的子问题,而子问题的性质可能和原问题的一样,并且建立了当前问题的解与子问题之间的关系,这种关系就是递推关系,可以用递推表达式来表示。例如上面的求n的阶乘的递归例程,原问题是要求出n的阶乘fac(n),递归调用实际上是求解规模较小的子问题——n-1的阶乘,它们之间的关系是fac(n)=n*fac(n-1),这是一个递推关系,找到递推关系是设计递归代码的关键。

        并不是所有递归算法都比普通的迭代循环高效,有些时候会存在一些及其低效的递归实现,在这方面,似乎计算斐波那契数比较经典,于是我们来看看。

         我们定义一列斐波那契数是这样的:它的第一项和第二项都是1,从第三项开始,每一项的值都是其前两项的和,如果第一项从0开始数起,那么会有如下的递推关系式:

fib(n)=\left\{\begin{matrix}1 &, n=0,1\\ fib(n-1)&,n>1 \end{matrix}\right.

显然这样的递推关系式是已知的,但有时候递推关系式并不是明显的,我们需要自己找出正确的递推关系式。按照上述的 关系式,我们写出了如下递推代码。

public static int fib(int n){
        if(n<=1)return 1;
        return fib(n-1)+fib(n-2);
    }

注意,我们可以使用第二数学归纳法可以证明fib(n)这一递推函数的正确。通常3我们假设的是某个n,相应的命题P(n)成立,在推出P(n+1)也成立,我们可以假设一切小于n的相应命题k(k<=n)都成立。如上的 代码,我们假设一切小于n-1的相应调用fib(k)(k<=n-1)都能计算出斐波那契项,再于基准情况合并,我们就认为此代码是可行的。

        不过话说回来,它为什么是低效的呢?我们可以计算此递归算法的运行渐进时间,我们假设在一次if语句判断和一次加法分别最多用时一个单位长度,根据递推关系,设T(n)为输入n,输出结果后算法所花费的中运行时间,有如下的式子:

T(n)=\begin{cases} 1&,n=0,1 \\ T(n-1)+T(n-2)+2 &,n>1 \end{cases}

 当n等于0或1时,只执行了一条if语句就返回,若n大于1,执行了包含if语句和一次加法再加上两次调用。可以证明,对充分大的n,有

\left ( \frac{3}{2} \right )^{n}<T(n)<2^{n}

这说明T(n)是指数级增长的。

为什么会这样呢?这是因为每次fib(n-2)都被计算了两次,浪费了时间,在计算fib(n)时就需要计算fib(n-2)。如果将其记录下来,那么就会节省不少时间。 可以这样:

public static int fib(int n){
        int[] f=new int[n+1];
        f[0]=f[1]=1;
        for(int i=2;i<=n;i++){
            f[i]=f[i-1]+f[i-2];
        }
        return f[n]
    }

在设计递归的程序时,我们要考虑:

1.递归的方法内部的所有非递归部分几乎都是在处理基准情况;
2.递归方法体内的递归调用就是归纳假设,因此在设计递归方
法时一定要明确方法的功能,并假设它能够调用实现应该的
功能。千万不要进入递归方法调用内部分析功能;
3.有些递归算法并不高效。

 下面给出示例:

示例1

若要对n个不同的小写字母进行排列,并打印出所有可能的排列,请设计出算法实现它。

如:输入n=1以及“a”会得到“a”.输入n=2以及"ab",会得到“ab”和“ba”;“abc”则是“abc”,"acb","bac","bca","cab","cba"。通过观察我们发现,长度为n的排列都是在长度为n-1的排列前面加上另外一个字母而得,即n个字母的排列是这n个不同的字母与剩下的n-1个不同字母组成的所有排列组合的结果。

如n=3,n-1=2:
a:(abc,acb)
    bc
    cb
b:(bac,bca)
    ac
    ca
c:(cab,cba)
    cab
    cba


又如n=4,n-1=3:
d:(dabc,dacb,dbac,dbca,dcab,dcba)
    abc
    acb
    bac
    bca
    cab
    cba
a:(abcd,abdc,acbd,acdb,adbc,adcb)
    bcd
    bdc
    cbd
    cdb
    dbc
    dcb
b:    ......

于是我们假设lists(String str,String head)是一个输入n个不同字母的其中一个排列字符串(如a,b,c的一个排列"abc")打印这n个不同字母的所有排列并且这些排列都额外加上了字符串head作为前缀的方法(在提醒一次,要先假设递归函数的功能,在调用时不要尝试进入递归方法体内部分析)。

public static void lists(String str,String head){
        if(str==null)return;
        if(str.length()<=1){
            System.out.println(head+str);
            return;
        }

        for(int i=0;i<str.length();i++){
            lists(
              str.substring(0,i)+str.substring(i+1,str.length()),
                    String.valueOf(head+str.charAt(i))
            );
        }

首先,本方法处理了待排列字符串的为空或长度为1的基准情况,如果这个字符串的长度不小于等于1,则对n个不同字母中的每一个,将它和剩下的n-1个不同字母的所有排列相组合,每一个有1*(n-1)!种排列,n个(次for循环)共n!个排列。注意要明确定义方法的功能,不要有模糊的地方,最后需要额外加上前缀head。

示例2

这是一个经典的问题,被称为汉萝塔问题。据古印度神话,在贝纳勒思的神庙里安放着一块铜板,版上竖插着三根银针,在其中的一根针上插着由小到大(越往上越小)的n片金片,现需要将这n片带插孔的金片移动到另一根针上,移动规则是一次只能将一个金片移动到另一根针上,并且在任何时候以及任何一根针上,小片只能放在大的上面。如n=3时,其中第一根针上有小到大、从上至下插着1、2、3三片金片,要将1、2,3都移动到第二根针上,将这三根针分别依次编号为A,B,C,则可以这样移动:

将金片1从A移动到B
将金片2从A移动到C
将金片1从B移动到C
将金片3从A移动到B
将金片1从C移动到A
将金片2从C移动到B
将金片1从A移动到B。

请设计程序打印出n片金片的移到过程。

        很显然,当金片个数多于1时,仅仅凭着两根针是不能完成移动的,需要借助第三根针的帮助。根据由小及大的规则,我们先将n-1个小金片从针A移动到针C,让后将最大的那个金片从针A移动到针B,最后再将先前的n-1个金片从C移动到B。

我们定义方法moves(int n,char from,char to,char by)可以打印出将n片金片借助by从from移动到to的过程。则代码如下:

public static void moves(int n,char from,char to,char by){
        if(n==1)
            System.out.println("将金片"+1+"从"+from+"移动到"+to);
        else {
            moves(n-1,from,by,to);
            System.out.println("将金片"+n+"从"+from+"移动到"+to);
            moves(n-1,by,to,from);
        }

    }
 //汉萝他问题——将编号为1~n的n个金片从第一根针A移动到第二根针B,并借助第三根针C
        moves(4,'A','B','C');

在上面的代码中,if语句处理基准情况,else语句是三行语句,分别将n-1个小金片从针from移动到针by,让后将最大的那个金片从针from移动到针to,最后再将先前的n-1个金片从by移动到to。

示例3

下面内容含有一些介绍,不熟悉的读者可以在网上查阅相关资料。

 树结构是递归定义的数据结构,一颗树T,要么为空集,要么至少包含一个节点,多于1的节点只间具有从这到那的矢量关系,起始的那个节点是父节点,被箭头指向的那个节点是父节点的子节点,它们是父子关系,是一种单向关系,每个子节点有且只有一个父节点。一个节点(这个节点就是树根,一个唯一在整个树中没有父亲的节点,且所有其它节点都是它的后代)下游若干棵子树,这些子树又是一颗树,它也有树根,它的树根是在它这棵子树中没有父节点的节点,而它又是整个大树的树根的子节点。

遍历一颗二叉树(它的每棵父节点只有两个子节点,且分左子节点和右子节点),我们可以先访问其根,然后再访问其左子节点和右子节点,这叫做先序遍历。

我们定义的二叉树如下:

class Tree {
    Node root;

    public Tree(Node root) {
        this.root = root;
    }
}
class Node{
    String data;
    Node left;
    Node right;

    public Node(String data) {
        this.data = data;
    }

    public Node(String data, Node left, Node right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }
}

我们定义方法visitTree(Node root)能够从树根root开始先序遍历以root为根的树,则代码如下:

public class TreeTest{

    public static void visitTree(Node root){
        if(root==null)return;
        else {
            System.out.println(root.data);
            visitTree(root.left);
            visitTree(root.right);
        }
    }

    public static void main(String[] args) {
        //初始化一个二叉树
        Node f = new Node("F");
        Node e = new Node("E",null,f);
        Node d = new Node("D");
        Node c = new Node("C",e,null);
        Node b = new Node("B",d,null);
        Node a = new Node("A",b,c);
        Tree tree = new Tree(a);

        visitTree(a);

    }

以上代码,if语句处理基准情况,else语句递推,首先打印root.data访问根节点,然后分别从左子节点和右子节点开始遍历左子树和右子树。

示例4 

        下面,我们再举一个运用递归来设计算法的例子。 我们要求随机地构建一颗节点数为n的二叉树。若n==1,那么这颗树就是确定的,若n>1,那么我们可以递归地构建,先构建一颗左字树,再构建一颗右子树,它们的节点数分别为i和n-1-i,我们只需随机地指定左子树的节点数即可。我们规定方法

Node<Integer> randomTree(int n,int[] data)

用于随机地生成一颗 二叉树,它返回这颗二叉树的根节点,而方法中的参数data是为了标识每个节点,之所以是数组,是因为只有这样才能改变data中的值。下面代码只需递归调用即可。

/**
     * 试图编写一个可以随机生成一颗二叉树的方法
     */
    public static BinaryTree<Integer> randomTree(int n){
        int[] data=new int[1];
        return new BinaryTree(randomTree(n,data));

    }
    private static Node<Integer> randomTree(int n,int[] data){
        if(n==0)return null;
        if(n==1)return new Node<Integer>(data[0]++);
        int m=new Random().nextInt(n);//产生随机数确定左子树节点数
        Node<Integer> left=randomTree(m,data);
        Node<Integer> right=randomTree(n-m-1,data);
        return new Node<Integer>(data[0]++,left,right);
    }

 那么问题来了,到底有多少二叉树?解答在附录中。

示例5 

        为了看清楚所生成的树时随机的,你可以编写打印树的代码来观察。这又是一个递归的例子,我们可以指定树的深度来打印,代码如下: 

 static <E> void printTree(Node<? extends E> r,int depth){
        if(r==null)return;
        for(int i=0;i<depth;i++)System.out.print("\t");
        System.out.println(r.data);
        printTree(r.left,depth+1);
        printTree(r.right,depth+1);
    }

示例6

下面内容也含有一些介绍,不熟悉的读者可以在网上查阅相关资料。

        为了弄清楚递归调用的一些机制,我们先来看看一个问题,它模拟了递归调用。

问题:这是一个平衡符号的问题,要求输入一串包含([{}])的字符串,判断这三种括号是否成对存在且不相互交叉,但可以互相独立地排列着,例如,([)]是交叉,是非法的,另外是包含与被包含、独立第排列:例如([])和()[]都是合法的。

        恒明显这样的问题可以有栈来实现。代码如下:

/**
     * 这是一个平衡符号算法,是栈的典型应用,同时也是对操作系统底层方法
     * 调用的简单模拟。开放符号表示方法的加载和调用,封闭符号表示方法的
     * 返回。
     * @param str 是输入的一串包含[{()]}的字符串。
     * @return true 如果[]、{}、()是成对存在的并且它们之间不交叉,
     * 否则 返回 false
     */
    public static boolean balance(String str){
        Stack<Character> s = new Stack<>();
        for (int i = 0; i < str.length(); i++) {
            char c=str.charAt(i);
            if(c=='(' || c=='[' || c=='{'){
                s.push(c);
            }else if(c==')' || c==']' || c=='}'){
                if(s.isEmpty()){
                    return false;
                }
                else if(s.peek()=='(' && c==')')s.pop();
                else if(s.peek()=='[' && c==']')s.pop();
                else if(s.peek()=='{' && c=='}')s.pop();
                else return false;
            }
        }
        return s.isEmpty();
    }

        这是一个平衡符号算法,是栈的典型应用,同时也是对操作系统底层方法 调用的简单模拟。开放符号表示方法的加载和调用,封闭符号表示方法的 返回。

        如果在一个方法中,调用了一个方法,那么就是包含与被包含,如果一些方法被按顺序地调用执行,呢么就是独立的排列的关系。读者可以在这里体会体会。

        最后,附上了一些有关二叉树的代码作为参考。

附录

解答:S(n)=\left\{\begin{matrix} 1 &,n\leq 1, \\ \sum_{i=0}^{n-1}[S(i)\times S(n-1-i)]& ,n>1. \end{matrix}\right.,其中n是一棵二叉树的节点数。

代码: 

package data_structures.trees;

import java.util.LinkedList;
import java.util.Random;
import java.util.Stack;

public class BinaryTree<E> {

    Node<E> root;

    public BinaryTree(){}
    public BinaryTree(Node<E> root) {
        this.root = root;
    }

    /**
     * 依据后缀表达式来生成一颗相应的表达式树。
     * @param expr 包含数字、二元操作符且用空格分隔的后缀表达式
     * @return expr 的后缀表达式
     */
    public static BinaryTree<String> createExprTree(String expr){
        String[] arr = expr.split(" ");
        Stack<Node<String>> stack = new Stack<>();
        for (String s : arr) {
            if(!s.equals("+") && !s.equals("-") && !s.equals("*") && !s.equals("/")){
                stack.push(new Node<>(s));
            }else {
                Node<String> p;
                switch (s){
                    case "+":
                        p = new Node<>("+");
                        p.right=stack.pop();
                        p.left=stack.pop();
                        stack.push(p);
                        break;
                    case "-":
                        p = new Node<>("-");
                        p.right=stack.pop();
                        p.left=stack.pop();
                        stack.push(p);
                        break;
                    case "*":
                        p = new Node<>("*");
                        p.right=stack.pop();
                        p.left=stack.pop();
                        stack.push(p);
                        break;
                    case "/":
                        p = new Node<>("/");
                        p.right=stack.pop();
                        p.left=stack.pop();
                        stack.push(p);
                        break;
                }
            }
        }
        return new BinaryTree<>(stack.pop());
    }


    private static class Node<E> {
        E data;
        Node<E> left;
        Node<E> right;
        boolean visited;

        public Node(E data) {
            this.data = data;
        }

        public Node(E data, Node<E> left, Node<E> right) {
            this.data = data;
            this.left = left;
            this.right = right;
        }
    }

    public void postPrint(Node<E> r){
        if(r == null)return;
        postPrint(r.left);
        postPrint(r.right);
        System.out.print(r.data+"  ");
    }

    /**
     * 二叉树的非递归后序遍历算法
     */
    public static <E> void postPrint(BinaryTree<E> tree){
        Node<E> root=tree.root;
        LinkedList<Node<E>> q = new LinkedList<>();
        if(root == null)return;
        q.addLast(root);

        while(!q.isEmpty()){
           Node<E> cur=q.getLast();
           if(cur.left != null && !cur.left.visited)
                   q.addLast(cur.left);
           else if(cur.right != null && !cur.right.visited)
               q.addLast(cur.right);
           else {
               cur.visited=true;
               System.out.print(q.removeLast().data+"  ");
           }
        }
    }

    /**
     * 试图编写一个可以随机生成一颗二叉树的方法
     */
    public static BinaryTree<Integer> randomTree(int n){
        int[] data=new int[1];
        return new BinaryTree(randomTree(n,data));

    }
    private static Node<Integer> randomTree(int n,int[] data){
        if(n==0)return null;
        if(n==1)return new Node<Integer>(data[0]++);
        int m=new Random().nextInt(n);
        Node<Integer> left=randomTree(m,data);
        Node<Integer> right=randomTree(n-m-1,data);
        return new Node<Integer>(data[0]++,left,right);
    }

    /**
     * 非递归地打印二叉树
     */
//    public void printTree(){
//         class Tmp<E>{
//            Node<E> node;
//            boolean visited;
//        }
//
//       Stack<Tmp<E>> stack=new Stack<>();
//
//         stack.push(root);
//
//         while(!stack.isEmpty()){
//             Tmp<E> cur=stack.peek();
//
//
//        }
//    }

     static <E> void printTree(Node<? extends E> r,int depth){
        if(r==null)return;
        for(int i=0;i<depth;i++)System.out.print("\t");
        System.out.println(r.data);
        printTree(r.left,depth+1);
        printTree(r.right,depth+1);
    }

    public static void main(String[] args) {
        BinaryTree<Integer> t = randomTree(10);

        printTree(t.root,0);
        System.out.println();
        t.postPrint(t.root);
        System.out.println();
        System.out.println();
        postPrint(t);
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值