图解链表反转系列Leetcode
前言
链表反转是基础数据结构链表的常见操作之一,链表的反转能够产生其他的拓展题目,以下将以LC206 反转链表、LC24 两两交换链表中的节点、LC25 K个一组翻转链表为例进行分析讲解。
LC 206 反转链表
对于本题,我们通过以下图解来说明。
首先,以下列链表为例:
这里我们创建一个节点(为null),使用指针pre指向它(作为当前节点的前驱节点),使用指针cur指向当前节点(此时为head节点),再用一个指针node指向当前节点的后继节点。
对于当前节点cur,我们执行以下操作:
- 首先记录当前节点cur的后继节点; node = cur.next
- 其次修改当前节点cur的后继节点为pre; cur.next = pre
对于第一个节点来说,当链表全部反转后,它将是最后一个节点,它的后继节点指向null。此时,我们完成了第一个节点的反转。
对第一个节点操作完成后,我们对第二个节点进行操作。
此时cur指向节点2,也就是上一次操作中的node节点; cur = node
pre指向节点1,也即上一次操作中的cur节点; pre = cur
对于当前节点cur,我们执行同样的操作:
- 首先记录当前节点cur的后继节点; node = cur.next
- 其次修改当前节点cur的后继节点为pre; cur.next = pre
此时我们完成了节点2的反转操作。
以此类推,此时对节点3进行反转:
此时对节点4进行反转:
此时对节点5进行反转:
这里注意在原链表中,节点5是最后一个节点,其后继节点为null,当我们依次操作每个节点,直到当前节点为原链表最后一个节点时,对于当前节点cur,我们执行同样的操作:
- 首先记录当前节点cur的后继节点; node = cur.next
- 其次修改当前节点cur的后继节点为pre; cur.next = pre
操作完毕,我们可以通过判断node是否为null决定是否返回反转后的链表。
通过以上的步骤,我们实现了对普通链表的反转操作,代码如下
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode node = cur.next;
cur.next = pre;
if(node == null){
return cur;
}
pre = cur;
cur = node;
}
return cur;
}
LC 24 两两交换链表中的节点
本题是链表反转的拓展版,在简单的链表反转中,我们只需要反转一个链表即可。而在本题中,我们相当于需要反转length / 2个短的链表,每个短链表含有两个节点。(length是链表的长度)
接下来,我们继续进行图解:(依然以上述链表为例)
首先我们对第一个短链表1,2进行反转。同样的,定义一个节点-1,使用指针pre指向它(这里我们可以看到与LC206的区别,LC206的初始pre指向null,因为反转后的链表只有一个,最后一个节点的后继一定为null;而本题的pre指向一个初始化的节点,这是因为对于分段链表,每一段的pre应指向前一段的最后一个节点,而该节点为非空节点),使用指针cur指向当前节点(这里为head节点),使用指针node存储当前节点的后继节点。
对第一个短链表进行反转,反转后的短链表应为2,1。对当前节点1操作完毕,下一次将对节点3进行操作,那么显而易见的,当前节点cur的后继节点应为操作节点3。而pre节点的后继节点应为反转后短链表的头节点2。
对于当前节点cur执行以下操作:
- 首先记录当前节点的后继节点; node = cur.next
- 其次令当前节点的后继节点为node的后继节点; cur.next = node.next
- 然后令node节点的后继节点为当前节点; node.next = cur
- 最后令pre节点的后继节点为node节点; pre.next = node;
操作完毕,得到链表为:
第一段小的链表反转完毕,即将操作第二段链表,cur指针指向节点3,也即上一步中的cur.next,pre指针指向节点1,也即上一步中node节点的后继节点。
对第一二短链表进行反转,反转后的短链表应为4,3。对当前节点3操作完毕,下一次将对节点5进行操作,当前节点cur的后继节点应为操作节点5。而pre节点的后继节点应为反转后短链表的头节点4。
对于当前节点cur执行以下操作:
- 首先记录当前节点的后继节点; node = cur.next
- 其次令当前节点的后继节点为node的后继节点; cur.next = node.next
- 然后令node节点的后继节点为当前节点; node.next = cur
- 最后令pre节点的后继节点为node节点; pre.next = node;
操作完毕,得到链表为:
第二段小的链表反转完毕,即将操作第三段链表,cur指针指向节点5,也即上一步中的cur.next,pre指针指向节点3,也即上一步中node节点的后继节点。在第三段短链表中,我们可以看到,该段链表只有一个节点,我们将该段短链表视为5, null。该段链表不需要反转,可以得到链表遍历到最后的出口为cur.next为null。
通过以上的步骤,我们实现了对两两节点的反转操作,代码如下
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode cur = head;
ListNode pre = new ListNode(-1);
pre.next = cur;
ListNode res = head.next;
while(cur != null && cur.next != null){
ListNode node = cur.next;
cur.next = node.next;
node.next = cur;
pre.next = node;
cur = cur.next;
pre = node.next;
}
return res;
}
LC25 K个一组翻转链表
本题是前两道题的综合版,前一道题目两两反转,意味着反转多组短链表,每个链表的长度为2;本题则是反转多组链表,每个链表的长度为k(简单链表反转可以看作反转多组链表,每个链表的长度为1)。自然的,我们的思路为,对于每一组短链表,使用反转简单链表的函数reverse来进行翻转,翻转后将多段短链表连接起来。这里的重点问题是: 如何连接已翻转短链表和未翻转链表?
回顾之前的两道题目,我们定义了指针cur, pre来记录操作节点与操作节点的前驱节点,这里我们同样定义cur和pre,pre指向新创建的节点-1。
在第一个题目中,我们可以看到,cur节点与pre节点的距离为1;且遍历每一个节点时,pre与cur都向右移动一个距离;(pre指向当前短链表的末尾,cur指向下一个短链表的头部)
在第二个题目中,我们可以看到,cur节点与pre节点的距离同样为1;而遍历每一个节点时,pre与cur都向右移动两个距离;(pre指向当前短链表的末尾,cur指向下一个短链表的头部)
那么当短链表长度为k时,cur节点与pre节点的距离为1;遍历每一个节点时,pre与cur都向右移动k个距离,pre指向当前短链表的末尾,cur指向下一个短链表的头部,从而完成k个一组链表的翻转。(这里我们使用pHead记录当前短链表的头节点,nextPre记录下一个短链表的前驱节点,也即当前列表操作后的末尾节点,nextPHead记录下一个短链表的头节点)
顺着这个思路,我们以下列链表为例,令k=3, 图解其翻转过程:
首先,我们创建新节点-1,指针pre指向节点-1。初始化pHead和nextPre为头节点。对第一段短链表进行操作:
- 首先遍历第一段链表,找到nextPHead;
- 其次对第一段链表进行翻转;
- 令该段链表的前驱节点pre的后继指向反转后链表的头。 pre.next = reverse(pHead, k)
得到链表为:
对第一段链表操作完毕,接下来操作第二段链表。当前段链表的初始头节点pHead为节点4,也即上一次操作中的nextPHead节点;当前端链表的前驱节点pre为节点1,也即上一次操作中的nextPre节点。对于下一段链表,其前驱节点nextPre为本段链表的头节点4,遍历第二段链表,找到nextPHead。
同样地,
- 对第二段链表进行翻转;
- 令该段链表的前驱节点pre的后继指向反转后链表的头。 pre.next = reverse(pHead, k)
得到链表为:
对第二段链表操作完毕,操作第三段链表:
第三段链表长度不足k=3, 所以直接令前驱节点pre的后继节点为当前段链表的头pHead,无需翻转操作。
通过以上的步骤,我们实现了对K个一组链表的翻转操作,代码如下
public ListNode reverseKGroup(ListNode head, int k) {
int count = 0;
ListNode pre = new ListNode(-1);
pre.next = head;
ListNode res = pre;
ListNode pHead = head;
ListNode nextPHead = head;
ListNode nextPre = head;
int len = length(head);
while(count + k <= len){
for(int i = 0; i < k; i++){
count++;
nextPHead = nextPHead.next;
}
ListNode partHead = reverse(pHead, k);
pre.next = partHead;
pre = nextPre;
nextPre = nextPHead;
pHead = nextPHead;
}
if(pHead != null){
pre.next = pHead;
}
return res.next;
}
public ListNode reverse(ListNode head, int k){
// return the head of the reverse list
ListNode pre = null;
ListNode cur = head;
int count = 0;
while(count < k){
ListNode node = cur.next;
cur.next = pre;
pre = cur;
if(count == k - 1){
return cur;
}
cur = node;
count++;
}
return cur;
}
public int length(ListNode head){
if(head == null){
return 0;
}
int len = 0;
while(head != null){
len++;
head = head.next;
}
return len;
}
总结
以上题目之间存在层层递进关系,逐步分析,能够加深对链表反转的理解。