反轉鏈表系列問題
作者:Grey
原文地址:
反轉單鏈表
題目描述見:LeetCode 206. Reverse Linked List
思路如下
對於任何一個節點 cur
來說,記錄一個前驅節點 pre
(第一個節點的前驅節點是 null
)
先用一個臨時節點 tmp
記錄 cur
的下一個節點,然後設置
cur.next = pre;
pre = cur;
cur = tmp;
以下是示例圖
假設原始鏈表如下
第一個節點的反轉流程如下
第二個節點的反轉流程如下
最後返回 pre
節點即爲反轉後的節點。
代碼如下
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
時間複雜度\(O(N)\),空間複雜度\(O(1)\)。
反轉鏈表也可以用遞歸方法來實現
定義遞歸函數 ListNode reverse(ListNode cur)
,這個遞歸函數的含義是
反轉以 cur 爲頭的鏈表,並把反轉後的頭節點返回。
這個遞歸函數的 base case 是,只有一個節點的時候,即
if (cur == null || cur.next == null) {
return cur;
}
這種情況下,直接返回當前節點即可。
接下來是普遍情況:
當前來到 cur 節點,c,d,e 已經完成了反轉。
此時 cur 需要做如下操作:把 c , d, e 反轉後的頭節點獲取到,假設爲 pre , 在上圖中,pre 就是 e 節點,然後 cur 再做如下操作
cur.next.next = cur;
cur.next = null;
其中cur.next = null
非常重要,只有這樣,才能防止出現環。完整代碼如下
class Solution {
public ListNode reverseList(ListNode cur) {
return reverse(cur);
}
// 反轉cur爲頭的鏈表,並把反轉後的頭節點返回
public ListNode reverse(ListNode cur) {
if (cur == null || cur.next == null) {
return cur;
}
ListNode pre = reverse(cur.next);
cur.next.next = cur;
cur.next = null;
return pre;
}
}
時間複雜度\(O(N)\)
空間複雜度\(O(N)\)(遞歸棧佔用的空間)
反轉雙向鏈表
雙向鏈表和單鏈表的反轉類似,每個節點要多處理一次每個節點的前驅指針,
完整代碼如下
public static DoubleNode reverseDoubleList(DoubleNode head) {
if (head == null || head.next == null) {
return head;
}
DoubleNode pre = null;
DoubleNode cur = head;
while (cur != null) {
DoubleNode tmp = cur.next;
cur.next = pre;
cur.last = tmp;
pre = cur;
cur = tmp;
}
return pre;
}
反轉單鏈表一部分
題目描述見:LeetCode 92. Reverse Linked List II
本題核心依然是反轉鏈表,只是增加了一些變量來記錄需要反轉鏈表的頭位置和結尾位置,不過需要注意的是,本題的鏈表開始位置是從 1 開始,所以,如果m = 1 && n != 1
,說明反轉鏈表後需要返回新的頭部,只要m > 1
,反轉鏈表一部分以後,返回原先的頭即可。
此外,本題的 follow up 提到
Could you do it in one pass?
就是遍歷一次鏈表能否解決問題,具體代碼如下
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if (head.next == null || left == right) {
// 只有一個節點,怎麼反轉都一樣
// 只要反轉一個節點,反轉前後鏈表還是一樣的
return head;
}
if (left == 1) {
// 需要換頭
ListNode pre = null;
ListNode end = head;
ListNode cur = head;
int gap = right - left + 1;
while (gap != 0) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
gap--;
}
end.next = cur;
return pre;
} else {
ListNode pre = null;
for (int i = 1; i < left; i++) {
pre = pre == null ? head : pre.next;
}
ListNode end = pre;
ListNode cur = pre == null ? head : pre.next;
ListNode last = cur;
int gap = right - left + 1;
while (gap != 0) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
gap--;
}
if (end != null) end.next = pre;
if (last != null) last.next = cur;
// 不需要換頭,返回原先的頭節點
return head;
}
}
}
整個過程只遍歷一遍鏈表,時間複雜度\(O(N)\),空間複雜度\(O(1)\)。
本題也可以利用遞歸方式解,主函數可以直接定義爲遞歸函數
public ListNode reverseBetween(ListNode head, int left, int right)
該函數表示反轉 left
到 right
區間內的鏈表,並返回反轉後的最終鏈表的頭節點。
然後定義一個輔助函數(這個函數也是遞歸實現)
ListNode reverseN(ListNode head, int n)
該函數表示:反轉以 head 爲頭的鏈表的前 n 個節點,並返回反轉後的鏈表頭節點。
在left == 1
的條件下,主函數直接調用reverseN(head, right)
即可。
即:
public static ListNode reverseBetween(ListNode head, int left, int right) {
if (left == 1) {
// 相當於直接反轉前 right 個元素
return reverseN(head, right);
}
// ...
}
接下來是普遍情況:要實現 left
到 right
區間內的鏈表反轉,可以先考慮以head.next
爲頭的對應區間的鏈表反轉
如下圖
然後將head.next
指向反轉後的節點即可
接下來是reverseN
方法的實現,這個函數也是遞歸函數,表示反轉以head
爲頭的前n
個節點,並返回反轉後的頭節點,當n == 1
時,返回head
,接下來是普遍情況,反轉以head.next
爲頭的前n - 1
個節點,如下圖
反轉後的鏈表頭節點假設是last
,
這個last
節點就是整個鏈表反轉前n
個元素後的頭節點,然後將head
節點進行最後一次反轉
head.next.next = head;
最後需要將head
和e
節點連接起來,所以遞歸過程中,需要一直記錄反轉的部分鏈表的後繼節點,設置一個全局變量
static ListNode successor = null;
將head
指向successor
節點
完成了前n
個節點的反轉,完整代碼如下:
// 遞歸解法
public static 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;
}
// 反轉鏈表前N個節點
public static ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
head.next = successor;
return last;
}
static ListNode successor = null;