【LeetCode】->連結串列->通向連結串列自由之路
LeetCode - 連結串列
Ⅰ 前言
在我資料結構與演算法的連結串列講解中,我留下了幾個連結串列必須要掌握的操作,掌握看了它們,就基本可以實現 “連結串列自由” 了。
關於連結串列的詳細講解,可以跳轉到我下面的文章。
【資料結構與演算法】->資料結構->連結串列->LRU快取淘汰演算法的實現
這幾個操作分別對應了 LeetCode 上的幾道題,我們一起來看看。
Ⅱ 刪除連結串列倒數第 n 個結點(#19)
這道題的具體描述如下?
這個題我最開始的想法是用雜湊表先將這些結點按順序儲存起來,這樣可以直接根據鍵值來找到需要刪除的點,並且,也只需要遍歷一次,就可以將所有結點存入雜湊表中,後面直接根據鍵值來取就好了。
想法是好的,最後時間效能上打敗了百分之六十多的人,讓我意識到這個程式寫得是多麼蠢。
這個題其實還可以用一個小技巧來做,就是快慢指標。我們先讓一個快指標走 n 次,然後這時候慢指標和快指標一起移動,這樣當快指標移動到終點,也就是這條鏈的尾結點的時候,慢指標剛好到要刪除的地方。
為了方便對連結串列進行操作,我引入了一個哨兵,這樣如果要刪除的結點是頭結點,也不需要做特殊處理。我們直接來看程式碼。?
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* @lc app=leetcode.cn id=19 lang=java
*
* [19] 刪除連結串列的倒數第N個節點
*/
// @lc code=start
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(0);
ListNode fastPoint = pre;
ListNode slowPoint = pre;
pre.next = head;
while (n != 0) {
fastPoint = fastPoint.next;
n--;
}
while (fastPoint.next != null) {
fastPoint = fastPoint.next;
slowPoint = slowPoint.next;
}
slowPoint.next = slowPoint.next.next;
return pre.next;
}
}
// @lc code=end
可以看到效能提高了非常多。
Ⅲ 兩個有序連結串列的合併 (#21)
我先將這道題的具體描述貼上。
這道題大家很容易想到利用 while 迴圈,然後對兩個有序連結串列進行逐節點的比較,這個思路比較簡單,我一開始也是想不到什麼好方法,只能用這種笨蛋解法。我先將我的笨蛋解法貼出來,思路很簡單,大家看一下就很容易理解了。
/*
* @lc app=leetcode.cn id=21 lang=java
*
* [21] 合併兩個有序連結串列
*/
// @lc code=start
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode();
ListNode tmp = head;
ListNode p = l1;
ListNode q = l2;
while (p != null && q != null) {
ListNode cur = new ListNode();
if (p.val < q.val) {
cur.val = p.val;
p = p.next;
} else {
cur.val = q.val;
q = q.next;
}
tmp.next = cur;
tmp = cur;
}
while (p != null) {
ListNode cur = new ListNode(p.val);
tmp.next = cur;
tmp = cur;
p = p.next;
}
while (q != null) {
ListNode cur = new ListNode(q.val);
tmp.next = cur;
tmp = cur;
q = q.next;
}
return head.next;
}
}
// @lc code=end
思路就是先對兩個有序連結串列進行比較,直到一個連結串列到盡頭了。那麼如果還有連結串列沒有被遍歷完,因為是順序的,所以直接將它餘下的元素加入到最後的連結串列中即可。
這個笨蛋解法的耗時是多少呢?
可以看到,時間複雜度和空間複雜度都非常高。
我們這個解法每一次比較之後,都要建立一個新的結點,在 while 迴圈中不僅浪費了很多時間,也浪費了大量的空間,其實沒必要,我們再看看一個更聰明的解法,用遞迴來解決這個問題。
題目的部分我不再給出,只貼出作答的部分?
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
程式碼比前面的少了非常多,我們再看看效能?
通過這個遞迴,最後返回的就是最小的結點,我們也不需要再建立額外的空間,直接將原先的結點連線起來就好。
Ⅳ 連結串列中環的檢測(#141)
同樣的,我先把題目貼出。
看到這個題我首先想到的就是用一個set集合或者hash table來儲存,遍歷整個連結串列,如果有相同的結點存進去,那就說明有環,返回 true,否則返回 false。但是,同樣的,我首先可以想到的演算法一定不是最好的演算法。
我們注意題目裡的進階要求,空間複雜度為 O(1)。那就肯定不可以用 set 集合了,那樣複雜度就變成 O(n) 了。
這時候,我們就要用到一個非常巧妙的解決方法,也是我們上面提到過的 快慢指標。那這個快慢指標要怎麼用呢?
大家肯定對我們初中學過的追及問題有印象吧?如果你比你追及的物件速度快,那你就一定可以追到它。對應到這道題,我們讓快指標每次往前走兩格,慢指標每次往前走一格,如果連結串列中有環的話,快慢指標最後就會進入環中,相當於兩個人開始在操場跑圈,那快的人一定會追上慢的人,兩個人一定能相見。
放在這個題中,也是同樣的思路。我們最後就檢測快指標有沒有追到慢指標,如果追到了,說明一定有環,如果沒有追到,那連結串列中一定沒有環。根據這個思路,我們來寫程式碼。
/*
* @lc app=leetcode.cn id=141 lang=java
*
* [141] 環形連結串列
*/
// @lc code=start
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
if (fast.next == slow) {
return true;
}
fast = fast.next.next;
slow = slow.next;
}
return false;
}
}
// @lc code=end
這裡我還想直接截個圖,因為不僅我的編輯器太漂亮了,這個解法也狠漂亮,非常簡潔。
Ⅴ 單連結串列反轉(#206)
具體題目如下?
進階的要求是用迭代或者遞迴來解決,那我們就不要考慮用一個容器比如 ArrayList 來存放然後反轉這種演算法了,我們直接來看比較複雜的迭代和遞迴。
這兩個演算法在我之前的文章中寫過,大家有興趣可以跳轉過去看。
【程式設計師必修數學課】->基礎思想篇->遞迴(上)->泛化數學歸納
【程式設計師必修數學課】->基礎思想篇->遞迴(下)->分而治之&從歸併排序到MapReduce
首先我們先看遞迴。
/*
* @lc app=leetcode.cn id=206 lang=java
*
* [206] 反轉連結串列
*/
// @lc code=start
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode cur = reverseList(head.next);
head.next.next = head;
head.next = null;
return cur;
}
}
// @lc code=end
最複雜的地方是下面這句
這其實就是一個反轉的過程。
拿上圖 4 個這個結點來舉例子。
head.next.next
指的就是 4.next.next
也就是 5.next
,它等於 head
,也就是指向了自己,這個語句就實現了結點 5 指向了 4,然後我們再把 head.next = null
,也就是 4 指向 5 給斷開,這就實現了反轉。
整個遞迴的過程,返回的 cur 就是 5,也就是最後一個結點,這樣所有的反轉完成之後,返回的 5 就相當於是這個反轉連結串列的頭結點了,也就完成了整個的反轉過程。
我們來看看遞迴的效能?
時間是很快,但是空間消耗是很大的,因為每一層遞迴都要消耗系統堆疊空間,所以佔用的記憶體很大。
我們再來看迭代。
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
ListNode tmp = null;
while (cur != null) {
tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
迭代法實現的思想就是每遍歷到一個結點,用 cur 標記,然後 pre 是 cur 前面的結點,使得 cur 指向 pre,這樣遍歷完,到 cur 指向 null 的時候,pre 正好指向尾結點,這樣就實現了反轉。
迭代法的思路不難理解,大家可以用程式碼做個參考。
迭代法實現的效能也是很強大,空間消耗比遞迴法少很多?
Ⅵ 求連結串列的中間節點(#876)
我們先看看這道題的具體描述。
我們要求一個連結串列的中間結點。經過前面幾道題,我們知道了有一個很巧妙的遍歷方法,就是快慢指標,這道題簡直就是為快慢指標而設的。
要求中間結點,那我們令快指標和慢指標都指向頭結點,然後開始遍歷。快指標一次走兩格,慢指標一次走一格,這樣直到快指標遍歷到連結串列末端。如果是奇數個,那快指標剛好指向尾結點,fast.next = null
。如果是偶數個,那快指標就指向了尾結點的下一個結點,也就是空,即 fast = null
。
所以,我們可以根據這兩個邊界條件,來判斷快指標是否遍歷完了整個連結串列。當快指標遍歷完連結串列之後,由於它每次都比慢指標多走一步,所以慢指標最後指向的就是我們要求的中間結點。
我們直接來看程式碼?
/*
* @lc app=leetcode.cn id=876 lang=java
*
* [876] 連結串列的中間結點
*/
// @lc code=start
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
// @lc code=end
為方便大家看,我再截個圖?
效能測試如下?
OK,這就是連結串列的幾個重要操作,掌握熟練這些題,我們就在通向連結串列自由之路上前進了一大截。
相關文章
- Leetcode_86_分割連結串列_連結串列LeetCode
- LeetCode-Python-86. 分隔連結串列(連結串列)LeetCodePython
- LeetCode 143 重排連結串列 HERODING的LeetCode之路LeetCode
- LeetCode-連結串列LeetCode
- 【LeetCode-連結串列】面試題-反轉連結串列LeetCode面試題
- 連結串列 - 單向連結串列
- 連結串列-迴圈連結串列
- 連結串列-雙向連結串列
- LeetCode 86 ——分隔連結串列LeetCode
- LeetCode連結串列專題LeetCode
- leetcode 反轉連結串列LeetCode
- 2024/12/2【連結串列】LeetCode 142 環形連結串列 II 【X】LeetCode
- 連結串列4: 迴圈連結串列
- 連結串列-雙向通用連結串列
- 連結串列-單連結串列實現
- leetcode:21. 合併兩個有序連結串列(連結串列,簡單)LeetCode
- 2024/12/1 【連結串列】 LeetCode 面試題 02.07. 連結串列相交LeetCode面試題
- 【LeetCode連結串列#9】圖解:兩兩交換連結串列節點LeetCode圖解
- [連結串列]leetcode138-複製帶隨即指標的連結串列LeetCode指標
- leetcode 92 反轉連結串列ⅡLeetCode
- LeetCode-143-重排連結串列LeetCode
- LeetCode 86. 分隔連結串列LeetCode
- 連結串列-雙向非通用連結串列
- 連結串列入門與插入連結串列
- 資料結構-單連結串列、雙連結串列資料結構
- 連結串列
- [連結串列]leetcode1019-連結串列中的下一個更大節點LeetCode
- 【C++ 資料結構:連結串列】二刷LeetCode707設計連結串列C++資料結構LeetCode
- leetcode題目解析(js)--連結串列LeetCodeJS
- 【Leetcode】61.旋轉連結串列LeetCode
- leetcode206. 反轉連結串列LeetCode
- leetcode 206.反轉連結串列LeetCode
- LeetCode 234. 迴文連結串列LeetCode
- 力扣(LeetCode) -143 重排連結串列力扣LeetCode
- [leetcode 92] 反轉連結串列 IILeetCode
- leetcode 206. 反轉連結串列LeetCode
- 每日leetcode——160. 相交連結串列LeetCode
- LeetCode入門指南 之 連結串列LeetCode