结构递归与生成递归有何不同?

Posted

技术标签:

【中文标题】结构递归与生成递归有何不同?【英文标题】:How does structural recursion differ from generative recursion? 【发布时间】:2012-12-25 11:30:28 【问题描述】:

Wikipedia 中对生成递归的描述我很清楚,但我对结构递归的概念感到困惑。

有人能解释一下计算第 n 个斐波那契数的函数和计算从 1 到 N 阶乘的函数是结构性的还是生成性的?

【问题讨论】:

我的两分钱:Fib 使用该定义是生成递归的,因为数据是随着它的进行而“生成”的。然而,根据文章,结构递归是关于遍历 [现有] 图。文章接着指出,一个关键的区别是结构递归可以通过结构归纳来证明终止.. @pst- 我认为斐波那契实际上是结构递归。与其说是“生成”数据,不如说是关于如何为递归调用提供参数。 【参考方案1】:

结构递归和生成递归之间的主要区别在于,递归过程在哪里获取它所处理的数据以及它如何处理这些数据。具体来说,对于结构递归,对原始输入数据的子集进行递归调用。而对于生成递归,递归调用是对根据原始输入数据构造/计算的数据进行的。

例如,如果要计算链表中元素的数量,可以执行以下操作:

int NumberOfNodes(ListNode* node) 
    if (node == nullptr) return 0;
    return 1 + NumberOfNodes(node->next);

在这里,对NumberOfNodes 的递归调用是在node->next 上进行的,这是已经存在的原始输入的一部分。在这种情况下,递归的工作原理是将输入分解成更小的部分,然后在更小的部分上递归。

类似地,在 BST 中搜索值的代码将是结构递归,因为递归调用是针对原始输入的子部分:

TreeNode* Find(TreeNode* root, DataType value) 
    if (root == nullptr) return nullptr;
    if (value < root->value) return Find(root->left, value);
    else return Find(root->right, value);

术语“结构递归”源于这些结构(列表、BST 等)可以递归定义:

一个列表要么什么都不是,要么是一个单元格后跟一个列表。 二叉树要么什么都不是,要么是一个有两棵二叉树作为子节点的节点。

在进行结构递归时,您正在“撤消”这些结构相互构建的操作。例如,NumberOfNodes 函数“取消”获取节点并将其添加到现有列表的构造。 Find 运算符“取消”将节点粘合到另外两棵树的操作。因此,很容易看出为什么这些函数必须终止 - 最终,您“撤消”了最初构建对象的所有操作,然后递归停止。

另一方面,考虑快速排序,它执行以下操作:

    选择一个支点。 创建三个新列表:小于枢轴的所有元素之一,大于枢轴的所有元素之一,以及等于枢轴的所有元素之一。 对这些列表中的第一个和第二个进行递归排序。 连接较小、相等和较大值的列表。

在这里,递归调用是在不属于原始输入的较小数组上进行的 - 必须从数据中创建列表。 (通常,实现会为这些列表重用空间,但不能保证这些子列表直接存在于输入中)。

当涉及到自然数时,这种区别是模糊的。通常,自然数递归定义如下:

0 是自然数。 如果 n 是自然数,则 n + 1 是自然数。 没有其他东西是自然数。

在此定义下,数字 n 是 n + 1 的“部分”。因此,此递归代码计算 n!是结构递归:

int Factorial(int n) 
    if (n == 0) return 1;
    return n * Factorial(n - 1);

这是结构递归,因为参数 n - 1 是原始输入 n 的“一部分”。

同样,根据这个定义,递归计算第 n 个斐波那契数算作结构递归:

int Fibonacci(int n) 
    if (n <= 1) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);

这被认为是结构递归,因为 n - 1 是 n 的一部分(通过“撤消”+1 形成),而 n - 2 是 n - 1 的一部分(再次通过“撤消”+1 形成)。

另一方面,计算 gcd 的这段代码将被视为生成递归,而不是结构递归:

int gcd(int a, int b) 
    if (b == 0) return a;
    return gcd(b, a % b);

原因是,由于a % b 是从ab“计算”出来的,而不是通过“撤消”一些+1 操作而形成的,因此生成了数据。

生成递归与结构递归不同的原因在于不能保证它会终止。例如,想想这个函数:

int BadTimes(int a, int b) 
    if (a == 0 && b == 0) return 0;
    return BadTimes(a * 2, b - 1);

这个生成递归函数永远不会终止:a 不断变大,尽管 b 不断变小。

老实说,我以前从未听说过这种区别,我教授离散数学和编程课程。除非有人要求您知道其中的区别,否则我不会太担心。

希望这会有所帮助!

【讨论】:

从您提供的示例和***中的示例中,我了解到所有分而治之的算法(如果以递归方式实现)都将受到生成递归的影响。 @AdityaKumar- 几乎可以,但不一定;这取决于所讨论的结构。例如,在树中找到最大值可以通过分而治之的方法完成,但仍然是结构递归。 嗯...生成递归中的二进制搜索示例是造成我困惑的最大原因之一。 @AdityaKumar- 我认为这与您定义数组的方式有关。我认为他们的定义是“一个数组要么什么都不是,要么是一个值后跟一个数组”。根据该定义,将数组切成两半并不是“撤消”构造操作。再说一遍——老实说,我认为这是一个毫无意义的区别;在从事离散数学多年之前,我从未遇到过这种情况。 开头提到的关键区别,是不是累加递归?

以上是关于结构递归与生成递归有何不同?的主要内容,如果未能解决你的问题,请参考以下文章

Java数据结构与算法——递归与回溯

《数据结构与算法之美》07——递归

生成不同字符串的非递归组合算法

尚硅谷算法与数据结构学习笔记05 -- 递归

数据结构与算法—递归recursion

数据结构与算法——初谈递归