算法基础(十一)—— 递归树如何看懂分治算法的运行时间

1. 定位导航

前面已经学习了分治思想:

text 复制代码
分解 → 解决 → 合并

分治算法经常可以写成递归式。

例如归并排序:

text 复制代码
先把数组拆成左右两半;
分别排序左右两半;
再合并两个有序数组。

它的运行时间可以粗略写成:

T(n)=2T(n/2)+n T(n) = 2T(n/2) + n T(n)=2T(n/2)+n

这里的意思是:

  • 2T(n/2):两个规模为 n/2 的子问题;
  • + n:合并两个有序数组需要线性时间。

问题是:这个递归式最终是多少?

递归树就是一种非常直观的分析方法。

2. 概念术语

术语 定义 举例
递归树 把递归调用展开成树状结构 根节点是原问题
根节点 原始问题 规模为 n 的排序问题
子节点 递归产生的子问题 两个规模为 n/2 的问题
层数 递归展开的深度 每次折半,大约 log n
每层成本 同一层所有节点成本之和 归并排序每层约为 n
叶子节点 递归基对应的问题 规模为 1 的子数组
总成本 所有层成本之和 n + n + ... + n

关键澄清:

  1. 递归树不是执行流程图,而是成本分析图。
  2. 每个节点代表一个子问题的工作量。
  3. 分析时不能只看树有多深,还要看每一层总成本。
  4. 最后要把所有层的成本加起来。

3. 什么是递归树

递归树就是把递归式不断展开。

比如:

T(n)=2T(n/2)+n T(n) = 2T(n/2) + n T(n)=2T(n/2)+n

可以理解为:

text 复制代码
一个规模 n 的问题
拆成两个规模 n/2 的问题
每个 n/2 又继续拆成两个 n/4
直到规模变成 1

递归树的核心用途是:

text 复制代码
把递归调用的总成本,拆成一层一层来统计。

4. 用递归树分析归并排序

归并排序的递归式是:

T(n)=2T(n/2)+n T(n) = 2T(n/2) + n T(n)=2T(n/2)+n

递归树如下:

观察这棵树:

第 0 层

只有一个问题,规模是 n

合并成本是:

n n n

第 1 层

有两个问题,每个规模是 n/2

这一层总成本是:

n/2+n/2=n n/2 + n/2 = n n/2+n/2=n

第 2 层

有四个问题,每个规模是 n/4

这一层总成本是:

4×n/4=n 4 \times n/4 = n 4×n/4=n

可以发现:

text 复制代码
每一层总成本都是 n。

5. 动态推演:递归树如何逐层展开

下面用动态图看递归树展开过程。

展开过程可以理解为:

  1. 根节点表示原问题,规模是 n
  2. 第一层拆成两个 n/2
  3. 第二层拆成四个 n/4
  4. 一直拆到规模为 1 的叶子;
  5. 每一层总成本约为 n
  6. 层数约为 log n

6. 每层成本如何计算

对于归并排序:

k 层有:

2k 2^k 2k

个子问题。

每个子问题规模是:

n2k \frac{n}{2^k} 2kn

所以第 k 层总成本是:

2k×n2k=n 2^k \times \frac{n}{2^k} = n 2k×2kn=n

这就是为什么归并排序每一层总成本都约为 n

7. 总成本如何求和

既然每层成本都是 n,那只需要知道有多少层。

每次问题规模折半:

text 复制代码
n → n/2 → n/4 → n/8 → ... → 1

要折半多少次才能到 1?

答案大约是:

log⁡2n \log_2 n log2n

所以总成本是:

n+n+n+⋯+n n + n + n + \cdots + n n+n+n+⋯+n

一共有约 log n 层,因此:

T(n)=O(nlog⁡n) T(n) = O(n \log n) T(n)=O(nlogn)

这就是递归树分析的核心套路:

text 复制代码
总成本 = 每层成本 × 层数

8. 常见递归式的递归树直觉

递归树不仅能分析归并排序,还能帮助理解很多递归式。

8.1 T(n)=T(n/2)+1

每层只递归到一个子问题,每层额外成本是常数。

层数是 log n,所以总成本是:

O(log⁡n) O(\log n) O(logn)

典型例子是二分查找。

8.2 T(n)=2T(n/2)+n

每层总成本是 n,层数是 log n

所以总成本是:

O(nlog⁡n) O(n \log n) O(nlogn)

典型例子是归并排序。

8.3 T(n)=2T(n/2)+1

每个节点成本是常数,但节点数量会越来越多。

整棵树节点数大约是:

O(n) O(n) O(n)

所以总成本是:

O(n) O(n) O(n)

