《算法设计与分析》第二章:递归

2.1 关于递归

2.1.1 递归的定义

  • 在定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归
  • 若调用自身,称之为直接递归
  • 若过程或函数p调用过程或函数q,而q又调用p,称之为间接递归
  • 任何间接递归都可以等价地转换为直接递归。
  • 如果一个递归过程或递归函数中递归调用语句是最后一条执行语
    句,则称这种递归调用为尾递归

例:
在这里插入图片描述

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

在该函数fun(n)求解过程中,直接调用fun(n-1)(语句4)自身,所以它是一个直接递归函数。又由于递归调用是最后一条语句,所以它又属于尾递归

2.1.2 何时使用递归

在以下三种情况下,常常要用到递归的方法。

  • 定义是递归的
  • 数据结构是递归的
  • 问题的求解方法是递归的

定义是递归的

有许多数学公式、数列等的定义是递归的。例如,求n!
和Fibonacci数列等。这些问题的求解过程可以将其递归定
义直接转化为对应的递归算法。
例如:
在这里插入图片描述

数据结构是递归的

有些数据结构是递归的。例如单链表就是一种递归数据结构,其
结点类型声明如下:

typedef struct LNode 
{ ElemType data;
 struct LNode *next; 
} LinkLis;

在这里插入图片描述

结构体LNode的定义中用到了它自身,即指针域next是一种指向自身类型的指针,所以它是一种递归数据结构

例如:

  • 求一个不带头结点的单链表L的所有data域(假设为int型)之和的递归
    算法如下:
int Sum(LinkList *L)
{ if (L==NULL)
    return 0;
  else 
    return(L->data+Sum(L->next));
}
  • 分析二叉树的二叉链存储结构的递归性,设计求非空二
    叉链bt中所有结点值之和的递归算法,假设二叉链的data域为int型。

    在这里插入图片描述

问题求解方法是递归的

汉诺塔(Hanoi)问题求解

在这里插入图片描述
三阶汉诺塔问题解题步骤
在这里插入图片描述

问题解法:

当n=1时,只要将编号为1的圆盘从柱子A直接移到柱子C上即可。
当n>1时,就需要借助另外一根柱子来移动。将n个圆盘由A移到C上可以分解为以下几个步骤:

(1)  将A柱子上的n-1个圆盘借助C柱子移到B柱子上;

(2)  把A柱子上剩下的一个圆盘从A柱子移到C柱子上;

(3)  最后将剩下的n-1个圆盘借助A柱子从B柱子移到C柱子上。

在这里插入图片描述

#include <stdio.h>

void move(int numberOfDisks, char start, char destination) {
    printf("将 %d 号盘子从 %c 柱移到 %c 柱\n", numberOfDisks, start, destination);
}

void hanoi(int numberOfDisks, char start, char spare, char destination) {
    if (numberOfDisks == 1) {
        move(1, start, destination);
        return;
    }
    hanoi(numberOfDisks - 1, start, destination, spare);
    move(numberOfDisks, start, destination);
    hanoi(numberOfDisks - 1, spare, start, destination);
}

int main() {
    int numberOfDisks;
    printf("请输入盘子数量: ");
    scanf("%d", &numberOfDisks);
    hanoi(numberOfDisks, 'A', 'B', 'C');
    return 0;
}

在这里插入图片描述
hanoi 函数的实现:

  • 如果只有一个盘子需要移动,直接调用 move 函数打印移动步骤即可。
  • 如果有多个盘子,则先将上面的 numberOfDisks - 1 个盘子从 start 移动到 spare柱子上,然后将最大的盘子从 start 移动到 destination柱子上,最后再将 spare 柱子上的 numberOfDisks - 1 个盘子移动到 destination 柱子上。这就是汉诺塔问题的典型递归解法。

2.1.3 递归模型

递归模型是递归算法的抽象,它反映一个递归问题的递归结构。
在这里插入图片描述
在这里插入图片描述

