(e)迭代与递归

官方信息技术老师·12 页·深入(追求细节与边界)·0 次浏览·3 天前
递归迭代对比复杂度

(e)迭代与递归

看懂递归的展开与回溯,对比迭代的差异与权衡

按 空格/→ 演示下一步

1 / 12 页

全部页面点击任意一页,跳回舞台从这页播放

递归迭代对比复杂度

(e)迭代与递归

看懂递归的展开与回溯,对比迭代的差异与权衡

1第 1 页 · (e)迭代与递归

: 迭代与递归

数一摞书:拿一本数 1,再拿一本数 2,一本一本地数;或者把问题变成"最上面一本 + 剩下那摞有多少本",对剩下那摞重复同一动作,直到剩 0 本。前者是迭代,后者就是递归。

迭代
用循环反复执行,靠变量保存当前进度(空间 O(1))
递归
函数调用自己,靠调用栈保存每层状态(空间 O(n))
终止条件
递归的基准情形,缺了它就是无限递归→栈溢出
性能取舍
递归调用有开销,深层递归易爆栈;迭代更稳
可转化性
任何递归都能用「循环+显式栈」改写为迭代
套娃对应 →递归调用

每打开一层看到更小的一层,直到最小那颗为止——对应递归不断调用更小的自己

f(n)=nf(n1),f(0)=1f(n) = n \cdot f(n-1), \quad f(0) = 1
2第 2 页 · : 迭代与递归

: 减而治之

递归能把任何问题变成「对自身的调用」,但具体怎么「变小」决定了策略。减而治之是最朴素的一条:从规模 n 退回 n-1,把子问题做完,再补回当前这步——里面藏着递归设计的整套思维。

规模减一
把规模为 n 的问题,化归为规模 n-1 的同型子问题
平凡基例
存在最小规模的问题可直接求解,否则递归永不终止
递推与回归
沿递推链递归下沉至基例,再沿回归链逐层回填子结果
复杂度通常线性
若每步代价 O(1),则 T(n)=T(n-1)+O(1)=O(n)
与分而治之的边界
每层只递归一次的是减而治之,调用 ≥2 次则是分而治之
拆一盒俄罗斯套娃对应 →减而治之的递推链

每打开一层,剩下那层同型但更小;直到最里那颗(基例),再把外壳逐个合回去(回归)

T(n)=T(n1)+O(1)    T(n)=cn+T(0)=O(n)T(n) = T(n-1) + O(1) \;\Rightarrow\; T(n) = c\cdot n + T(0) = O(n)
3第 3 页 · : 减而治之

: 递归跟踪

从「减而治之」我们把问题递归变小求解,但递归一旦深起来,脑子里就乱成一团——怎么才能看清它在做什么?这就要靠递归跟踪。

调用栈帧
每次递归调用压一帧,存参数、局部变量、返回地址
参数与返回值
沿调用链记录每层入参与返回值,定位传递方向
递归树形态
用树状图展示调用与回溯的拓扑,分支即子问题
复杂度依据
通过计数调用次数、量取最大栈深,得出时空复杂度
拆俄罗斯套娃对应 →递归跟踪

每层套娃是一次调用,逐层拆开记录编号,直到最小的不能拆的

4第 4 页 · : 递归跟踪

: 递推方程

递归跟踪让我们看清了每一次调用的开销,但调用树一旦指数级膨胀,手工追踪就不再现实。我们需要一个数学工具,把整棵调用结构压缩成一行——这就是递推方程。

定义
用自身更小规模表达自身的方程,T(n) 由 T(更小) 递推得出
一般形式
T(n)=a·T(n/b)+f(n),a 为分支数、b 为规模比、f(n) 为本层开销
边界条件
必须显式指定 T(1) 或 T(0),否则递推将无限展开
典型应用
分析递归算法复杂度,如归并排序 T(n)=2T(n/2)+O(n)
求解工具
替换法 / 递归树 / 主定理,三条路径目标都是闭合解
数楼梯走法对应 →递推方程

到第 n 级的走法 = 到 n-1 级的走法 + 到 n-2 级的走法,每级都用更小的级表达自己

T(n)=aT(n/b)+f(n),T(1)=O(1)T(n) = a \cdot T(n/b) + f(n), \quad T(1) = O(1)
5第 5 页 · : 递推方程

: 数组倒置

: 数组倒置:定义、要点与典型应用

: 数组倒置
: 数组倒置:定义、要点与典型应用
6第 6 页 · : 数组倒置

: 分而治之

上一节的减而治之,每步只把问题缩小一点点。但有些问题更顺手——一刀切成几块,各算各的,最后拼起来。这就是分而治之。

拆分(Divide)
把规模 n 的问题分成多个(约 a 个,a≥2)独立的子问题
求解(Conquer)
对每个子问题递归求解,规模约为 n/b
合并(Combine)
把各子问题的解合并为原问题的解
与减而治之的差异
拆成多个子问题(a≥2),而非只拆一个
递推形式
形如 T(n)=aT(n/b)+f(n),由 a、b、f(n) 三者刻画
拼大型乐高对应 →分而治之

