【力扣】反转链表I和II(迭代和递归)
前言
有句话叫做:如果面试官跟你看顺眼的话,就给你出一道反转链表,否则就出一道 hard。
所以反转链表不能不会吧,要不面试官想要你都没有机会了。
206. 反转链表

class Solution {
public ListNode reverseList(ListNode head) {
}
}
迭代
迭代就是遍历链表,需要有个 cur 指针在链表上游走。
先思考一般情况,假设遍历到某一节点,应该做什么?

如上图所示,应该修改其 next 指针,指向前一个节点。由于单链表的性质,无法从当前节点获取前一个节点的指针,所以需要有个 pre 指针记录当前节点的前一个节点。把 cur 的指针修改后,就无法获取其后一个节点了,因此需要有个 next 指针保存 cur 的下一个节点。更新 pre 和 cur 指针,让它们后移一位即可。
class Solution {
public ListNode reverseList(ListNode head) {
// pre记录cur的前一个节点,cur是当前遍历到的节点
ListNode pre = null, cur = head;
// 遍历链表一般用while循环
while(cur != null){
// 保存cur的下一个节点
ListNode next = cur.next;
// 修改cur指针,指向它前一个节点
cur.next = pre;
// 更新pre、cur, 同时后移一位
pre = cur;
cur = next;
}
// 此时cur为null, pre指向最后一个节点
return pre;
}
}
递归
递归有两个性质:
- 边界条件
- 转化为子问题调用自身
先思考第二个问题,如何转化为子问题调用自身?
转化为子问题也就是缩小问题的规模,如果 head 指向的链表有 n 个节点,那么 head.next 指向的链表就有 n - 1 个节点,因此 head.next 就是一个子问题。为了更好地复用递归函,需要明确递归函数的定义,这里递归函数的定义是,输入一个单链表的头指针,返回该链表反转后的链表的头指针。
调用 head.next 的示意图如下:

根据递归函数的定义,它计算后返回值的情况如下所示:

接下来,就是要处理 head 和 reverseList(head.next) 的关系了,这个例子中就是 1 号节点和后面部分的关系。这里有两步,第一步,让 2 号节点指向 1 号节点;第二步,让 1 号节点指向 null。第一步翻译成代码就是head.next.next = head,第二步翻译成代码就是head.next = null。

现在,第二个问题就处理完了,代码如下
class Solution {
public ListNode reverseList(ListNode head) {
// 1、边界条件
// if(){
// }
// 2、转化为子问题调用自身
ListNode last = reverseList(head.next);
// 修改head和后面部分指针的关系
head.next.next = head;
head.next = null;
return last;
}
}
对于递归函数,我觉得第二部分是核心,边界条件是特殊情况,可以先不考虑。自己去想可能会遗漏一些情况,这时如果允许执行测试用例,可以借助执行测试用例来帮助我们补全边界条件。
比如,这里我暂时不清楚边界条件,先执行测试用例,看看它会报什么错,然后根据它的错误提示来写边界条件。
直接执行上面的代码,会报这样的错

这样就知道 head 不能为空,加上条件
// 1、边界条件
if(head == null){
return head;
}
再执行代码,会报如下的错

这样就知道 head.next 不能为空,加上条件
// 1、边界条件
if(head == null || head.next == null){
return head;
}
完整代码
class Solution {
public ListNode reverseList(ListNode head) {
// 1、边界条件
if(head == null || head.next == null){
return head;
}
// 2、转化为子问题调用自身
ListNode last = reverseList(head.next);
// 修改head和后面部分指针的关系
head.next.next = head;
head.next = null;
return last;
}
}
92. 反转链表 II
迭代
希望复用反转单链表的代码,这样就需要把需要反转的部分单独抽取出来,反转好了之后再拼接回去。因此需要有 4 个指针,leftNode 和 rightNode 分别指向需要反转部分的首尾节点,pre 指针指向 leftNode 的前一个,cur 指针指向 rightNode 的后一个,pre 和 cur 的作用是为了保存断开链表和拼接链表的位置。4 个指针的位置关系如下图所示。