8.4 T(n)=4T(n/2)+n

每层子问题数量增长更快。

叶子层成本会变得非常大,通常最后得到平方级:

O(n2) O(n^2) O(n2)

9. 代码实践:打印递归层级成本

下面用 Python 模拟归并排序的递归层级成本。

python 复制代码
def recursion_tree_cost(n):
    level = 0
    size = n
    nodes = 1

    while size >= 1:
        cost_per_node = size
        level_cost = nodes * cost_per_node

        print(
            f"第 {level} 层: "
            f"节点数={nodes:<4} "
            f"单节点规模={size:<4} "
            f"本层成本={level_cost}"
        )

        if size == 1:
            break

        level += 1
        nodes *= 2
        size //= 2


recursion_tree_cost(16)

可能输出:

text 复制代码
第 0 层: 节点数=1    单节点规模=16   本层成本=16
第 1 层: 节点数=2    单节点规模=8    本层成本=16
第 2 层: 节点数=4    单节点规模=4    本层成本=16
第 3 层: 节点数=8    单节点规模=2    本层成本=16
第 4 层: 节点数=16   单节点规模=1    本层成本=16

可以看到,每层成本都是 16。

如果输入规模是 16,层数是:

log⁡216=4 \log_2 16 = 4 log216=4

算上叶子层,总共大约 5 层。

所以总成本接近:

text 复制代码
16 × 5

这和 n log n 的直觉是一致的。

10. 常见误区

误区一:只看递归深度

递归深度只是层数,不是总成本。

还要看每一层做多少工作。

误区二:只看节点数量

节点数量多,不一定每层成本就大。

因为子问题规模也在变小。

误区三:忘记叶子层成本

有些递归式中,叶子层可能贡献主要成本。

不能随便忽略。

误区四:层数算错

如果问题规模每次折半,层数通常是:

O(log⁡n) O(\log n) O(logn)

不是 O(n)

11. 现代延伸

递归树分析在实际工程里也有很多影子。

场景 递归树视角
归并排序 每层合并成本相同
二分查找 每层只保留一半问题
快速排序 递归树是否平衡决定性能
并行任务拆分 任务树的深度和每层负载决定吞吐
MapReduce 分片处理再汇总,类似多层聚合
大文件归并 多路归并可以看成分层合并
分布式查询 子查询树决定整体成本

比如快速排序,如果每次划分都很均匀,递归树比较平衡,性能通常很好。

但如果每次都极端不均匀,递归树会退化成链状结构,运行时间就会变差。

这就是为什么递归树不仅能分析"总成本",也能帮助我们观察算法是否稳定。

12. 思考题

  1. 为什么归并排序每一层总成本都是 n
  2. 为什么问题每次折半时,层数是 log n
  3. T(n)=T(n/2)+1 为什么是 O(log n)
  4. T(n)=2T(n/2)+1 为什么是 O(n)
  5. 快速排序在极端不平衡时,递归树会变成什么形状?

13. 本篇小结

本篇讲清楚了递归树分析方法。

核心结论是:

  • 递归树把递归式展开成层级结构;
  • 每个节点代表一个子问题的成本;
  • 每一层的节点成本相加,就是这一层的总成本;
  • 所有层成本相加,就是递归算法总成本;
  • 归并排序的递归式是 T(n)=2T(n/2)+n
  • 它每层成本约为 n,层数约为 log n
  • 所以总复杂度是 O(n log n)

以后看到递归式,可以先尝试问三个问题:

text 复制代码
每层有多少个子问题?
每个子问题规模是多少?
所有层加起来是多少?

这就是递归树分析的主线。

相关推荐
weixin_5386019710 分钟前
智能体测开Day56
java
杨丰玮41825 分钟前
从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(五)
java·python·游戏·游戏引擎·图形渲染·动画·贴图
莫得感情 o1 小时前
踩坑 - 压测三轮后 502:一个默认 10 的连接池如何拖垮整个服务
java
Escalating_xu1 小时前
【Linux线程】线程控制全解析:终止、join/detach、cancel、线程栈与 NPTL(下篇)
java·linux·运维
happyprince1 小时前
03-深刻观-CodeX哲学与升华(源码)
算法·ai编程
不会打球的王子1 小时前
Day 27:迁移学习与微调 — 站在巨人的肩膀上
算法
2602_959960921 小时前
电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析
java·jvm·spring boot·redis·面试题
吃饱了得干活1 小时前
限界上下文之后:微服务怎么拆、上下文怎么聊?
java·后端·架构
yngsqq2 小时前
窗体快速取消
java·开发语言
岁月如歌77862 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构