参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
记录的是class020→class022,包括了递归的本质理解、Master公式的应用、归并排序的递归与非递归实现,以及归并分治思想在解决小和问题和翻转对问题中的应用。
020【必备】递归和Master公式
递归的本质理解
思想层面的递归
递归不是玄学,它是一种**”大事化小”**的思维方式。对于新手来说,画调用图是理解递归的关键。
1 | def max_value(arr): |
递归的三个关键点:
- 明确递归要干什么:这里是”返回 arr[l…r] 的最大值”
- 找递归的终止条件:这里是”l == r”,即区间只有一个数
- 思考如何缩小规模:这里是分别递归处理左区间和右区间
实际层面的递归
递归底层利用系统栈来实现:
- 当函数调用发生时,系统会将函数的状态(参数、局部变量、返回地址)压入栈中
- 当函数返回时,系统从栈中弹出状态,恢复到调用点继续执行
- 这个过程是可视化的,所以所有递归函数都可以改成非递归
递归改非递归的必要性:
- 工程实践:几乎一定要改,除非确定递归深度不会太大
- 算法竞赛:能通过就不改,时间紧迫时优先保证正确性
形象比喻:
- 迭代像是擂台赛:一个个来,逐步解决
- 递归像是季后赛:分组对战,逐层淘汰
/class20-%E9%80%92%E5%BD%92%E5%92%8Cmaster%E5%85%AC%E5%BC%8F/%E9%80%92%E5%BD%92%E8%BF%87%E7%A8%8B%E5%88%86%E6%AD%A5.png)
/class20-%E9%80%92%E5%BD%92%E5%92%8Cmaster%E5%85%AC%E5%BC%8F/%E9%80%92%E5%BD%92%E6%97%B6%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png)
Master公式详解
Master公式用于分析分治算法的时间复杂度,适用于所有子问题规模相同的递归。
公式形式
$$T(n) = a \times T(\frac{n}{b}) + O(n^c)$$
其中:
- a:子问题被调用的次数
- b:子问题规模(数据量变为原来的1/b)
- c:除去子问题之外的时间复杂度指数
判断标准
设 $\log_b(a) = d$,则:
| 条件 | 时间复杂度 | 说明 |
|---|---|---|
| $d < c$ | $O(n^c)$ | 合并工作量占主导 |
| $d > c$ | $O(n^d)$ | 递归调用占主导 |
| $d = c$ | $O(n^c \log n)$ | 两者平衡 |
经典例子分析
1 | # 归并排序:T(n) = 2*T(n/2) + O(n) |
特殊情况
对于 $T(n) = 2 \times T(\frac{n}{2}) + O(n \log n)$:
- 这不符合标准Master公式形式,结果是 $O(n \times (\log n)^2)$,需要特殊记忆,证明过程较复杂,这种递归式常见于“分治 + 合并时需要二分/复杂统计”的问题,比如“翻转对”、“区间对统计”,主定理告知其复杂度为 $O(n \log^2 n)$
021【必备】归并排序
归并排序原理
核心思想
- 左部分排好序、右部分排好序
- 利用merge过程让左右整体有序
- merge过程:谁小拷贝谁,直到左右两部分数字耗尽,拷贝回原数组
为什么归并排序比O(n²)排序快?
比较行为没有浪费!
对比三种原始排序(选择、冒泡、插入):
- 每次1到N-1次比较只能确定一个位置
- 大量比较工作被浪费,效率低下
归并排序中:
- 每次比较都有意义,用于合并两个有序序列
- 比较结果被充分利用,没有浪费
- 系统栈不会太深
/class20-%E9%80%92%E5%BD%92%E5%92%8Cmaster%E5%85%AC%E5%BC%8F/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E7%9A%84%E6%A0%88%E4%B8%8D%E4%BC%9A%E6%B7%B1.png)
/class20-%E9%80%92%E5%BD%92%E5%92%8Cmaster%E5%85%AC%E5%BC%8F/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F%E7%9A%84%E6%A0%88%E4%B8%8D%E4%BC%9A%E6%B7%B12.png)
测试链接 :https://www.luogu.com.cn/problem/P1177
递归实现
1 | MAXN = 100001 |
非递归实现
1 | def mergeSort2(): |
非递归实现的核心思路
- step表示每次要合并的有序段长度,初始为1(每个元素自己是有序段)
- 每一轮成对合并长度为step的有序段,合并成长度为2*step的有序段
- 下一轮step翻倍,继续两两合并
- 重复直到step >= n,整个数组有序
过程示例:
1 | 原数组: [3, 8, 7, 6, 4, 5, 1, 2] |
merge过程详解
双指针合并策略
1 | def merge(l, m, r): |
复杂度分析
时间复杂度:O(n log n)
- 递归层数:log₂(n)层,每次将问题规模减半
- 每层工作量:O(n),所有元素都要参与一次合并
- 总复杂度:O(n) × O(log n) = O(n log n)
空间复杂度:O(n)
- 辅助数组:需要与原数组等长的help_arr
- 递归栈:最大深度O(log n),但主要空间开销是辅助数组
- 原地归并:理论上可以做到O(1)空间,但时间复杂度会退化到O(n²)
022【必备】归并分治
归并分治的核心思想
归并分治是在归并排序基础上的拓展,用来解决更复杂的问题。
应用条件判断
一个问题能用归并分治解决,需要满足:
- 大范围答案 = 左部分答案 + 右部分答案 + 跨越左右产生的答案
- 计算”跨越左右产生的答案”时,左右各自有序能带来计算便利性
- 如果以上两点成立,该问题很可能被归并分治解决
求解过程:在归并排序过程中加入统计逻辑,利用左右有序的特性获得计算便利性。
小和问题
问题描述
给定数组arr,对于每个位置i,求出其左边所有小于等于arr[i]的数的累加和,所有位置的累加和即为数组的”小和”。
例子:
1 | 数组: [1, 3, 5, 2, 4, 6] |
/class22-%E5%BD%92%E5%B9%B6%E5%88%86%E6%B2%BB/%E5%88%86%E6%B2%BB%E6%8C%87%E9%92%88%E6%BB%91%E5%8A%A8.png)
/class22-%E5%BD%92%E5%B9%B6%E5%88%86%E6%B2%BB/%E5%B0%8F%E5%92%8C%E9%97%AE%E9%A2%98%E6%8B%86%E5%88%86%E6%88%90%E5%AD%90%E9%97%AE%E9%A2%98.png)
测试链接 :https://www.nowcoder.com/practice/edfe05a1d45c4ea89101d936cac32469
归并分治解法
1 | def smallSum(l, r): |
算法关键思路
为什么要在merge过程中统计?
- 左右有序的便利性:因为左右都有序,可以用双指针线性扫描
- 避免重复计算:每个跨区的小和贡献只需要计算一次
- 时间复杂度优势:总体保持O(n log n),而暴力解法是O(n²)
核心技巧:
- 对于右半部分的每个元素arr[j],左半部分所有 ≤ arr[j] 的元素都会对小和产生贡献
- 由于左半部分有序,可以用指针i从左向右扫描,累加贡献值
- 指针i只会前进不会后退,总的扫描时间为O(n)
翻转对问题
问题描述
给定数组nums,如果i<j且nums[i]>2*nums[j],我们就将(i,j)称作一个重要翻转对。求数组中翻转对的总数量。
例子:
1 | 数组: [1,3,2,3,1] |
测试链接 :https://leetcode.cn/problems/reverse-pairs/
归并分治解法
1 | def reversePairs(arr): |
算法关键思路
统计策略:
- 利用左右部分独立有序:对于左半部分的每个元素arr[i],在右半部分找到满足arr[i] > 2*arr[j]的所有j
- 指针单向移动:左右部分都是有序的,指针j只需要前进,不需要回退
- 计数技巧:当找到第一个不满足条件的j时,说明j前面的所有元素都满足条件
时间复杂度分析:
- 每个元素只会被访问一次,总的统计时间:O(n),符合归并分治的要求
归并分治总结
适用问题特征
- 可分解性:问题可以分解为左部分 + 右部分 + 跨区部分
- 有序性便利:跨区部分的计算在左右有序时能够优化
- 线性合并:跨区计算的时间复杂度为O(n)
解题模板
1 | def divide_conquer(l, r): |
常见应用
- 小和问题:统计左侧小于等于当前元素的累加和
- 翻转对问题:统计满足特定大小关系的数对
- 最近点对问题:二维空间中最近两点距离(高难度)
- 逆序对问题:统计数组中的逆序对数量
与其他算法的关系
- 线段树:也可以解决类似问题,但常数因子可能更大
- 树状数组:适合在线查询修改,离线场景下归并分治更简洁
- 分块算法:另一种分治思想,将在后续课程中介绍
归并分治是一种优雅而强大的算法思想,它将复杂问题通过分治和有序性的结合,优雅地降低了时间复杂度。掌握这种思想对于解决许多看似困难的问题都有很大帮助。