当前位置: 首页 > news >正文

【数据结构与算法】第46篇:算法思想(一):递归与分治

一、递归的本质

1.1 什么是递归

递归就是函数调用自身。一个递归函数通常包含两部分:

  • 终止条件:什么时候停止递归

  • 递推公式:如何将大问题转化为小问题

c

// 阶乘的递归实现 int factorial(int n) { if (n <= 1) return 1; // 终止条件 return n * factorial(n - 1); // 递推公式 }

1.2 递归的底层原理:系统栈

每次函数调用,系统都会在栈上分配空间,存储:

  • 函数的参数

  • 局部变量

  • 返回地址

当函数调用结束时,栈帧被弹出,程序回到调用点继续执行。

以 factorial(3) 为例

text

调用 factorial(3): 栈: [fact(3)] 调用 factorial(2): 栈: [fact(3), fact(2)] 调用 factorial(1): 栈: [fact(3), fact(2), fact(1)] 返回 1 栈: [fact(3), fact(2)] 计算 2 * 1 = 2,返回 栈: [fact(3)] 计算 3 * 2 = 6,返回 栈: []

递归深度:栈中最多同时存在的栈帧数量。深度过大(如递归10000次)会导致栈溢出

1.3 递归 vs 迭代

对比项递归迭代
代码可读性简洁直观相对复杂
空间复杂度O(递归深度)O(1)
性能函数调用开销大循环开销小
适用场景树、分治、回溯简单重复计算

二、递归的经典问题:汉诺塔

2.1 问题描述

有三根柱子(A、B、C),A柱上有n个大小不同的圆盘,大的在下,小的在上。要求把所有圆盘从A移动到C,每次只能移动一个圆盘,且大圆盘不能放在小圆盘上面。求移动步骤。

2.2 递归思路

要把n个盘子从A移到C:

  1. 先把上面 n-1 个盘子从A移到B(借助C)

  2. 把最大的盘子从A移到C

  3. 再把 n-1 个盘子从B移到C(借助A)

text

终止条件:n == 1 时,直接移动

2.3 代码实现

c

#include <stdio.h> // 汉诺塔递归实现 void hanoi(int n, char from, char to, char aux) { if (n == 1) { printf("移动 1 号盘: %c -> %c\n", from, to); return; } // 将 n-1 个盘子从 from 移到 aux hanoi(n - 1, from, aux, to); // 移动最大的盘子 printf("移动 %d 号盘: %c -> %c\n", n, from, to); // 将 n-1 个盘子从 aux 移到 to hanoi(n - 1, aux, to, from); } int main() { int n = 3; printf("汉诺塔 %d 个盘子移动步骤:\n", n); hanoi(n, 'A', 'C', 'B'); return 0; }

运行结果:

text

汉诺塔 3 个盘子移动步骤: 移动 1 号盘: A -> C 移动 2 号盘: A -> B 移动 1 号盘: C -> B 移动 3 号盘: A -> C 移动 1 号盘: B -> A 移动 2 号盘: B -> C 移动 1 号盘: A -> C

复杂度:移动次数 = 2ⁿ - 1,时间复杂度 O(2ⁿ)


三、分治法(Divide and Conquer)

3.1 分治法的三步骤

分治法将大问题分解成若干个相同的小问题,递归解决后合并结果。

步骤说明
分解将原问题分解成若干个子问题
解决递归解决子问题(子问题足够小时直接解决)
合并将子问题的解合并成原问题的解

3.2 典型应用

算法分解合并时间复杂度
归并排序分成两半合并两个有序数组O(n log n)
快速排序按基准分区不需要合并O(n log n)
二分查找取中间点不需要合并O(log n)
汉诺塔分成n-1和1简单组合O(2ⁿ)
最大子数组分成左右和跨中取最大值O(n log n)

3.3 归并排序(分治法经典)

c

#include <stdio.h> #include <stdlib.h> // 合并两个有序子数组 void merge(int arr[], int left, int mid, int right, int temp[]) { int i = left, j = mid + 1, k = left; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; } } while (i <= mid) temp[k++] = arr[i++]; while (j <= right) temp[k++] = arr[j++]; for (i = left; i <= right; i++) { arr[i] = temp[i]; } } // 归并排序(分治) void mergeSort(int arr[], int left, int right, int temp[]) { if (left >= right) return; // 终止条件:只有一个元素 int mid = left + (right - left) / 2; // 分解 mergeSort(arr, left, mid, temp); mergeSort(arr, mid + 1, right, temp); // 合并 merge(arr, left, mid, right, temp); } int main() { int arr[] = {8, 3, 5, 1, 6, 2, 7, 4}; int n = sizeof(arr) / sizeof(arr[0]); int *temp = (int*)malloc(n * sizeof(int)); printf("原数组: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); mergeSort(arr, 0, n - 1, temp); printf("排序后: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); free(temp); return 0; }

四、递归的优化:尾递归与记忆化

4.1 尾递归

尾递归是指递归调用是函数的最后一个操作,编译器可以优化为迭代,避免栈溢出。

c

// 普通递归阶乘(不是尾递归,因为最后还有乘法) int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); } // 尾递归阶乘 int factorialTail(int n, int result) { if (n <= 1) return result; return factorialTail(n - 1, n * result); }

