LeetCode-連結串列
文章目錄
摘要
這篇文章主要是記錄一下在LeetCode刷題過程中,與“連結串列“相關的題目。實際上刷的題數肯定不止,但要慢慢整理,寫好思路跟程式碼。所以,慢慢來吧。
一. 前言
連結串列是一種常用資料結構,其題型也是很有辨識度。對於連結串列的基礎知識在這裡也不必多說,直接整理一下LeetCode上比較經典的連結串列題目吧。很多連結串列的題目,看起來很簡單,但自己動手就會發現很多問題。所以一定要親自去實現一次,不能僅僅停留在讀懂解題思路的階段。
連結串列問題通常都需要迴圈遍歷,有時候需要判斷是否為null。那麼到底什麼情況是判斷head != null
還是head.next != null
呢?這種實際上也是程式設計的基本功。當你在呼叫一個物件的方法,你需要確保它不為空,否則就會丟擲那每一個Java程式設計師都會碰到的NullPointerException。如果你的程式碼裡用到了,比如current = current.next
,你需要確保的是current不為空,這樣就不會導致空指標錯誤。如果你的程式碼裡用到了current = current.next.next
,這時候首先你要確保current不為空,其次current.next也不為空。
對於頭結點的處理,通常都比較特殊,因為當我們改變了頭結點時候,後續return就很麻煩。這時候可以新建一個我們自定義的“偽頭結點"dummy,並且使得Dummy.next = head
。這樣不管後面head是否發生改變,只需要最後return的是dummy.next即可,即將頭節點普通化,不再需要認為頭節點是特殊的節點。但是dummy的值要巧妙設定,不能影響後續的處理。
而且,切記不要直接操作head,雖然有一些題目無關緊要,但通常我們只想遍歷,如果直接head = head.next
,那麼整個連結串列都會隨著遍歷而改變了。正確的做法是建立一個變數,比如current,指向head,然後改變這個current。
對於連結串列的問題,如果不能直接想出來,那麼就多畫圖吧,畫圖就很好理解了。
二. LeetCode 面試題02.01. 移除重複節點
題目描述: 編寫程式碼,移除未排序連結串列中的重複節點。保留最開始出現的節點。題目連結
分析:如果使用O(n)的空間複雜度,那麼很簡單,只需要建立一個雜湊表。如果已經存在該節點的值,便跳過,刪除。如果不能使用額外的空間,也就是O(1)的空間複雜度,那麼就需要用雙指標進行兩層遍歷,此時時間複雜度為O(n ^ 2)。
程式碼①: (O(n)時間複雜度,O(n)空間複雜度)
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
Set<Integer> set = new HashSet<>();
ListNode current = head, prev = head;
while (current != null) {
if (set.contains(current.val))
prev.next = current.next;
else {
set.add(current.val);
prev = current; // 如果contains,就不需要更改prev
}
current = current.next;
}
return head;
}
}
程式碼②: (O(n ^ 2)時間複雜度,O(1)空間複雜度)
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head; // 創造一個dummy頭結點
ListNode current = head, prev = dummy, helper;
while (current != null) {
helper = head;
boolean flag = false;
while (helper != current) { // 檢查current前面是否有重複
if (helper.val == current.val) {
flag = true;
break;
}
helper = helper.next;
}
if (flag)
prev.next = current.next;
else
prev = current; // 如果contains,就不需要更改prev
current = current.next;
}
return dummy.next;
}
}
三. LeetCode 206. 反轉連結串列
描述:反轉一個單連結串列。題目連結
分析:直接定義一個prev變數,用於記錄當前值的前一個值。至於什麼時候設定prev,當然是在上一輪迴圈的時候。在當前迴圈要進行的操作就是:current.next = prev
①,這樣就使得當前的current指向了上一個值。可是我們需要遍歷,這時候我們遍歷所用的current = current.next
已經不可以了,因為current.next已經改變。所以我們還要增加一個next變數,在執行①之前先記錄current.next的值,然後執行完①,就可以更改prev的值,並且將current指向原本的下一個值。(同樣地,修改prev的操作需要在修改current前進行)。只要清晰整個了邏輯,程式碼就水到渠成。
迭代程式碼:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode current = head;
ListNode prev = null; // head的前一個是null
while (current != null) {
ListNode next = current.next; // 先記錄current.next
current.next = prev;
prev = current; // prev改為指向當前的current
current = next; // current指向原本的current.next
}
return prev; //最後current為null,prev就是最後的元素
}
}
這是迭代的版本,如果使用遞迴,程式碼更簡潔,但邏輯要更加清晰。遞迴的關鍵在於反向工作。假設列表的其餘部分已經被反轉,現在我該如何反轉它前面的部分?
假設列表為:
n(1)→…→n(k−1)→(nk)→n(k+1)→…→n(m)→∅
若從節點 n(k+1) 到 n(m) 已經被反轉,而我們正處於 n(k)。
n(1)→…→n(k−1)→(nk)→n(k+1)←…←n(m)
我們希望n(k+1)的下一個節點指向n(k),所以: n(k).next.next = nk
除此之外,n1的下一個必須指向null。否則當連結串列長度為2時,會形成迴圈,導致出錯。
遞迴程式碼:
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode rest = reverseList(head.next); // 把rest想成一個整體,rest實際上是原本的tail
head.next.next = head; // 注意此時head.next是rest的尾結點
head.next = null; // 避免連結串列成環
return rest;
}
}
四. LeetCode 160. 相交連結串列
題目描述:編寫一個程式,找到兩個單連結串列相交的起始節點。題目連結
分析: 尋找交點,如果我們要使用兩個相同速度的指標,那麼就是要確保在某一個時候,二者走的距離相同,並且會在交點相遇。假設二者存在交點,那麼這兩個連結串列的差別主要在於交點前的長度不同(相交之後是公共的連結串列部分)。那麼很容易想到,只要讓兩個指標都同時遍歷了一次這兩段交點前部分即可。所以思路:讓指標到達終點時,將指標指向另一段單連結串列的起點,然後繼續前行。這樣保證兩個指標會在交點相遇。同時,這種“指向起點”的操作只需要進行1次,如果無法相遇,說明兩個單連結串列沒有交點。
程式碼:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode currentA = headA, currentB = headB;
boolean flag1 = false, flag2 = false; // 記錄二者是否都到達了一次end
while (currentA != null && currentB != null) {
if (currentA == currentB)
return currentA; // 相交的結點
currentA = currentA.next;
currentB = currentB.next;
if (currentA == null && !flag1) {
currentA = headB; // A跳轉到headB的開頭
flag1 = true;
}
if (currentB == null && !flag2) {
currentB = headA;
flag2 = true; // 確保只跳轉一次
}
}
return null;
}
}
這段程式碼比較直接,完全就是按照解題思路寫的。但其實還有更簡便的方法,實際上只要兩個指標指向同一個位置的時候,此處就是交點,當然到達終點依然會進行一次跳轉操作。所以此時的迴圈結束條件是curentA == currentB
。如果沒有交點該如何?這時候兩個指標必定會同時到達尾部,此時二者都為null,所以同樣是返回currentA或者currentB即可。
簡潔版程式碼:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while(pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
同時這道題有點像,如果一個連結串列有環,返回環的入口(雙指標文章裡有提到)。那麼這裡我們同樣可以構造出一個環:遍歷連結串列B,直到終點last,然後將last.next == headA
。這時候對於單連結串列A來說,如果有交點,那麼就存在環。如果無交點,就會構成一個環,而環入口就是二者的交點。如果fast最後會到達null,或者fast.next為null,說明不成環,無交點。如果存在環,二者一定在某一時刻相遇,此時只要將fast或者slow置於起始點(headA),然後以同樣的速度繼續前行,二者會在交點處/環入口相遇(證明過程見“雙指標”一文)。同時,題目不允許對原連結串列進行修改,所以在返回之前,還得把last.next改回去,即last.next = null
構造環:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null)
return null;
ListNode last = headB;
while (last.next != null)
last = last.next;
last.next = headB; // 此時如果有交點,那麼A存在環
ListNode fast = headA, slow = headA;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (slow == fast) {
slow = headA;
break;
}
}
if (fast == null || fast.next == null) {
last.next = null; // reset
return null; // 無交點
}
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
last.next = null; // reset
return slow; // or fast
}
}
五. LeetCode 21. 合併兩個有序連結串列
題目描述:將兩個升序連結串列合併為一個新的升序連結串列並返回。新連結串列是通過拼接給定的兩個連結串列的所有節點組成的。 題目連結
分析:直接比較兩個連結串列,選擇更小的值新增到新連結串列中。直到其中一條為null,直接把另一條直接新增即可。同時可以建立一個dummy頭結點,便於操作。
程式碼:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(1);
ListNode res = new ListNode(100);
dummy.next = res;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
res.next = l1;
l1 = l1.next;
}
else {
res.next = l2;
l2 = l2.next;
}
res = res.next;
}
if (l1 == null)
res.next = l2;
else
res.next = l1;
return dummy.next.next;
}
}
六. LeetCode 83. 刪除排序連結串列中的重複元素
題目:給定一個排序連結串列,刪除所有重複的元素,使得每個元素只出現一次。題目連結
分析:最簡單的方法當然就是HashSet了,但那樣就浪費了題中的條件“排序連結串列”。因為是排序,所以我們只要與前一個元素相比,只要相同,那麼就跳過當前的元素。值得注意的是,使用pre來代表前一個元素,但pre並不是每一次都要改變。比如測試用例[1, 1, 1],當current與pre相同的時候,這時候不需要改變pre。本題的唯一難點。
程式碼:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null)
return head;
ListNode prev = head;
ListNode current = head.next; // 從第二個開始
while (current != null) {
if (prev.val == current.val)
prev.next = current.next;
else
prev = current; // 相同的情況下,無須改變prev
current = current.next;
}
return head;
}
}
七. LeetCode 19. 刪除連結串列的倒數第N個結點
描述: RT(保證給定的n的有效的)題目連結
分析:如果是進行兩趟掃描,那麼思路很簡單,第一遍先獲取連結串列的長度,然後就可以知道倒數第n個是順序的第幾個。這道題的難點是如何順序第一遍就知道正確的位置。可以使用兩個指標,先讓其中一個走n步,那麼當這個先行的指標走到結尾,後行的指標就正處於倒數第n個的位置。
程式碼① (兩趟掃描):
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
int length = 0;
ListNode current = head;
while (current != null) {
length++;
current = current.next;
}
current = dummy; // 從dummy開始,不然刪除head就比較麻煩
int tmp = 0;
while (tmp <= length - n - 1) {
current = current.next;
tmp++;
}
current.next = current.next.next;
return dummy.next;
}
}
程式碼② (一趟掃描):
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode fast = dummy;
for (int i = 0; i <= n; i++) // n從1開始,要多走一步
fast = fast.next;
ListNode slow = dummy;
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
八. LeetCode 24. 兩兩較換連結串列中的結點
描述:給定一個連結串列,兩兩交換其中相鄰的節點,並返回交換後的連結串列。題目連結
你不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。
分析:直接定義三個變數,pre,first,second。我們需要設定:second.next = first
,first.next = second.next
,可以提前保留second.next這個值。操作完成之後,更換pre,first,second的值。這裡要注意是否會為null的情況。因為first = next
,second = next.next
,顯然要判斷next是否為null。如果為null,說明後面沒有元素,無須繼續操作,可以結束迴圈。直覺上很容易想,如果還剩下一個元素應該怎麼辦?此時next不為null,但next.null為null。這時候仍然可以結束迴圈,所以如果你願意,你可以把這個判斷條件更改為:
if (next != null || next.next != null)
。但實際上,只剩下1個元素的情況下,實際上就是second為null,這時候再迴圈一次其實就在while迴圈裡結束了,所以是一樣的。(對於while跟if的結束條件,有很多種寫法,理解就好)
程式碼:
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
ListNode first = head;
ListNode second = head.next;
while (first != null && second != null) {
ListNode next = second.next;
pre.next = second;
second.next = first;
first.next = next;
pre = first;
first = next;
if (next == null)
break;
second = next.next;
}
return dummy.next;
}
}
以上是迭代演算法,同樣地,也寫出遞迴演算法,就跟206題一樣。遞迴要考慮的是3個部分:
①終止條件 ②返回值 ③本級遞回應該做什麼 (此處參考“網戀教父”的的文章 )
主要是把整個連結串列看作3個部分,head,next,已經處理完的部分。然後經過操作變成:
next,head,已經處理完的部分
遞迴程式碼:
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode next = head.next;
head.next = swapPairs(next.next); // 一個整體
next.next = head;
return next;
}
}
九. LeetCode 445. 兩數相加 Ⅱ
題目描述: 給定兩個非空連結串列來代表兩個非負整數。數字最高位位於連結串列開始位置。它們的每個節點只儲存單個數字。將這兩數相加會返回一個新的連結串列。題目連結
你可以假設除了數字 0 之外,這兩個數字都不會以零開頭。
如果輸入連結串列不能修改該如何處理?換句話說,你不能對列表中的節點進行翻轉。
分析:對連結串列進行倒序讀取是很麻煩的操作,所以一下子就想到了用兩個陣列/棧來重新維護資料。實現的過程也遇到了一點小麻煩,效率也還可以,排前面的程式碼也是這種思路,所以看來這就是最終的方法了。這裡顯然是用棧來維護最好(LinkedList),因為我們只需要對首尾的元素進行操作。值得注意的是,當兩個棧都為空,如果此時還有進位,那麼還需要額外new一個值為1的頭結點。
程式碼:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
LinkedList<Integer> list1 = new LinkedList<>();
LinkedList<Integer> list2 = new LinkedList<>();
ListNode h1 = l1, h2 = l2;
while (h1 != null) {
list1.addLast(h1.val);
h1 = h1.next;
}
while (h2 != null) {
list2.addLast(h2.val);
h2 = h2.next; // 資料存放到list1和list2中
}
ListNode next = null;
int increment = 0;
while (true) {
int val1 = list1.size() == 0 ? 0 : list1.removeLast();
int val2 = list2.size() == 0 ? 0 : list2.removeLast();
int sum = val1 + val2 + increment;
if (sum >= 10) {
sum %= 10;
increment = 1;
}
else
increment = 0;
ListNode current = new ListNode(sum);
current.next = next;
next = current;
if (list1.size() == 0 && list2.size() == 0) {
if (increment == 0)
return current;
ListNode head = new ListNode(1); // 迴圈結束,但還有進位的情況
head.next = current;
return head;
}
}
}
}
十. LeetCode 234. 迴文連結串列
描述: 判斷一個連結串列是否為迴文連結串列,要求時間複雜度為O(n),空間複雜度為O(1)。題目連結
分析: 空間複雜度為O(1),所以不能使用List等額外的資料結構。對於迴文連結串列,可以理解為中點左右的值都相等。所以我們可以先找到連結串列的中點,然後將後半段反轉,這時候逐個比較後半段與連結串列開頭的值。其實LeetCode裡就有尋找中點的題,也是雙指標。但該題題目要求寫道,如果有兩個中間結點,那麼返回第二個結點。這裡是一個細節。我們獲得了中間結點之後,是不需要把中間結點與head比較的,所以我們會是從mid.next與head開始相比,這就導致了奇偶數情況不同:如果是奇數,可以直接mid.next即可。如果是偶數,因為那樣的解法是獲得第二個中點,顯然我們應該獲得“第一個“作為中點。這時候需要在尋找中點的做法裡做一些改動。
程式碼:
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null)
return true;
ListNode fast = head, slow = head;
while (fast.next != null && fast.next.next != null) {
// 這會使得偶數連結串列返回第一個中間結點
// 如果想返回第二個中間結點, 應該用 fast != null && fast.next != null
fast = fast.next.next;
slow = slow.next;
}
slow = reverse(slow.next);
while (slow != null) {
if (slow.val != head.val)
return false;
slow = slow.next;
head = head.next;
}
return true;
}
public ListNode reverse(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
}
相關文章
- 【LeetCode-連結串列】面試題-反轉連結串列LeetCode面試題
- LeetCode- 19 刪除連結串列的倒數第N個節點LeetCode
- 連結串列 - 單向連結串列
- 連結串列-迴圈連結串列
- 連結串列-雙向連結串列
- 連結串列4: 迴圈連結串列
- 連結串列-雙向通用連結串列
- 連結串列-單連結串列實現
- 連結串列-雙向非通用連結串列
- 【LeetCode】->連結串列->通向連結串列自由之路LeetCode
- 連結串列入門與插入連結串列
- Leetcode_86_分割連結串列_連結串列LeetCode
- 資料結構-單連結串列、雙連結串列資料結構
- 連結串列
- LeetCode-Python-86. 分隔連結串列(連結串列)LeetCodePython
- 單連結串列建立連結串列出現問題
- **203.移除連結串列元素****707.設計連結串列****206.反轉連結串列**
- php連結串列PHP
- 連結串列逆序
- 2、連結串列
- 連結串列(python)Python
- 重排連結串列
- 單連結串列
- 分割連結串列
- (一)連結串列
- 資料結構與演算法——連結串列 Linked List(單連結串列、雙向連結串列、單向環形連結串列-Josephu 問題)資料結構演算法
- 資料結構之連結串列:206. 反轉連結串列資料結構
- 反轉連結串列、合併連結串列、樹的子結構
- [連結串列】2.輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。[多益,位元組考過]
- 程式碼隨想錄第3天 | 連結串列 203.移除連結串列元素,707.設計連結串列,206.反轉連結串列
- 【圖解連結串列類面試題】移除連結串列元素圖解面試題
- 【圖解連結串列類面試題】環形連結串列圖解面試題
- 資料結構實驗之連結串列二:逆序建立連結串列資料結構
- 資料結構--陣列、單向連結串列、雙向連結串列資料結構陣列
- 資料結構實驗之連結串列九:雙向連結串列資料結構
- Linux核心連結串列Linux
- 構建連結串列
- 反轉連結串列