之前刷算法题, 刷到递归的问题经常懵逼, 以前看b站递归教学视频经常说是递归就按公式来做, 不要想太多, 但是我本身就是比较爱钻牛角尖的人, 学东西希望能理解透彻.
所以一直没用那个递归公式, 导致做递归的时候一直很懵逼, 最近刷题的时候实在想不出来就直接用公式做了提交到Leetcode上竟然真通过了.
由此我对这个算法的原理产生了兴趣, 思考了一下这个递归公式为什么这么神奇, 明白了原理, 以后碰到递归用这个递归公式相信会越来越熟练, 理解会越来越深入.
题目背景
通过分析"K个一组翻转链表"问题,深度理解递归的本质和解题范式。
核心思想:先假设成立,再处理当前
递归公式的三个步骤
-
假设递归函数已经成立
- 先假设子问题已经被正确解决
- 思考如何利用子问题的结果来解决当前问题
- 这是"归"的处理逻辑
-
设计基准情况处理
- 找到递归的终止条件
- 直接处理最小规模的问题
- 为"归"提供起点
-
将两者融合到一个函数中
- 先处理基准情况(递归终止)
- 如果不是基准情况,则按照"子问题假设成立"的逻辑处理
K个一组翻转链表的递归分析
问题:给定链表和k值,每k个节点为一组进行翻转
例子:1->2->3->4->5->6->7, k=2
期望:2->1->4->3->6->5->7
步骤一:假设递归已经成立("归"的处理)
假设函数 reverseKGroup(head, k) 已经能正确处理子问题:
- 子问题:从第k+1个节点开始的子链已经按k个一组反转完成
- 当前层处理:
- 先连接:将第k个节点的next指向子问题返回的头节点
- 再反转:对前k个节点使用reverseN2进行反转
- 返回:反转后的新头节点
关键顺序:必须先连接再反转。
步骤二:基准情况处理
两种终止条件:
- 链表头节点为null:直接返回null
if(head == null){
return null;
}
- 剩余节点不足k个:按题意返回原链表头节点
// 判断逻辑:从head开始走k-1步
for (int i = 0; i < k - 1; i++) {
head = head.next;
if (head == null) {
return flag; // 返回原始头节点
}
}
步骤三:完整递归函数
public ListNode reverseKGroup(ListNode head, int k) {
// 首先记录下子链的头节点,别一会head移动找不到了
ListNode flag = head;
// 基准情况1:链表头节点为null
if(head == null){
return null;
}
// 基准情况2:head不为null,但链表长度小于k
for (int i = 0; i < k - 1; i++) {
head = head.next;
if (head == null){
return flag;
}
}
// 一般情况:子链有k个节点
// 此时经过前面的循环head已经为子链第k个节点了
ListNode Last = head;
// 继续移动head到下一个子链的头节点
head = head.next;
// 假设函数已经可以正确处理子链情况,进行递归调用
Last.next = reverseKGroup(head, k);
// 当后面的子链完成反转回来了,就轮到这个子链了
reverseN2(flag, k);
return Last;
}
递归的神奇之处
JVM帮我们完成了"递"和"归"
- "递"的过程:JVM在调用递归函数时自动完成栈帧的创建和参数传递
- "归"的过程:在栈帧结束时按照数学归纳法的逻辑自动处理
递归公式的思维简化
我们只需要考虑两个关键点:
- 基准情况如何处理
- 假设子问题已解决,如何处理当前问题
递归框架会自动帮我们:
- 将大问题分解为小问题
- 按正确顺序处理各个子问题
- 将子问题结果合并为最终答案
递归的数学本质:数学归纳法
核心认识
递归求解公式本质上就是数学归纳法的程序实现
当基准情况可以被我们的函数正确解决时,我们的假设就成立了,也就是子问题得到了正确的解决。我们的函数可以在子问题的基础上求解原问题,根据递推原理,我们的函数就可以求解任何规模的问题。
递归执行过程中数学归纳法的体现
递归函数的执行分为两个阶段,数学归纳法主要体现在"归"的过程中:
"递"的过程:问题分解
- 作用:将原问题逐层分解为子问题,直到最小情况
- 实现方式:函数调用栈,每次要用到子问题求解好的结果时, 就会开一个新栈帧来进入子问题求解
- 本质:这是为数学归纳法做准备,建立从小到大的问题序列
"归"的过程:数学归纳法解决问题的过程
- 作用:从最小情况开始,逐层向上构建解
- 实现方式:栈帧的逐层返回和结果合并
- 本质:这里才是数学归纳法的正式步骤
- 最小情况(基础步骤)→ 第二小情况(第一次归纳)→ ... → 原问题
执行流程示例(K个一组翻转):
递的过程:
问题1: 1->2->3->4->5->6->7 (k=2)
↓ 分解
问题2: 3->4->5->6->7 (k=2)
↓ 分解
问题3: 5->6->7 (k=2)
↓ 分解
问题4: 7 (不足k个,基准情况)
归的过程(数学归纳法):
基础步骤: 问题4直接返回7
↓ 归纳推导
归纳步骤1: 基于问题4的结果解决问题3 6->5->7
↓ 归纳推导
归纳步骤2: 基于问题3的结果解决问题2 4->3->6->5->7
↓ 归纳推导
归纳步骤3: 基于问题2的结果解决问题1 2->1->4->3->6->5->7
重要认识:
- "递"只是分解问题,为数学归纳法创造条件
- "归"才是数学归纳法的真正执行过程
- 每一次"归"都是一次归纳推导:已知子问题解 → 推导当前问题解
感悟
"我们只考虑了基准情况和递归函数成功解决子问题时我们怎么解决原问题,代入这个递归函数后就正确的帮我们实现了递和归,完成了我们问题的求解,大大简化了我们思考问题的思考量。"
递归的美妙在于:
- 数学严密性:基于数学归纳法的理论保证
- 信任:相信递归函数能解决子问题(归纳假设)
- 专注:只关注当前层的处理逻辑(归纳证明)
- 简化:让JVM帮我们自动处理复杂的调用流程
熟练掌握这个思考公式后,复杂的递归问题都可以被简化为一个数学归纳法的证明过程。