Как структурная рекурсия отличается от генеративной рекурсии?

Описание генеративной рекурсии в Wikipedia мне понятно, но я смущен концепцией структурной рекурсии.

Может ли кто-нибудь объяснить, будет ли функция, вычисляющая n-й номер Фибоначчи, и функция, вычисляющая факториал от 1 до N, будет структурной или генеративной?

Ответы

Ответ 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 и т.д.) могут быть определены рекурсивно:

  • Список - это либо ничего, либо ячейка, за которой следует список.
  • Двоичное дерево либо ничего, либо node с двумя двоичными деревьями в виде дочерних элементов.

При выполнении структурной рекурсии вы "отменяете" операцию, из которой эти структуры построены друг из друга. Например, функция NumberOfNodes "отменяет" конструкцию принятия node и добавляет ее в существующий список. Оператор Find "отменяет" операцию склеивания a node на два других дерева. Поэтому легко понять, почему эти функции должны заканчиваться - в конце концов, вы "отмените" все операции, которые выполнялись для создания объекта в первую очередь, и рекурсия останавливается.

С другой стороны, рассмотрим Quicksort, который выполняет следующие действия:

  • Выберите стержень.
  • Создайте три новых списка: один из всех элементов, меньший, чем точка поворота, один из всех элементов, большего, чем точка поворота, и один из всех элементов, равный оси.
  • Рекурсивно сортировать первый и второй из этих списков.
  • Объединение списка меньших, равных и больших значений.

Здесь рекурсивные вызовы выполняются на меньших массивах, которые не были частью исходного ввода - списки должны были быть созданы из данных. (Как правило, реализация могла бы использовать пространство для этих списков, но эти подсписки не гарантировались бы непосредственно внутри входа).

Это различие размыто, когда дело доходит до натуральных чисел. Обычно натуральные числа рекурсивно определяются следующим образом:

  • 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 "вычисляется" из a и b, а не формируется путем "отмены" некоторого количества операций +1, данные генерируются.

Причина, по которой генеративная рекурсия отличается от структурной рекурсии, заключается в том, что нет гарантии, что она прекратится. Например, подумайте об этой функции:

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

Эта генерирующая рекурсивная функция никогда не заканчивается: a продолжает расти, даже если b продолжает становиться все меньше.

Честно говоря, я никогда не слышал об этом различии раньше, и я преподаю курсы по дискретной математике и программированию. Я не стал бы слишком беспокоиться об этом, если кто-то не потребует от вас знать разницу.

Надеюсь, это поможет!