Pattern: In-place Reversal of a LinkedList,链表翻转
介绍部分来自:https://www.zhihu.com/question/36738189/answer/908664455 作者:穷码农
在众多问题中,题目可能需要你去翻转链表中某一段的节点。通常,要求都是你得原地翻转,就是重复使用这些已经建好的节点,而不使用额外的空间。这个时候,原地翻转模式就要发挥威力了。
这种模式每次就翻转一个节点。一般需要用到多个变量,一个变量指向头结点(下图中的current),另外一个(previous)则指向咱们刚刚处理完的那个节点。在这种固定步长的方式下,你需要先将当前节点(current)指向前一个节点(previous),再移动到下一个。同时,你需要将previous总是更新到你刚刚新鲜处理完的节点,以保证正确性。
咱们怎么去甄别这种模式呢?
- 如果你被问到需要去翻转链表,要求不能使用额外空间的时候
经典题目:
1、Reverse a LinkedList (easy)
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
解题思路:pre
指向反转链表的表头。current
指向被反转链表的表头的下一个节点。 p
指向被反转的节点。
1->2->3->4->5->null
1<-2<-pre(p) (p)current->3->4->5->null
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
// 1->2->3->4->5->null
// 1<-2<-pre(p) (p)current->3->4->5->null
if (head == null)
return null;
ListNode p = head;
ListNode pre = null;
ListNode current;
while (p != null){
current = p.next; // 保存原链表的下一个节点
p.next = pre; // 断开的节点指向反转链表的头部
pre = p; // 反转链表头部等于新的增加的节点
p = current; // 继续进行下一个节点的反转
}
return pre;
}
}
2、Reverse a Sub-list (medium)
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
解题思路: 根据上一题的反转模式。
-
创建一个新的头结点
res
,保证如果子链从第一个节点开始反转时能有上一个节点preHead
。 -
preHead
保存反转节点的上一个节点。例如m = 1
时,上一个节点值就是头结点:(preHead)0->1->2->3->4->5->NULL
-
preHead.next.next
为反转链表的末尾节点的next
。用于指向反转后断开的节点(cur)。 -
preHead.next
为反转链表的末尾节点的上一个节点的next
。用于指向反转后的头节点(pre)。 -
最后返回
res.next
即链表头结点的下一个引用。3. 4. 例如本题:
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
if (m == n || head == null)
return head;
ListNode res = new ListNode(0); // 创建一个链表头节点,保证从第一个节点反转时,能有上一个节点
res.next = head;
ListNode preHead = res; // 指向被反转子链表的上一个节点 (preHead)1->2->3->3->5->NULL
for (int i = 1; i < m; i++) {
preHead = preHead.next;
}
// 反转链表操作
ListNode p = preHead.next;
ListNode pre = null; // 反转后 (pre)4->3->2->NULL
ListNode cur = null; // 反转后 (cur)5->NULL
for (int i = m; i <= n; i++) { // 反转操作
cur = p.next;
p.next = pre;
pre = p;
p = cur;
}
preHead.next.next = cur; // 反转后的链表的尾结点指向剩余节点 cur
preHead.next = pre; // 反转的上一个节点指向反转的头节点 pre
return res.next;
}
}
3、Reverse every K-element Sub-list (medium)
描述:
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明:
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
自己的思路太复杂了,写一大坨代码,还是看别人的吧。淦
k = 2 递归大致思路:
注释我感觉我是乱写的,看图和代码比较清晰…
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode cur = head; // 从头结点往下探索子链
int count = 0; // 记录子链是否足够长
while (cur != null && count != k){
cur = cur.next;
count++;
}
if (count == k){ // 有足够长的子链
cur = reverseKGroup(cur, k); // 继续从当前节点往下探索子链
while (count != 0){ // 反转链表
count--;
ListNode p = head.next; // 记录头结点的下一个节点
head.next = cur; // 第一遍为递归完返回的子链表
cur = head; // 往前挪一个节点
head = p; // 继续从下一个节点开始
}
head = cur;
}
return head;
}
}
K = 3 非递归尾插法思路:
注释我感觉我是乱写的,看图和代码比较清晰…
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 0)
return head;
ListNode res = new ListNode(0); // 新建表头,指向 head
res.next = head;
ListNode preHead = res; // 从表头开始,定位子链头部
ListNode tail = res; // 从表头开始,定位子链尾部
while (true){ // 一直循环,直到 tail 定位到尾部
int count = 0;
while (tail != null && count != k){ // 尾部不为空 且 数量为 k
count++;
tail = tail.next;
}
if (tail == null) break; // 尾部为空则不进行翻转
// 尾插法
ListNode tempHead = preHead.next; // 记录反转后的子链表的头部节点
while (preHead.next != tail){ // 遍历至 定位的尾指针 的前一个节点
ListNode cur = preHead.next; // 从头开始,遍历节点
preHead.next = cur.next; // 定位到下一个节点
cur.next = tail.next; //
tail.next = cur; // 尾插
}
preHead = tempHead; // 从子链表的尾部继续反转
tail = tempHead; // 从子链表的尾部继续探索
}
return res.next;
}
}