写在前面
Hello朋友们,我是秋刀鱼,一只活跃于Java区与算法区的新人博主~
欢迎大家加入高校算法学习社区: https://bbs.csdn.net/forums/Suanfa,社区里大佬云集,大家互相交流学习!
从今天开始我将陆续更新《轻松拿捏大厂面试题》专栏文章,本专栏将挑选大厂出现频率极高的面试题做专题解读,本篇也是专栏的第一篇《反转链表篇》。
主页:秋刀鱼与猫期待你的支持与关注~
反转链表作为大厂一类高频面试题,相信可能会让很多人头疼,究其原因主要是对于链表结构把握不到位且没有完整梳理链表反转过程。
因此这篇博客主要分享一下我对于反转链表解题的理解,用画图的方式帮助大家理解过程,循序渐进地带领大家一步一步攻克这类难题
题目传送门
开始链表反转类问题之前,先阐述一下这类题的一般解题方法:
- 定义一个虚拟头结点,方便反转后链表头结点的返回。
- 找到需要操作的结点位置,使用修改
next
的方式修改链表指向顺序达到反转的目的。话不多说就让我们开始吧
为了将给定的链表反转,首先需要指定当前遍历到的结点head
,其次反转链表的本质是将 head 结点指向其前一个结点,因此存储 head 结点的前驱结点为 pre
。既然 head 结点反转需要指前一个结点 pre ,那修改前 head 的下一个结点就存储在 next
中,保证遍历地进行。总结一下就是:
head
:当前遍历的结点pre
:head 结点的前一个结点,head为头结点时值为 null。next
:反转 head 结点前的下一个结点下面先讲讲如何反转一个链表,假定反转前首先初始状态如下:
遍历的结点为 head
,反转的过程实际上是修改head.next
为 head 的前一个元素pre,也就是执行head.next=pre
。
可以看到:head 结点的 next 指向已经被修改,覆盖了原来的 next 指向 , 此时就体现出 next
指针的重要性。将 pre 指向 head, head 指向其修改前的下一个元素 next ,进入下一次修改的循环:
当 head 指向为 NULL 时,循环结束,此时的真正的头结点是 head 的前一个元素 pre ,因此将 pre 作为结果返回。
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
// 指向 head 的前一个元素
ListNode pre = null;
// 循环结束的条件为 head == null
while (head != null) {
// 用 next 保存修改前的下一个元素位置
ListNode next = head.next;
// 修改
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
题目要求:只能反转一部分链表,剩余一部分链表顺序保持不变。在尝试了半个小时的挣扎后看着臃肿的代码与不知所名的变量,秋刀鱼不禁陷入了沉思:有什么好的方法能够简单、高效地解决这一类问题呢?
片刻后灵光一现!突然想到上一题中反转链表的代码与本题都是处理链表的反转,那上题的代码或许能派上用场!
反转链表代码中传入参数是一个链表的头结点,返回的是反转后的头结点。那只需要找到反转部分的头部结点作为参数传入后,返回的不就是反转后链表的头部结点了嘛!
但如果直接传入反转头结点进入函数中,那之后所有的结点都会被反转!因此将反转部分尾与其之后结点断开是一个不错的处理方式。
因此只需要做到下面几点就能够优雅地解决这道题目:
h
preH
t
afterT
反转可能出现在头结点上,这种情况下 preH
的处理会相当的困难。可以使用一个链表中常见的方法:添加一个虚拟头结点preHead
的方式,使得虚拟头结点做为原来头结点head
的前驱结点,那么preHead.next
就是需要返回的结果。
一图胜千言,下面这张图更好地阐述了上面的构思:
但仔细观察:反转之后 preH.next
与 h.next
指向明显出现错误,因此重新调整链表是不可缺少的一环。
根据上图不难发现:调整过程只需要将 preH.next
指向反转后的头结点 t
,h.next
指向 afterT
就能够完成调整!
当调整完成后,返回 preHead.next
即是新的头结点也是题目的答案。
class Solution {
// 上一题翻转链表的代码
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
ListNode pre = null;
while (head != null) {
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
// 题目函数
public ListNode reverseBetween(ListNode head, int left, int right) {
// 虚拟头结点 preHead
ListNode preHead = new ListNode();
preHead.next = head;
ListNode preH, h, t, tmp, afterT;
// 初始化
t = afterT = preH = tmp = h = preHead;
// 位置参数,从0开始
int idx = 0;
while (tmp != null) {
// tmp 指向 preH
if (idx == left - 1) {
h = tmp.next;
preH = tmp;
}
// tmp 指向 t
if (idx == right) {
t = tmp;
afterT = tmp.next;
// 断开不需要翻转的部分
tmp.next = null;
break;
}
tmp = tmp.next;
++idx;
}
// 等价于 preH.next = reverseList(h);
reverseList(h);
preH.next = t;
//修改h.next的指向完成尾部的拼接
h.next = afterT;
return preHead.next;
}
}
我相信大多数的朋友第一次遇见这题都是蒙圈的状态。题目要求每 K 组进行一次链表反转,长度不够 K 组的部分保留原来顺序,这怎么办处理才好呢还真是伤脑筋。
此时我们可以换换思路,用之前两道题的解题思路解决这道题目:每 K 组进行一次链表反转,是不是对应着反转 [ 1 , k ] , [ k + 1 , 2 k ] , [ 2 k + 1 , 3 k ] . . . [1,k],[k+1,2k],[2k+1,3k]... [1,k],[k+1,2k],[2k+1,3k]... 的链表元素呢,而上一题 反转链表 II 中处理的正好就是这种某段链表反转的问题!因此只需要借用上一题的代码就能够优雅地拿捏这道题。
为了方便处理首先定义一个头结点的前驱结点preHead
,而每次传入 反转链表 II 参数是头结点,即传入preHead.next
。定义一个计数器 idx
同时定义一个遍历链表的指针 tmp
,tmp 开始时指向头结点 head ,例如下图所示。
idx
记录遍历次数tmp
遍历链表next
当计数器 idx%k==0
时,tmp指向需要一组的最后一个结点,此时进行 K 个一组的反转操作,反转 [ i d x − k + 1 , k ] [idx-k+1,k] [idx−k+1,k] 链表结点。
第一次转置会导致preHead.next
指向出错,因此反转后将新的头结点赋值给 preHead.next
。当 tmp 指向为空时循环结束,最终返回preHead.next
即是结果。
class Solution {
// 反转链表 代码
public ListNode reverseList(ListNode head) {
if (head == null) {
return head;
}
ListNode pre = null;
while (head != null) {
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
// 反转链表II 代码
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode preHead = new ListNode();
preHead.next = head;
ListNode preH, h, t, tmp, afterT;
t = afterT = preH = tmp = h = preHead;
int idx = 0;
while (tmp != null) {
if (idx == left - 1) {
h = tmp.next;
preH = tmp;
}
if (idx == right) {
t = tmp;
afterT = tmp.next;
tmp.next = null;
break;
}
tmp = tmp.next;
++idx;
}
reverseList(h);
preH.next = t;
h.next = afterT;
return preHead.next;
}
// 题目函数
public ListNode reverseKGroup(ListNode head, int k) {
// preHead 声明为头结点的前驱结点
ListNode preHead = new ListNode();
preHead.next = head;
// tmp 遍历链表
ListNode tmp = head;
int idx = 0;
while (tmp != null) {
++idx;
ListNode next = tmp.next;
if (idx % k == 0) {
preHead.next = reverseBetween(preHead.next, idx - k + 1, idx);
}
tmp = next;
}
return preHead.next;
}
}
反转链表的高频题讲解到此就结束了,其核心还是抓住推导反转的过程,一步一步完成链表的反转操作,但是细节慢慢一定要小心谨慎(秋刀鱼本人就掉过很多次坑),因此一定要小心。