2.1.4 递归算法的执行过程

  • 一个正确的递归程序虽然每次调用的是相同的子程序,但它的参量、输入数据等均有变化。
  • 在正常的情况下,随着调用的不断深入,必定会出现调用到某一层的函数时,不再执行递归调用而终止函数的执行,遇到递归出口便是这种情况。
  • 系统为每一次调用开辟一组存储单元,用来存放本次调用的返回
    地址以及被中断的函数的参量值。
    - 这些单元以系统栈的形式存放,每调用一次进栈一次,当返回时
  • 执行出栈操作,把当前栈顶保留的值送回相应的参量中进行恢复,并按栈顶中的返回地址,从断点继续执行。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

递归调用的实现是分两步进行:

  1. 第一步是分解过程,即用递归体将“大问题”分解成“小问题”,直到
    递归出口为止;
  2. 第二步的求值过程,即已知“小问题”,计算“大问题”。前面的
    fun(5)求解过程如下所示。
    在这里插入图片描述

2.2 递归算法设计

2.2.1 递归算法设计的一般步骤

  1. 先找出 递归模型
  2. 转换为对应的C/C++语言函数
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>

int fmax(int a[], int n) {
    if (n == 1) {
        return a[0];
    } else {
        return max(a[n-1], fmax(a, n-1));
    }
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int size = sizeof(arr) / sizeof(arr[0]);
    int max = fmax(arr, size);
    printf("The maximum element is: %d\n", max);
    return 0;
}

在这里插入图片描述

#include <stdio.h>

int Max(int A[], int i, int j) {
    if (i == j) {
        return A[i];
    } else {
        int mid = (i + j) / 2;
        int max1 = Max(A, i, mid);
        int max2 = Max(A, mid + 1, j);
        return (max1 > max2) ? max1 : max2;
    }
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int size = sizeof(arr) / sizeof(arr[0]);
    int max = Max(arr, 0, size - 1);
    printf("The maximum element is: %d\n", max);
    return 0;
}

2.2.2 递归数据结构及其递归算法设计

1. 递归数据结构的定义
采用递归方式定义的数据结构称为递归数据结构

在递归数据结构定义中包含的递归运算称为基本递归运算。

2. 基于递归数据结构的递归算法设计

  • 1)单链表的递归算法设计

【例1】有一个不带头结点的单链表L,设计一个算法释放其中所有结点。
在这里插入图片描述
【例2】设L为不带头结点的单链表,实现从尾到头反向输出链表中
每个结点的值。
在这里插入图片描述

2)二叉树的递归算法设计

【例】对于含n(n>0)个结点的二叉树,所有结点值为int
类型,设计一个算法由其先序序列a和中序序列b创建对应的二叉
链存储结构。
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

typedef char ElemType;
typedef struct BTNode {
    ElemType data;
    struct BTNode *lchild, *rchild;
} BTNode;

BTNode *CreateBTree(ElemType pre[], ElemType in[], int n) {
    int k;
    if (n <= 0) return NULL;
    ElemType root = pre[0]; // 根结点值
    BTNode *bt = (BTNode *)malloc(sizeof(BTNode));
    bt->data = root;
    for (k = 0; k < n; k++) // 在in中查找in[k]=root的根结点
        if (in[k] == root)
            break;
    bt->lchild = CreateBTree(pre + 1, in, k); // 递归创建左子树
    bt->rchild = CreateBTree(pre + k + 1, in + k + 1, n - k - 1); // 递归创建右子树
    return bt;
}

int main() {
    ElemType pre[] = {'A', 'B', 'D', 'E', 'C', 'F'};
    ElemType in[] = {'D', 'B', 'E', 'A', 'F', 'C'};
    BTNode *root = CreateBTree(pre, in, 6);
    // 您可以在这里添加遍历二叉树的代码进行测试
    return 0;
}

2.3 递归算法设计示例

青蛙跳台阶

问题描述
青蛙跳台阶问题:一只青蛙一次可以跳上1级台阶,也可以
跳上2级。编写代码求青蛙跳上一个n级的台阶,总共有多少
种跳法?

思路:
如果青蛙第一次跳 1 级台阶,那么剩下 n-1 级台阶的跳法数为 f(n-1);
如果青蛙第一次跳 2 级台阶,那么剩下 n-2 级台阶的跳法数为 f(n-2);
所以 f(n) = f(n-1) + f(n-2)。

#include <stdio.h>

int frog_steps(int n) {
    if (n == 1) {
        return 1;
    } else if (n == 2) {
        return 2;
    } else {
        return frog_steps(n - 1) + frog_steps(n - 2);
    }
}

