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

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 复制代码
每层有多少个子问题?
每个子问题规模是多少?
所有层加起来是多少?

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

相关推荐
郝学胜-神的一滴1 小时前
二叉树与递归:解锁高级数据结构的编程内功心法
开发语言·数据结构·c++·算法·面试
程序员三明治1 小时前
【AI】一文讲清 RAG:从大模型局限到企业级知识库落地流程
java·人工智能·后端·ai·大模型·llm·rag
Devin~Y1 小时前
大厂 Java 面试实录:Spring Boot/Cloud、Kafka、Redis、JVM、K8s、RAG 一条龙(小Y翻车版)
java·jvm·spring boot·redis·spring cloud·kafka·kubernetes
无限进步_1 小时前
【C++】深入右值引用:移动语义与完美转发
java·开发语言·c++
霑潇雨1 小时前
原生 Zookeeper 实现分布式锁案例
java·分布式·zookeeper·云原生·maven
小王C语言1 小时前
【线程同步与互斥】:互斥量(锁)、条件变量(唤醒等待线程)、生产者消费者模型
java·开发语言
我命由我123451 小时前
Jetpack Compose - 设置 Compose 编译器、设置 Compose 依赖项
android·java·java-ee·kotlin·android jetpack·android-studio·android runtime
csdn_aspnet1 小时前
C++ (Naive Partition Algorithm)朴素划分算法
数据结构·c++·算法
eggrall1 小时前
找到字符串中所有字母异位词(medium)
算法·leetcode·职场和发展