zoukankan      html  css  js  c++  java
  • (八)递归

    目标

    1) 判定所给出的递归方法是否能在有限时间内顺利结束

    2) 写一个递归方法

    3) 评估递归方法的时间复杂度

    4) 识别尾递归并能用迭代来替代它

    迭代for、while等循环。包含想要重复执行的语句及控制重复次数的机制。

    缺点:有时会非常复杂,找到或验证这样的方案很困难。

                                    ↓

    递归:替代复杂的迭代(有时也不能用,因为效率极低)

    目录

    7.1 什么是递归

    7.2 跟踪递归方法

    7.3 返回一个值的递归方法

    7.4 递归处理数组

    7.5 递归处理链

    7.6 递归方法的时间效率

      7.6.1 countDown的时间效率

      7.6.2 计算Xn的时间效率

    7.7 困难为题的简单求解方案

    7.8 简单问题的低劣求解方案

    7.9 尾递归

    7.10 间接递归

    7.11 使用栈来替代递归

    小结

    7.1 什么是递归

      当你解决一个问题时,将它划分为更小的问题且用相同的方法来解决。问题求解过程中每次具体的变形,除了其大小外,较小的问题与原来的问题是一样的。这种特殊的处理称为递归(recursion)。递归的一个关键是,最终你能到达一个较小的问题,而这个较小问题的解决方案你是知道的,或者因为答案很明显,或者因为已经给出了答案。这个最小问题的求解或许不是原始问题的求解方案,但它能帮助你达成目标。无论在求解更小问题之前或之后,通常你都解决了问题的一部分。这个不烦与其他更小的部分的解决方案一起,得到更大问题的求解方案。

    注:调用自己的方法称为递归方法(recursive method)。调用是递归调用(recursive callrecursive invocation)。

    设计递归方案时要回答的问题

    1) 方案的哪个部分的工作能让你直接完成?

    2) 哪些较小且相同的问题已有了求解方案,当加上你的贡献时,能提供对原始问题的求解?

    3) 过程何时结束?即,哪个更小但相同的问题已有能让你达成目标或基础情形(base case)的已知的解决方案?

     

     

    public static void countDown(int integer){
         System.out.println(integer);     //可以直接完成的部分

         if(integer > 1){      //询问是否到达了基础情形
         countDown(integer - 1);  //更小的问题从integer-1开始

      }

    }

    :成功递归的设计原则

    1) 必须给方法一个输入值,通常作为参数给出;

    2) 方法定义必须含有这个输入值并能导致不同情形的逻辑。一般地,这样的逻辑包含一个if语句或一个switch语句;

    3) 这些情形中的一个或多个,应该不再需要递归。这些是基础情形,或终止情形(stopping case)

    4) 一个或多个情形必须包含对方法的递归调用。这些递归调用应该含有一些步骤,这些步骤通过使用“更小的”参数,或者由方法完成的“更小”版本的任务的求解,在某种意义上逐步导向基础情形。

    程序设计技巧:无穷递归

    1)不检查基础情形或者缺少基础情形的递归方法,将“永远”执行。这种情形称为无穷递归。

    2)迭代方法含有一个循环。递归方法调用自己。虽然有些递归方法也含有一个循环且调用自身,但如果在递归方法中写while语句,一定要确信不是想写if语句。

     

    7.2 跟踪递归方法

      图7-3表示方法countDown的多个副本。实际上,并不存在多个副本。对方法的每次调用(递归或非递归)Java都记录方法执行的当前状态,包括它的参数和局部变量的值,以及当前指令的位置。每个记录称为一个活动记录,它提供运行期间方法状态的快照。记录放入程序栈中。栈按时间先后组织这些记录,所以当前正执行的方法的记录位于栈顶。

    注:一般地,递归方法比迭代方法使用更多的内存,因为每次递归调用都要产生一个活动记录。

    程序设计技巧:栈溢出

      进行许多次递归调用的递归方法,将在程序栈中放很多个活动记录。递归调用太多可能用掉程序栈中可用的所有内存,使栈满。结果,出现出错信息“栈溢出”。无穷递归或较大规模问题都可能导致这个错误。(对于大n很可能导致栈溢出,迭代不会有这样的困难

     

    7.3 返回一个值的递归方法

    程序设计技巧:调试递归方法

    1) 方法至少有一个输入值吗?

    2) 方法含有测试输入值的语句,且能导向不同情形吗?

    3) 考虑了所有可能情形吗?

    4) 至少有一种情形导致至少一次的递归调用吗?

    5) 这些递归调用调用更小的实参、更小的任务或接近于解决方案的任务吗?

    6) 如果这些递归调用产生或返回了正确的结果,则方法产生或返回正确结果了吗?

    7) 是不是至少有一种情形(即基础情形)没有递归调用?

    8) 基础情形足够吗?

    9) 每个基础情形都不能得到对应于这种情形的结果吗?

    10) 如果方法返回一个值,每种情形都返回一个值吗?

     

    7.4 递归处理数组

    1)从array[first]开始,displayArray(array, first+ 1, last)

    2)从array[last]开始,displayArray(array, first, last-1)

    3)将数组分半,mid=(first+last)/2,基础情形是含有一个元素的数组。

    注:寻找数组的中点

    计算数组中间元素的下标,可以使用语句

    int mid = first + (last - first) / 2;

    来代替

    int mid = (first + last) / 2;

    防止last+first发生溢出为负数,从而导致ArrayIndexOutOfBoundsException异常。

     

     

    7.5 递归处理链

      反向显示链:反向遍历链的节点,用迭代是困难的,递归如下。

    private void displayChainBackward(Node nodeOne){

       if (nodeOne != null){

          displayChainBackward(nodeOnde.getNextNode());

          System.out.println(nodeOne.getData());

       }

    }

     

    7.6 递归方法的时间效率

    7.6.1 countDown的时间效率

    public static void countDown(int integer){
       System.out.println(integer);

       if(integer > 1){
          countDown(integer - 1);

       }

    }

      当n为1时countDown显示1。这是基础情形,需要常数级的时间。t(n)表示countDown(n)的时间需求,则可以写为:

      t(1) = 1

      t(n) = 1 + t(n - 1)   n > 1

    表示t(n)的方法称为递推关系(recurrence relation),因为函数t的定义中又含有自身——递推。

      求解递推关系。设一个n=4——t(4) = 4,设t(n) = n  n >= 1

      证明t(n) = n。因为t(n) = 1 + t(n - 1)  n > 1成立

      需要替换方程右侧的t(n - 1),如果当n > 1时,有t(n - 1) = n – 1,则当n > 1时,t(n) = 1 + n – 1 = n是正确的。所以,如果能找到整数k,满足t(k) = k,则下一个整数也将满足它。归纳法证明。所以方法是O(n)的。

     

    7.6.2 计算Xn的时间效率

    (1)Xn = XXn-1  X0 = 1

    (2)Xn = (Xn/2)2         当n是正偶数时

         Xn = X(X(n -1)/2)2    当n是正奇数时

         X0 = 1

      计算可由方法power(x, n)实现,它含有递归调用power(x, n/2)。因为Java中整除是截断结果,所以不管n是偶数还是奇数,这个调用都是合适的。所以power(x, n)将调用power(x, n/2)一次,然后对结果求平方,如果n是奇数再将平方乘以x,乘积都是O(1)操作。所以power(x, n)的运行时间与递归调用的次数成正比。

    时间

    t(n) = 1 + t(n/2)  当n >= 2时

          t(1) = 1      t(0) = 1

    猜想:t(n) = 1 + log2n

    证明:对于n ≥ 1这个猜想确实是对的。对于n = 1, t(1) = 1 + log21 = 1 成立。对于n > 1,t(n) = 1 + t(n/2)是正确的。替换t(n/2),对于所有n < k,设都有t(n) = 1 + log2n,有t(k/2) = 1 + log2(k/2),因为k/2 < k,所以t(k) = 1 + t(k/2) = 1 + (1 + log2(k/2)) = 1 + log2k。

    假定对于所有的n<k,有t(n) = 1 + log2n,表明t(k) = 1 + log2k。所以对所有的n ≥ 1, 有t(n) = 1 + log2n。因为power的时间需求由t(n)表示,所以方法是O(log n)的。

     

    7.7 困难为题的简单求解方案

      汉诺塔问题

    Algorithm solveTowers(numberOfDisks, starPole, tempPole, endPole)

    if (numberOfDisks == 1)

       将盘子从startPole移到endPole

    else{
       solveTowers(numberOfDisks – 1, startPole, endPole, tempPole)

       将盘子从startPole移到endPole

       solveTowers(numberOfDisks – 1, tempPole, startPole, endPole)

    }

      若选择0个盘子代替1个盘子作为基础情形,则可以稍稍简化算法,但是会执行更多次的递归调用。

    Algorithm solveTowers(numberOfDisks, starPole, tempPole, endPole)  // 版本2

    if (numberOfDisks > 0)

    {
       solveTowers(numberOfDisks – 1, startPole, endPole, tempPole)

       将盘子从startPole移到endPole

       solveTowers(numberOfDisks – 1, tempPole, startPole, endPole)

    }

      效率

      m(n)表示solveTowers求解n个盘子时必须的移动步数。显然m(1)=1,对于n>1,算法使用两次递归调用,每次调用解决n-1个盘子的问题。每种情形中,所需的移动步数是m(n-1)。所以根据算法有m(n)=m(n-1)+1+m(n-1)=2 x m(n-1)+1,推算m(n)猜测m(n)=2n-1,使用数学归纳法证明为正确。移动步数乘指数增长m(n) = O(2n)。

      证明汉诺塔问题的求解不能少于2n-1步

     

    7.8 简单问题的低劣求解方案

      斐波那契数列

    Algorithm Fibonacci(n)

    if (n <= 1)

       return 1

    else

       return Fibonacci(n - 1) + Fibonacci(n - 2)

      递归调用会反复的计算值,Fibonacci(n-1)会再次计算Fibonacci(n-2),又都会计算Fibonacci(n-3),会形成下面的树形结构

      算法Fibonacci的时间效率

      t(n) 表示计算Fn的算法的时间需求,则有t(n)=1+t(n-1)+t(n-2)  n≥2, t(1) = 1, t(0) = 1,这个递推关系很像是斐波那契数自己。对于n>=2,有t(n)>Fn.

      程序设计技巧:不要使用在递归调用中重复解决同一问题的递归方案。

     

    7.9 尾递归

      当递归方法执行的最后一个动作是递归调用时发生尾递归。countDown就是尾递归

    public static void countdown(int integer){
       if(integer >= 1){
          System.out.println(integer);

          countdown(integer - 1);

       } // end if

    } // end countDown

      从尾递归转为迭代比较容易,这些开销主要是涉及内存,而不是时间,如果必须节省空间,就应该考虑用迭代来替代递归。

      修改汉诺塔

    Algorithm solveTowers(numberOfDisks, startPole, tempPole, endPole)

    while (numberOfDisks > 0){
       solveTowers(numberOfDisks-1, startPole, tempPole, endPole)

       将盘子从startPole移到endPole

       numberOfDisks—

       交换tempPole和startPole的内容

    }

     

    7.10 间接递归

      方法A调用方法B,方法B调用方法C,而方法C用调用方法A。这个的递归(称为间接递归)更难理解并跟踪,但在某些应用中自然存在。

     例如,下列规则描述了是合法代数表达式的字符串:

      1) 一个代数表达式或者是一项,或者是由+或-运算符分开的两项

      2) 一项或者是一个因子,或者是由*或/运算符分开的两个因子

      3) 一个因子或者是一个变量,或者是一个包含在圆括号内的代数表达式

      4) 一个变量是一个单字符

    假定方法isExpression、isTermisFactor和isVariable分别检测一个字符串是否是表达式、项、因子及变量。

     

      间接递归的一个特殊情况,即方法A调用方法B,而方法B调用方法A,称为相互递归(mutual recursion)。

     

    7.11 使用栈来替代递归

      使用迭代替代递归的一个方法是模拟程序栈。事实上,可以使用一个栈替代递归,从而实现递归算法。

      将displayArray修改为类内的一个非静态方法,带有一个数组作为数据域:

    public void displayArray(int first, int last){
       if(first == last)

          System.out.println(array[first] + “ ”);

       else{

          int mid = first + (last - first) / 2;

          displayArray(first, mid);

          displayArray(mid+1, last);

       } // end if

    } // end displayArray

      通过使用模拟程序栈的一个栈,将前一段给出的递归方法displayArray替换为迭代版本。创建局部于方法的一个栈。Java程序栈中的活动记录含有方法的实参、它的局部变量和指向当前指令的引用。

      为表示一个记录,我们需要定义一个类

    private class Record{
       private int first, last;

       private Record(int firstIndex, int lastIndex){
          first = firstIndex;

          last = lastIndex;

       } // end constructor

    } // end Record

      一般地,当该方法开始运行时,它将一个活动记录压入程序栈中。当它返回时,从这个栈中弹出一个记录。让迭代的displayArray来维护自己的栈。当方法开始运行时,它应该将一个记录压入这个栈中。每次递归调用都应该这样做。当栈不空时,该方法应该从栈中删除一个记录,并根据记录的内容来执行。当栈空时该方法结束运行。

    private void displayArray(int first, int last){
       boolean done = false;

       StackInterface<Record> programStack = new LinkedStack<>();

       programStack.push(new Record(first, last));

       while(!done && !programStack.isEmpty()){

          Record topRecord = programStack.pop();

          first = topRecord.first;

          last = topRecord.last;

          if(first == last)

             System.out.println(array[first] + “ ”);

          else{

             int mid = first + (last - first) / 2;

             // Note the order of the records pushed onto the stack

             programStack.push(new Record(mid+1, last));

             programStack.push(new Record(first, mid));

          } // end if

       } // end while

    } // end displayArray

    小结

    1) 递归是将问题划分为更小的同样问题的求解问题

    2) 递归方法的定义必须含有能处理方法的输入(常常是一个参数)的逻辑,并导向不同的情形。其中的一个或多个情形包括方法的递归调用,通过求解“更小”版本的任务,而向基础情形迈进

    3) 对方法的每次调用,Java将方法参数和局部变量的值记录在活动记录中。将记录放入栈中,并按时间顺序组织。最近入栈的记录是当前正在运行的方法。这样,Java可以暂停递归方法的执行,并用新的参数值重新执行它

    4) 当递归方法处理一个数组时,常常将数组分成几部分。对方法的递归调用将处理数组的每个部分

    5) 处理链式节点链的递归方法,需要一个指向链的第一个节点的引用作为参数

    6) 用来实现ADT的递归方法常常是私有的,因为它的使用需要对底层数据结构的了解。虽然这样的方法不适合作为ADT的操作,但它能被实现操作的公有方法调用

    7) 递推关系用函数自己来表示函数。可以使用递推关系来描述递归方法所做的事情

    8) 有n个盘子的汉诺塔问题的求解,至少需要2n-1次移动。这个问题的递归方案清晰,且尽可能地高效。但对于一个O(2n)的算法,只对很小的n值是可用的

    9) 斐波那契数列中的每个数(头两个之后)都是前两个数的和。递归地计算斐波那契数是不高效的,因为所需的前面的每个数都被计算了多次

    10) 当递归方法的最后一个动作是递归调用时发生尾递归。这个递归调用执行了可用迭代完成的重复部分。将尾递归方法转换为迭代方法,通常是一个简单的过程

    11) 当一个方法调用一个方法,后者又调用一个方法,等等,直到又调用第一个方法时,导致间接递归

    12) 可以使用栈替代递归来实现递归算法。这个栈模拟了程序栈的行为。

     

    程序设计技巧

    1) 迭代方法包含一个循环、递归方法调用自己。虽然有些递归方法内含有循环且调用自身,但如果你在递归方法内写一个while语句,确定你不是要写一个if语句

    2) 不检查基础情形或者丢掉基础情形的递归方法,不会正常终止。这种情况称为无穷递归

    3) 递归调用太多会导致错误信息“stack ovweflow”(栈溢出)。这意味着活动记录的栈已经满了。本质上,方法使用了太多的内存。无穷递归或大规模的问题容易引起这个错误

    4) 不要使用在递归调用中重复求解同一问题的递归方案

    5) 如果递归方法没有得到想要的结果,则回答下列问题。任何否定的答案都可能帮助你找到错误

    • 方法至少有一个参数或输入值吗?
    • 方法含有测试参数或输入值的语句,且能导向不同的情形吗?
    • 考虑了所有可能的情形吗?
    • 至少有一种情形导致至少一次的递归调用吗?
    • 这些递归调用调用更小的实参、更小的任务或接近于解决方案的任务吗?
    • 如果这些递归调用能产生或返回正确的结果,那么方法产生或返回正确结果了吗?
    • 是不是至少有一种情形(基础情形)没有递归调用?
    • 基础情形足够吗?
    • 每个基础情形都能得到对应于这种情形的结果吗?
    • 如果方法返回一个值,则每种情形都返回一个值吗?
  • 相关阅读:
    MSXML 解析XML文件
    内存泄露
    TCHAR CHAR LPTSTR LPCTSTR
    union过代理存根
    jquery placeholder
    SASS 编译后去掉缓存文件和map文件
    js冒泡排序
    android 下滤镜效果的实现
    ViewPagger介绍
    android下实现对wifi连接的监听
  • 原文地址:https://www.cnblogs.com/datamining-bio/p/9677962.html
Copyright © 2011-2022 走看看