int main() {
    int n;
    printf("Enter the number of steps for the frog: ");
    scanf("%d", &n);
    printf("The number of ways for the frog to climb %d steps is: %d\n", n, frog_steps(n));
    return 0;
}

这个函数使用递归的方式实现了动态规划的思想。我们可以看到,当 n 较大时,这个递归实现效率会较低,因为会有大量重复计算。

为了提高效率,我们可以使用一个数组来存储中间结果,这样就不需要重复计算了。使用一个数组 dp 来存储中间结果。dp[i] 表示青蛙跳上 i 级台阶的总跳法数。我们先初始化 dp[1] 和 dp[2],然后使用动态规划的方法计算出 dp[3] 到 dp[n]。最后返回 dp[n] 作为最终结果。

下面是改进后的代码:

#include <stdio.h>

int frog_steps(int n) {
    int dp[n + 1];
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

int main() {
    int n;
    printf("Enter the number of steps for the frog: ");
    scanf("%d", &n);
    printf("The number of ways for the frog to climb %d steps is: %d\n", n, frog_steps(n));
    return 0;
}

瓷砖覆盖问题

问题描述:
编写代码求用2╳1小矩形横着或竖着去覆盖2╳n的大矩形。输
出总共有多少种覆盖方法。
在这里插入图片描述
分析:

  • 我们先把2xn的覆盖方法记为f(n)。
  • 用第一个1x2小矩阵覆盖大矩形的最左边时有两个选择,竖着放或者横着放。
  • 当竖着放的时候,右边还剩下2x(n-1)的区域,这种情况下的覆盖方法记为f(n-1)。
  • 横着放的时候。当1x2的小矩形横着放在左上角的时候,左下角也必须横着放一个1x2的小矩形,而在右边还剩下2x(n-2)的区域,这种情况下的覆盖方法记为f(n-2)。
#include <stdio.h>

int cover_rectangle(int n) {
    if (n == 1) {
        return 1;
    } else if (n == 2) {
        return 2;
    } else {
        return cover_rectangle(n - 1) + cover_rectangle(n - 2);
    }
}

int main() {
    int n;
    printf("Enter the width of the rectangle: ");
    scanf("%d", &n);
    printf("The number of ways to cover the %d x 2 rectangle is: %d\n", n, cover_rectangle(n));
    return 0;
}

改进,使用数组记录数据,提高效率

#include <stdio.h>

int cover_rectangle(int n) {
    int dp[n + 1];
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

int main() {
    int n;
    printf("Enter the width of the rectangle: ");
    scanf("%d", &n);
    printf("The number of ways to cover the %d x 2 rectangle is: %d\n", n, cover_rectangle(n));
    return 0;
}

全排列问题

#include <stdio.h>
#include <stdlib.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void Perm(int list[], int k, int m) {
    if (k == m) { // 找到一个完整的排列
        for (int i = 0; i <= m; i++) {
            printf("%d ", list[i]);
        }
        printf("\n");
    } else {
        for (int j = k; j <= m; j++) {
            swap(&list[k], &list[j]); // 交换第 k 个元素和第 j 个元素
            Perm(list, k + 1, m); // 递归处理剩余元素
            swap(&list[k], &list[j]); // 交换回原来的位置
        }
    }
}

int main() {
    int n;
    printf("请输入数组的长度: ");
    scanf("%d", &n);

    int list[n];
    printf("请输入数组元素: ");
    for (int i = 0; i < n; i++) {
        scanf("%d", &list[i]);
    }

    printf("全排列结果如下:\n");
    Perm(list, 0, n - 1);

    return 0;
}

Perm 函数:
这个函数实现了递归全排列的核心逻辑。
它有三个参数:
list: 要排列的数组
k: 当前要处理的起始索引
m: 数组的结束索引
首先,我们检查当前递归是否已经到达数组末尾(k == m)。如果是,则说明我们已经找到了一个完整的排列,输出结果。
如果还没有到达数组末尾,我们需要生成从 k 到 m 的所有可能排列:
我们遍历从 k 到 m 的所有元素。
对于每个元素 list[j],我们将其与 list[k] 交换,模拟将 list[j] 放在第 k 个位置。
然后递归调用 Perm(list, k+1, m),处理剩余的元素。
最后,我们再将 list[k] 与 list[j] 交换回去,保持数组的原始顺序,继续处理下一个元素。

整数划分问题

在这里插入图片描述
分析:以f(6,4)为例
在这里插入图片描述

正整数n的划分算法

#include <stdio.h>

int split(int n, int m) {
    if (n == 1 || m == 1) {
        return 1;
    } else if (n < m) {
        return split(n, n);
    } else if (n == m) {
        return split(n, n - 1) + 1;
    } else {
        return split(n, m - 1) + split(n - m, m);
    }
}

int main() {
    int n;
    printf("请输入一个正整数 n: ");
    scanf("%d", &n);

    int result = split(n, n);
    printf("正整数 %d 的划分数为: %d\n", n, result);

    return 0;
}

递归的简单选择排序

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void SelectSort(int a[], int i, int n) {
    int j, k;
    if (i == n) // 满足递归出口条件
        return;
    else {
        k = i; // k 记录 a[i..n] 中最小元素的下标
        for (j = i + 1; j <= n; j++) // 在 a[i..n] 中找最小元素
            if (a[j] < a[k])
                k = j;
        if (k != i) // 若最小元素不是 a[i]
            swap(&a[i], &a[k]); // a[i] 和 a[k] 交换
        SelectSort(a, i + 1, n); // 递归处理剩余元素
    }
}

int main() {
    int n, i;
    printf("请输入数组长度: ");
    scanf("%d", &n);
    int a[n];
    printf("请输入数组元素:\n");
    for (i = 1; i <= n; i++)
        scanf("%d", &a[i]);

    SelectSort(a, 1, n);

    printf("排序后的数组:\n");
    for (i = 1; i <= n; i++)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

递归的冒泡排序

#include <stdio.h>
#include <stdbool.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void BubbleSort(int a[], int i, int n) {
    int j;
    bool exchange;
    if (i == n) // 满足递归出口条件
        return;
    else {
        exchange = false; // 置 exchange 为 false
        for (j = 1; j <= n - i; j++) {
            if (a[j] > a[j + 1]) { // 当相邻元素反序时
                swap(&a[j], &a[j + 1]);
                exchange = true; // 发生交换置 exchange 为 true
            }
        }
        if (exchange == false) // 未发生交换时直接返回
            return;
        else // 发生交换时继续递归调用
            BubbleSort(a, i + 1, n);
    }
}

int main() {
    int n, i;
    printf("请输入数组长度: ");
    scanf("%d", &n);
    int a[n + 1]; // 使用 1 作为数组起始下标
    printf("请输入数组元素:\n");
    for (i = 1; i <= n; i++)
        scanf("%d", &a[i]);

    BubbleSort(a, 1, n);

    printf("排序后的数组:\n");
    for (i = 1; i <= n; i++)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

2.4 递归算法转化非递归算法

在这里插入图片描述

2.5 递归算法分析

当一个算法包含对自身的递归调用过程时,该算法的运行时间复
杂度可用递归方程进行描述,求解该递归方程,可得到对该算法
时间复杂度的函数度量。

在这里插入图片描述

递归方程求解

(1)替换法

替换方法的最简单方式为:根据递归规律,将递
归公式通过方程展开、反复代换子问题的规模变量,
通过多项式整理,如此类推,从而得到递归方程的解。

在这里插入图片描述

在这里插入图片描述
总结:
在这里插入图片描述

(2)用特征方程求解递归方程

在这里插入图片描述
在这里插入图片描述
特征方程的k个根不同:
在这里插入图片描述
特征方程k个根有重根
在这里插入图片描述

前面2种情况下的c1,c2,…,ck均为待定系数; 将初始条件代入,建立联立方程,确定各个系数具体值, 得到通解f(n)。

  • 例【1】:
    在这里插入图片描述
    在这里插入图片描述
    ·
  • 例【2】:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

(3)递归树法

  • 用递归树求解递归方程的基本过程是:
    ① 展开递归方程,构造对应的递归树。
    ② 把每一层的时间进行求和,从而得到算法时间复
    杂度的估计

    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此处在这里插入图片描述

(4)主方法

在这里插入图片描述
应用方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值