确定 4 个指针的位置并不复杂,循环指定步数即可。确定好 4 个指针的位置后,就是断开 pre 和 leftNode、rightNode 和 cur,调用之前的反转单链表将中间部分反转,最后再拼接回去即可。
完整代码
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 虚拟头节点
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy, cur = null;
ListNode leftNode = null, rightNode = head;
// 从虚拟头节点走left-1步,来到left前一个节点
for(int i = 0; i < left - 1; i++){
pre = pre.next;
}
// left节点在pre节点下一个
leftNode = pre.next;
// 从头节点走right-1步,来到right节点
for(int i = 0; i < right - 1; i++){
rightNode = rightNode.next;
}
// cur节点在right节点下一个
cur = rightNode.next;
// 断开指针
pre.next = null;
rightNode.next = null;
// 反转between
reverseList(leftNode);
// 拼接指针
pre.next = rightNode;
leftNode.next = cur;
return dummy.next;
}
// 反转单链表
private ListNode reverseList(ListNode head){
ListNode pre = null, cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
递归
首先需要实现反转前 n 个节点的函数 ListNode reverseN(ListNode head, int n)。
比如要反转如下链表的前 3 个节点

那就是要反转以 head.next 开头的链表的前 2 个节点

如果reverseN函数写的是正确的,那么按照它的定义,返回值如下

这里又涉及到 head 和 reverseN(head.next, n - 1) 指针关系的处理,和递归反转单链表是类似的,第一步是让 2 号节点指向 1 号节点,第二步略有不同,反转单链表是直接让1号节点指向 null,而这里应该让 1 号节点指向 4 号节点,也就是不需要反转的部分。因此,就需要一个指针记录 4 号节点的位置。
reverseN代码如下
// 记录不用反转的第一个节点
ListNode suc = null;
private ListNode reverseN(ListNode head, int n){
// 1、边界条件
if(n == 1){
suc = head.next;
return head;
}
// 2、转化为子问题调用自身
ListNode last = reverseN(head.next, n - 1);
// 修改head和后面部分指针的关系
head.next.next = head;
head.next = suc;
return last;
}
接下来实现reverseBetween
当 left 等于 1 的时候,就转化成了reverseN的问题。
对于 head,反转 left 和 right;对于 head.next,应该反转 left - 1 和 right - 1;对于head.next.next,应该反转 left - 2 和 right - 2…
所以,reverseBetween函数代码如下
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left == 1){
return reverseN(head, right);
}
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
}
完整代码
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
// 当left==1, 就转化成立reverseN问题
if(left == 1){
return reverseN(head, right);
}
head.next = reverseBetween(head.next, left - 1, right - 1);
return head;
}
// 记录不用反转的第一个节点
ListNode suc = null;
private ListNode reverseN(ListNode head, int n){
// 1、边界条件
if(n == 1){
suc = head.next;
return head;
}
// 2、转化为子问题调用自身
ListNode last = reverseN(head.next, n - 1);
// 修改head和后面部分指针的关系
head.next.next = head;
head.next = suc;
return last;
}
}
参考资料
【力扣】反转链表I和II(迭代和递归)的更多相关文章
- 力扣Leetcode 45. 跳跃游戏 II - 贪心思想
这题是 55.跳跃游戏的升级版 力扣Leetcode 55. 跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 你的目标是使用最少的跳跃 ...
- 力扣 - 剑指 Offer 55 - II. 平衡二叉树
题目 剑指 Offer 55 - II. 平衡二叉树 思路1(后序遍历+剪枝) 这题是上一题剑指 Offer 55 - I. 二叉树的深度的进阶,逻辑代码和那个一样,也是后续遍历,获取两个子节点较大的 ...
- Leetcode力扣45题 跳跃游戏 II
原题目: 跳跃游戏 II 给定一个非负整数数组,你最初位于数组的第一个位置. 数组中的每个元素代表你在该位置可以跳跃的最大长度. 你的目标是使用最少的跳跃次数到达数组的最后一个位置. 示例: 输入: ...
- 力扣 - 445. 两数相加 II
目录 题目 思路 代码实现 题目 给你两个 非空 链表来代表两个非负整数.数字最高位位于链表开始位置.它们的每个节点只存储一位数字.将这两数相加会返回一个新的链表. 你可以假设除了数字 0 之外,这两 ...
- 力扣 - 剑指 Offer 53 - II. 0~n-1中缺失的数字
题目 剑指 Offer 53 - II. 0-n-1中缺失的数字 思路1 排序数组找数字使用二分法 通过题目,我们可以得到一个规律: 如果数组的索引值和该位置的值相等,说明还未缺失数字 一旦不相等了, ...
- 力扣 - 剑指 Offer 57 - II. 和为s的连续正数序列
题目 剑指 Offer 57 - II. 和为s的连续正数序列 思路1(双指针/滑动窗口) 所谓滑动窗口,就是需要我们从一个序列中找到某些连续的子序列,我们可以使用两个for循环来遍历查找,但是未免效 ...
- 力扣题解-面试题58 - II. 左旋转字符串
题目描述 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部.请定义一个函数实现字符串左旋转操作的功能. 比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转 ...
- 力扣119. 杨辉三角 II
原题 1 class Solution: 2 def getRow(self, rowIndex: int) -> List[int]: 3 ans = [1] 4 for i in range ...
- 【力扣】剑指 Offer II 092. 翻转字符
题目 解题思路 一个很暴力的想法,在满足单调递增的前提下,使每一位分别取 1 或 0,去看看哪个结果小. 递归函数定义int dp(StringBuilder sb, int ind, int pre ...
随机推荐
- C#实现生成Markdown文档目录树
前言 之前我写了一篇关于C#处理Markdown文档的文章:C#解析Markdown文档,实现替换图片链接操作 算是第一次尝试使用C#处理Markdown文档,然后最近又把博客网站的前台改了一下,目前 ...
- 十七、Job与Cronjob
Job 与 Cronjob 一.Job Job 负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束. 特殊说明: 1.spec.template 格式同 Pod 2 ...
- 小菜鸡学习---<正则表达式学习笔记2>
正则表达式学习笔记2 一.修饰符 前面我们学习的都是用于匹配的基本的关键的一些表达式符号,现在我们来学习修饰符.修饰符不写在正则表达式里,修饰符位于表达式之外,比如/runoob/g,这个最后的g就是 ...
- RK3568开发笔记(五):在虚拟机上使用SDK编译制作uboot、kernel和ubuntu镜像
前言 buildroot虽然灵活,但是基于实际情况,本身是侧重驱动和应用定制开发的只定制一次文件系统投入有点多,还不如直接ubunt自己交叉编译依赖库,做一些库的移植裁剪. 于是本篇就使用ubu ...
- 【题解】CF1715A Crossmarket
题面传送门 解决思路 首先,我们让 Megan 先走,因为他可以留下传送门.可以得知,不管怎么走,他到达终点所耗费的能量一定是 \(n+m-2\) . 然后,Stanley 走时就可以利用传送门.考虑 ...
- 2022-11-14 Acwing每日一题
本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...
- go工具pprof部署
在做内存分析时,用到了pprof,这里做一下部署介绍和入门级别的使用. pprof是golang的性能工具,有两种交互方式:命令行交互和web交互,同时还支持性能分析数据的图形化展示. 部署pprof ...
- 让 Serverless 更普惠,阿里云函数计算 FC 宣布全面降价,最大幅度达 37.5%
11月5日,2022 杭州 · 云栖大会上,阿里云宣布函数计算 FC 开启全面降价,vCPU 单价降幅** 11%,其他的各个独立计费项最高降幅达 37.5%**. 本次云栖大会上,阿里云智能总裁张建 ...
- 基于python的数学建模---非线性规划
凸函数的非线性规划 minimize 求解的是局部最优解 简单的函数,无所谓 复杂的函数 初始值的设定很重要 scipy.optimize.minimize(fun,x0,args=(),method ...
- UBOOT编译--- UBOOT顶层Makefile中目标_all和all的关系及背景(四)
@ 目录 1. 前言 2. 概述 3. 老版本UBOOT(背景) 4. 新版本UBOOT 5. 参考 1. 前言 UBOOT版本:uboot2018.03,开发板myimx8mmek240. 2. 概 ...