递归¶
对于大多数人来说,递归非常难以理解,因为人脑的限制,当我们思考递归过程时,非常容易“爆栈”。不过,我们可以通过下地下室取东西来类比递归:
- 我们从地面开始下地下室,每到达一层都有 3 种选择:
- 把东西打包好带上 → 前序遍历(已知当前层,未知后续层)
- 先把东西打包好,等到返回地面时再带走 → 中序遍历(仅限 BST)
- 在触底返回时打包带走 → 后序遍历(已知所有走过的路)
- 触底时开始返回
- 返回地面时,我们取回所有所需物品,递归结束。
在这个过程中,我们在每层重复的动作就是递归中的最小重复单元(这就是我们编写的递归代码),而触底返回,就是递归的终止条件。所以我们编写递归时,只要抓住了最小重复单元和终止条件,递归就迎刃而解了。
如何写出递归?¶
递归的本质是将大问题拆解为相似的小问题。编写递归时,可以遵循以下步骤:
- 确定递归函数的输入和输出(函数签名)
- 确定终止条件(Base Case)
- 链表 / 树:节点为
None或null时 - 数组 / 字符串:索引超出边界(如
low > high),或者长度为 0 - 数值计算:
n == 0或n == 1时
- 链表 / 树:节点为
- 确定最小重复单元:
- 递推关系:如何将问题拆解为子问题
- 处理逻辑:在递归调用前/后做什么(前序 vs 后序)
Tip
只要递归状态从 0 开始,数组就要多开 1 个以避免越界访问,如动态规划和前缀和。
经典题目¶
实战应用:扁平列表转树形结构¶
在实际业务中,数据库通常以扁平结构存储层级数据(如省市区、部门组织),每条记录通过 parent_id 指向父节点。API 层需要将其重组为树形结构以供前端渲染。这是递归在工程中最典型的应用场景之一。
数据结构定义如下:
测试数据:
方法一:递归(\(O(n^2)\))¶
思路直观:对每个节点,递归地从整个列表中筛选出其子节点,直到叶子节点为止(Base Case:当前节点无子节点,返回空切片)。
方法二:哈希表(\(O(n)\))¶
递归方案的瓶颈在于:每处理一个节点都要线性扫描整个列表。优化思路是先用哈希表建立 ID → 节点 的映射,将子节点查找从 \(O(n)\) 降为 \(O(1)\),整体只需两次线性遍历。
