(八)递归

目标

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

  • 方法至少有一个参数或输入值吗?
  • 方法含有测试参数或输入值的语句,且能导向不同的情形吗?
  • 考虑了所有可能的情形吗?
  • 至少有一种情形导致至少一次的递归调用吗?
  • 这些递归调用调用更小的实参、更小的任务或接近于解决方案的任务吗?
  • 如果这些递归调用能产生或返回正确的结果,那么方法产生或返回正确结果了吗?
  • 是不是至少有一种情形(基础情形)没有递归调用?
  • 基础情形足够吗?
  • 每个基础情形都能得到对应于这种情形的结果吗?
  • 如果方法返回一个值,则每种情形都返回一个值吗?

转载于:https://www.cnblogs.com/datamining-bio/p/9677962.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值