`
klcwt
  • 浏览: 194599 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

递归与间接递归

    博客分类:
  • java
阅读更多

1. 介绍


递归技术允许我们将原问题分解为一个或多个形式上相似的子问题

 


2. 例子:级数


级数(n!)的定义如下:

   n! = 1 * 2 * 3 * .... * (n-2) * (n-1) * n

可以根据定义写出如下迭代形式的实现

int fact(int n) {
   int i;
   int result;
  
   result = 1;
   for (i = 1; i <= n; i++) {
      result = result * i;
   }
   return result;
}

还可以写出递归形式的实现

int fact(int n) {
    if (n == 1) return 1;
    return n * fact(n - 1);
}

比较这两个版本:

  • 迭代版本中包含两个局部变量,而递归版本中只有一个
  • 迭代版本中有三条声明语句,而递归版本中只有一条
  • 迭代版本在返回前需要将结果存储在中间变量中,而递归版本在一个表达式中完成计算和返回


可以看到,递归简化了级数的实现:让计算机去做更多的事情,从而让程序员作更少的事情。

 


3. 定义


递归函数可以进行如下分类:
The recursive functions are characterized based on:

  1. 是否调用自身(直接或间接递归)
  2. 是否存在未决操作(尾递归(tail-recursive)或非尾递归)
  3. 调用模式的结构——是否未决操作本身也是递归的(线性递归或树状递归)

直接递归:

递归函数的函数体中存在显式的自我调用时,被称为直接递归。例如,函数foo中包含自我调用,因此是直接递归。

int foo(int x) {

   if (x <= 0) return x;

   return foo(x - 1);
}

间接递归:

函数foo被称为间接递归,如果它包含对另一个函数的调用而该函数最终会调用函数foo。

下面的一对函数属于间接递归。由于他们都互相调用,因此又被称为互递归函数

int foo(int x) {

   if (x <= 0) return x;

   return bar(x);
}

int bar(int y) {
   return foo(y - 1);
}

尾递归(tail recursion):

一个递归函数被称为尾递归函数,如果在递归调用的过程中不存在未决操作的话。

尾递归函数通常被描述为:“返回上次递归调用得到的值作为函数的返回值。”尾递归具有很大的价值,因为在(递归)计算过程中需要维护的信息量与递归调用次数是无关的。某些现代计算机系统在实际上通过迭代过程来完成尾递归函数的计算。

“声名狼藉"的级数函数通常被写成非尾递归的形式:

int fact (int n) { /* n >= 0 */

   if (n == 0)  return 1;

   return n * fact(n - 1);
}

注意到这里在每次递归调用返回时存在一个未决操作——乘法运算。只要存在未决操作,递归函数就是
非尾递归的。所有未决操作的相关信息必须被存储,因此是与递归调用的次数密切相关的。

级数函数也可以被写成尾递归形式:

int fact_aux(int n, int result) {

   if (n == 1) return result;

   return fact_aux(n - 1, n * result)
}


int fact(n) {

    return fact_aux(n, 1);
}

辅助函数fact_aux被用来保证不需要改变fact(n)的调用形式。实际的递归函数是fact_aux,而不
是fact。注意fact_aux中不存在未决操作。通过递归调用得到的值被不加修改的直接返回。计算过程
中需要维持的信息量是常数(变量n和变量value的值),与递归调用的次数无关。

 

线性和树状递归:


递归函数的另一种分类方式是递归调用是如何增长的。两种基本形式是"线性"和"树状"。

一个递归函数被称为线性递归,如果其中的未决操作(如果存在的话)并不涉及到递归调用的话。

例如,“声名狼藉"的fact函数是线性递归。其未决操作是简单的乘以一个标量,并不涉及对fact的递归调用。

一个递归函数被称为树状递归(或非线性递归),如果未决操作也是递归调用的话。

 

Fibonacci函数为树状递归提供了一个经典的例子。Fibonacci数列按如下规则定义:

   fib(n) = 0  if n is 0,
          = 1  if n is 1,
          = fib(n-1) + fib(n-2) otherwise

例如,前7个Fibonacci数分别是:

   Fib(0) = 0   
   Fib(1) = 1   
   Fib(2) = Fib(1) + Fib(0) = 1
   Fib(3) = Fib(2) + Fib(1) = 2
   Fib(4) = Fib(3) + Fib(2) = 3
   Fib(5) = Fib(4) + Fib(3) = 5
   Fib(6) = Fib(5) + Fib(4) = 8


这样引出了如下的实现:

int fib(int n) { /* n >= 0 */

   if (n == 0) return 0;

   if (n == 1) return 1;

   return  fib(n - 1) + fib(n - 2);
}


注意到函数体中的未决操作是另一次递归盗用,因此fib函数是树状递归。

 


4. 将递归函数转换为尾递归

一个非尾递归函数经常可以借助于一个附注参数被转换为尾递归函数,该参数被用于存放结果。基本思想是将未决操作合并到辅助参数中,从而消除未决操作。该技术通常还需要一个辅助函数,这只是为了保持文法的清晰和对外隐藏辅助参数的实际使用。


例如,可以通过使用两个用于存放结果的辅助参数来实现一个尾递归的Fibonacci函数。原有的树状递归函数fib需要两个附注参数来存放结果,这并不奇怪,因为它涉及两个递归调用。要计算fib(n),则调用fib_ax(n 1 0)

int fib_aux(int n, int next, int result) {

  if (n == 0) return result;
  return fib_aux(n - 1, next + result, next);
}



树状递归的fib() 函数是一个复杂度为O(2^n)的算法,换句话说当n递增1时问题尺寸加倍。另一方面,线性递算法则是O(n)的,换句话说,需要的工作量是线性增长的。


References: Thomas A. Anastasio, Richard Chang.
分享到:
评论

相关推荐

    算法 格雷码 递归算法

    递归指算法自己调用自己, 有直接递归与间接递归两种。 递归方法用于解决一类满足递归关系的问题。即:对原问题的求解可转化为对其性质相同的子问题的求解。 三. 该类算法设计与实现的要点 1. 递归关系(特性):...

    计算导论与程序设计:chap6 递归算法设计.ppt

    直接递归与间接递归** 递归可以分为直接递归和间接递归。直接递归是指函数直接调用自身,如上面的阶乘函数`f(n)`。间接递归则是通过其他函数间接调用自身,比如函数A调用函数B,而函数B再调用函数A。 **注意事项*...

    C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。

    递归函数就是直接或间接调用自身的函数。 许多教科书都把计算机阶乘和菲波那契数列用来说明递归,非常不幸我们可爱的著名的老潭老师的《C语言程序设计》一书中就是从阶乘的计算开始的函数递归。导致读过这本经书的...

    递归算法与非递归转化

    递归算法与非递归转化 递归算法是把问题转化为规模缩小了的同类问题的子问题,然后递归调用函数(或过程)来表示问题的解。递归的效率一般不高,但是递归比较符合人类的思维方式。一般而言非递归算法更有效;但很多...

    C#中的尾递归与Continuation详解

    递归与尾递归 关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度: 代码如下: public class Node {  public ...

    java编写的递归与非递归

    ### Java中的递归与非递归编程技巧 在Java编程中,递归和非递归是两种常用的处理问题的方法。递归通常用于解决那些可以通过分解为更小问题来解决的问题,而非递归则更适合于使用迭代的方式来解决问题。下面将详细...

    C#递归 C#递归 C#递归

    递归是一种编程技术,它允许一个方法或函数直接或间接地调用自身。递归通常用于解决可以通过重复相同过程分解成更小问题的问题。递归方法包含两个主要部分: 1. **基本情况**(Base Case):这是递归结束的条件。 2....

    NOIP普及讲座1-递归与分治(C++版).pdf

    根据给定文件的信息,我们可以总结出以下关于递归与分治技术的重要知识点: ### 一、递归的基本概念 #### 定义: 递归是一种在程序设计中非常重要的方法,它指的是在一个函数或过程中直接或间接地调用自身来解决...

    acm教程递归与分治

    **递归与分治策略详解** 递归与分治是计算机科学中两种强大的算法设计方法,尤其在解决复杂问题时非常有效。它们是ACM(国际大学生程序设计竞赛)等算法竞赛中常见的技术,也是软件工程师必备的技能之一。 **递归...

    acm递归算法总结竞赛

    3. **类型**:递归可以分为直接递归(函数直接调用自身)和间接递归(函数A调用函数B,函数B又调用函数A)。 4. **实例分析**: - **斐波那契数列**:经典的递归例子,第n个斐波那契数是前两个数之和,递归公式为F...

    消除文法左递归

    本文将深入探讨左递归的定义、为何需要消除以及如何消除,同时结合课程设计与实验报告的实际案例进行讲解。 一、左递归的概念 左递归是指在文法(Grammar)中,一个非终结符(Non-terminal)可以无限制地直接递归...

    5!递归算法和非递归算法

    为了更好地理解递归与非递归算法的区别,我们可以通过计算阶乘的例子来具体分析: #### 递归算法示例 ```java public class DiGui { public static void main(String[] args) { System.out.println(f(5)); } ...

    第2章 递归与分治策略.pdf

    在IT行业中,递归与分治策略是算法设计中非常重要的概念,尤其是在处理数据结构问题时。本章节内容将会详细阐述递归的概念、分治法的设计思想,以及如何通过递归方法来解决一些复杂的问题。分治策略通过将一个大问题...

    递归算法到非递归算法的转换.ppt

    如果一个过程调用另一个,而后者又调用前者,那么这是间接递归。例如,计算阶乘的递归算法会根据n的值调用自身计算n-1的阶乘,直到n等于0,这时递归停止,返回1。这就是递归的终止条件。 递归通常在以下三种情况下...

    算法思想——递归与分治.ppt

    递归与分治是计算机科学中两种重要的算法思想,它们在解决复杂问题时起到了关键作用。递归是一种方法,通过函数或程序直接或间接调用自身来解决问题。递归通常包含两个基本要素:边界条件和递归方程。边界条件是递归...

    递归算法的详解,各种常见递归算法

    在程序设计中,递归调用是指一个函数直接或间接地调用自身来解决问题。 递归的应用可以追溯到数学中的数列求解。例如,阶乘数列就是一个经典的例子。阶乘可以通过递归函数表示,如`f(n) = n * f(n-1)`,当n等于1时...

    递归与分治策略

    ### 递归与分治策略 #### 一、递归与分治策略概述 递归与分治策略是一种广泛应用于计算机科学和算法设计中的方法。这种方法的核心思想是将复杂问题分解为若干个相同类型的子问题,然后递归地解决这些子问题,最后...

    递归算法在C/C++程序设计的描述与实现

    以下步骤: ### 递归算法的核心思想 递归算法的核心思想是将复杂问题分解为相似但规模较小的子问题,直至...然而,开发者也应警惕递归带来的潜在问题,如效率低下和栈溢出风险,合理选择递归与迭代等其他算法策略。

    消除文法的左递归

    为了消除文法中的左递归,常见的方法有两种:直接左递归的消除和间接左递归的消除。 1. **直接左递归的消除**: - 对于直接左递归的产生式 A → Aα | β,可以转换为 A → βA’ 和 A’ → αA’ | ε 的形式。 ...

Global site tag (gtag.js) - Google Analytics