按零件分包 → 各人独立拼 → 模块合装成整体

T(n)=aT ⁣(nb)+f(n),a2T(n) = a\,T\!\left(\tfrac{n}{b}\right) + f(n), \quad a \geq 2
7第 7 页 · : 分而治之

: 二分递归:数组求和

上页刚拆完分而治之的'分-解-合'骨架。这页拿最经典的例子——二分递归求数组和——把它填满。注意:每个层级同时发出两个递归调用,正是'分'字最直观的体现。

递归基
lo > hi 时返回 0,空区间无元素贡献
对半切分
取中点 mi = ⌊(lo+hi)/2⌋,切成左 [lo,mi] 和右 [mi+1,hi]
两次递归
对左右两半各发一次递归调用,'二分'即源于此
合并结果
左半和加右半和,得到整个区间的和
工头分账本对应 →二分递归求和

工头把一摞账本对半分给两个副手,副手再对半分,直到没人只能自己算,再一层层把数字加回来报给工头

sum(lo,hi)={0lo>hisum(lo,mi)+sum(mi+1,hi)mi=(lo+hi)/2sum(lo,hi) = \begin{cases} 0 & lo > hi \\ sum(lo,mi) + sum(mi+1,hi) & mi = \lfloor(lo+hi)/2\rfloor \end{cases}
8第 8 页 · : 二分递归:数组求和

二分递归:Max2

求数组最大值,最直白的做法是从头扫到尾记一个当前最大——这是迭代。Max2 则把数组对半劈开,各自递归求最大,再比一次。思路简单,但时空代价和迭代版本差别显著。

递归结构
把 [lo,hi) 一分为二 [lo,mi) 与 [mi,hi),分别递归求最大值,再取较大者
递归基
区间长度为 1 时直接返回该元素,无需再拆
递推方程
T(n) = 2T(n/2) + O(1),每层只多一次比较
复杂度对比
时间 O(n) 与迭代相同;递归栈深度 O(log n) 占用额外空间
分治范式
典型 divide-conquer:拆→治→合,此处「合」只需一次 max
淘汰赛对阵图对应 →Max2 递归树

每场比赛从两人中留下强者;递归树每层把两组的冠军再比一次,决出全局最大

T(n)=2T(n/2)+O(1)    T(n)=O(n)T(n) = 2\,T(n/2) + O(1)\;\Rightarrow\;T(n)=O(n)
9第 9 页 · 二分递归:Max2

: Max2:二分递归

上一节的数组求和,把两半递归结果直接相加;Max2 也是这个套路,但'合'起来不再是加法,而是两场比较。同一个二分递归框架,换了一种归并动作。

问题定义
在长度为 n 的数组中,同时定位最大值与次大值
递归分治
对半拆,递归求出左右两半各自的 (max, smax)
归并比较
两半合一:先比两半 max 决冠军,再让输方 max 与赢方 smax 争次大,共 2 次
复杂度
T(n)=2T(n/2)+2,解得 2n-2 次比较,与两次扫描朴素法同量级
单败淘汰赛对应 →Max2 的归并

两分区各出冠军,决出总冠军后,再从败者中选最强者为亚军

T(n)=2T(n/2)+2,T(2)=2T(n)=2\,T(n/2)+2,\quad T(2)=2
10第 10 页 · : Max2:二分递归

本节要点

  • 递归本质是问题规模的自我引用,复杂度取决于递推关系而非写法
  • 减而治之:每层减一个常数项,时间通常呈 O(n)
  • 分而治之:规模折半加合并,T(n)=aT(n/b)+f(n) 是骨架
  • 递归跟踪表把调用-返回翻译成实例树,是分析时空代价的利器
  • 同一问题可有多种递归分解,需用复杂度下界判定最优
延伸主题:尾递归与迭代改写主定理求解递推方程动态规划:递归的查表
11第 11 页 · 本节要点

课后思考

先自己想,再点开参考答案对照——每个问题都值得琢磨几分钟。

1为什么任何迭代算法都能改写为递归形式,反之亦然?转换过程中,什么变了、什么没变?

参考答案从图灵等价看,循环对应调用栈的反复进出。控制结构变了,但数据依赖没变。代价是栈空间开销,以及某些情形下编译器能否做尾递归优化。

2数组倒置用了减而治之。如果换成单链表倒置,递归结构还能照搬吗?需要调整哪里?

参考答案能照搬——把head.next指向prev,再递归处理剩余段。关键是先保存next再改指向,否则递归丢失入口。减而治之的思路没变,只是剩余部分的获取从下标变成了指针。

3二分递归Max2在n=2^k时达到3n/2-2的最优比较。当n不是2的幂时,递推方程如何改写?最优下界还成立吗?

参考答案方程改写为T(n)=T(⌊n/2⌋)+T(⌈n/2⌉)+1,主定理仍是O(n)。下界成立依赖“必须比较n-1次”这一基本事实,递归只是常数因子的优化。

12第 12 页 · 课后思考