4.2 记忆化递归(避免重复计算)

斐波那契数列的普通递归有大量重复计算,用记忆化优化。

c

#define MAX 100 int memo[MAX]; int fib(int n) { if (n <= 1) return n; if (memo[n] != 0) return memo[n]; memo[n] = fib(n - 1) + fib(n - 2); return memo[n]; }
版本时间复杂度说明
普通递归O(2ⁿ)大量重复计算
记忆化递归O(n)每个数只算一次
迭代O(n)最优

五、递归的常见陷阱

5.1 栈溢出

递归深度过大时,系统栈空间耗尽。

c

// 递归深度10000,可能导致栈溢出 void deepRecursion(int n) { if (n <= 0) return; deepRecursion(n - 1); }

解决方案

  • 改用迭代

  • 增加栈大小(编译器选项)

  • 使用尾递归(部分编译器优化)

5.2 重复计算

斐波那契普通递归的重复计算问题。

5.3 终止条件错误

忘记终止条件或条件错误会导致无限递归。

c

// 错误:n==0时没有正确返回 int badFactorial(int n) { return n * badFactorial(n - 1); // n=0时永远不停止 }

六、递归与分治的应用场景

场景推荐原因
树/图的遍历递归结构天然适合递归
排序(归并/快排)分治经典应用
二分查找递归/迭代均可简单
动态规划递归+记忆化自顶向下思考
回溯(八皇后、迷宫)递归需要状态恢复
分治(最大子数组)递归分解合并清晰

七、小结

这一篇我们学习了递归与分治:

概念核心要点
递归本质系统栈的压入与弹出
递归要素终止条件 + 递推公式
汉诺塔经典递归问题,O(2ⁿ)
分治法分解 → 解决 → 合并
归并排序分治法的典型应用
尾递归可被编译器优化为循环
记忆化避免重复计算

递归思考模板

text

function(问题): if (问题足够小): 直接解决 else: 分解成子问题 递归解决子问题 合并子问题的解

下一篇我们讲动态规划。


八、思考题

  1. 递归函数的空间复杂度为什么等于递归深度?

  2. 汉诺塔问题中,移动 n 个盘子需要多少步?为什么?

  3. 如何判断一个问题适合用递归解决?递归的缺点是什么?

  4. 用递归实现二分查找,并分析其空间复杂度。

欢迎在评论区讨论你的答案。

http://www.jsqmd.com/news/642020/

相关文章:

  • AIAgent音乐创作革命(2026奇点大会闭门报告首曝):LLM+Audio Diffusion+实时乐理校验三引擎协同架构解密
  • 从645到698:智能电表通信协议升级,开发者需要知道的那些坑
  • 避坑指南:ESP8266连接心知天气API常见问题解析(含ArduinoJson6配置技巧)
  • 别再只用默认样式了!深度解析QToolButton的popupMode与toolButtonStyle组合玩法
  • 终极免费指南:如何一键检测微信单向好友并清理无效社交关系
  • 微信小程序的英语在线学习系统每日签到打卡
  • Nano-Banana提示词工程:如何获得最佳拆解图效果
  • 一条命令部署OpenClaw?PPClaw的便利背后,藏着哪些成本与边界
  • 动态规划专题(05):区间动态规划实践(乘法游戏)
  • 干了3年Java,我用AI编程多赚了两个月工资:真实经历分享
  • IgH EtherCAT 从入门到精通:第 3 章 第一次运行 Hello EtherCAT
  • ​2026年冲刺高新认定东莞这片科创热土靠谱的服务商都藏在哪里 - 沐霖信息科技
  • 2026年降AI工具三款横评:嘎嘎降AI、去i迹、比话实测对比
  • 2026年4月新发布:江苏内河码头服务商综合评估与推荐 - 2026年企业推荐榜
  • 在线电脑摄像头测试
  • Wan2.2-I2V-A14B学术研究:探索其在操作系统概念教学可视化中的应用
  • HJ177 可匹配子段计数
  • 从零开始:NVIDIA显卡驱动与CUDA环境搭建全攻略(附常见问题解决)
  • 终极抢票指南:3分钟学会用biliTickerBuy轻松抢到B站会员购限量商品
  • 深度学习正则化 —— 控制容量的实战武器库(十七)
  • 2026年至今河北白酒市场激变:销售公司如何破局选对“硬核”供应商? - 2026年企业推荐榜
  • 郭老师-抓住风口,重构自我
  • 昆仑通态触摸屏进阶开发技巧~2025.5.20
  • 如何利用ViGEmBus虚拟手柄驱动实现Windows游戏控制器完美兼容
  • 知识图谱-Neo4j实战指南:从安装到应用开发
  • 今天不看就淘汰:2026奇点大会定义的图像描述生成新标准——多轮指代理解、跨模态因果推理、可控细粒度生成,你达标了吗?
  • Fiji图像处理平台:从零开始掌握科研级图像分析
  • 如何用ncmdumpGUI将网易云音乐NCM文件转换为通用音频格式
  • STM32 RTC实战:从零构建高精度实时时钟系统
  • 郭老师-百年大变局中的学习力觉醒