鏈表的快慢指針(面試常考)

1.鏈表和數組的區別

鏈表和數組都是基本的數據結構,但是二者的存儲方式不同。

數組:
數組 在內存中存儲,需要一塊連續的內存空間,對內存的要求較高,比如需要200MB大小的存儲空間,那麼這200MB必須是連續的。

鏈表:
鏈表的存儲不需要連續的內存空間,它是通過指針來指定相應的內存空間,也就是通過“指針”,將一組零散的內存塊串聯起來。所以完全不用擔心連續內存空間以及擴容等問題。

鏈表中的每個結點,都存儲了下一個結點的內存地址的指針。所以鏈表在聲明時,不需要和數組一樣連續的內存空間,但是它需要額外的空間來存儲與之關聯的結點指針,通常就會認爲,鏈表比數組會消耗更多的內存空間。

但是其實在我們正常的編碼過程中,鏈表中存儲的每個結點,其實是遠大於指針存儲所消耗的空間,這點消耗,基本上是可以忽略不計的。

2.鏈表的類型

鏈表根據指向的不同以及每個節點存儲的指針關係,可分爲:

  1. 單鏈表:每個結點存在一個 next 指針,稱爲後續指針, next 指針指向後一個結點,尾結點的 next 指針,指向 NULL。
  2. 單向循環鏈表:和單鏈表類似,但是尾結點的 next 指針,指向頭結點。
  3. 雙向鏈表:在單鏈表的基礎上,爲每個結點增加一個 prev 指針,指向該結點在鏈表中的前驅結點。
  4. ·雙向循環鏈表:和雙向鏈表類似,但是頭結點的 prev 指向尾結點,而尾結點的 next 指向頭結點,以此形成一個環。

單鏈表是最基本的鏈表,也是面試題中最常考的鏈表,通過單鏈表可以衍生出很多的面試題,而快慢指針就是常考的一類。

3.單鏈表的快慢指針

對於單鏈表,每個結點只有一個存儲下一結點的 next 指針。當我們在查找某個結點的時候,無法和數組一樣通過下標快速定位到待查詢的結點,只能從頭結點開始,通過每個結點的 next 指針,一個個結點的匹配查找。

這就代表了單鏈表循環的一個特點,它在遍歷上無法後悔,遍歷走過了,就是過了,無法回頭(當然這裏可以使用遍歷兩次的方法,但快慢指針可以只遍歷一次)。除非再用一個數據結構去記錄已經遍歷的結點,那就不再是一個單純的單鏈表了。

這就引發出一種解決方案,就是快慢指針,一個指針無法完成任務,那就再引入一個指針。它通過兩個指針 fast 和 slow 指針,讓兩個指針保持一定的間隔規律,達到我們查找的目的。

4.快慢指針的例子

4.1判斷鏈表中是否存在環

題目:已知一個單鏈表的 head,判斷當前鏈表是否存在環。

在鏈表中,如果是在頭節點就已經確定是個環的時候,只需要在循環的時候,判斷尾結點的 next 是否指向頭結點即可。

但是如果在環的入口不確定,它可能從任意一個結點開始形成了環的情況下就不這麼簡單了,這裏我們就可以用快慢指針來進行判斷。

fast 每次走兩步而 slow 每次走一步,如果單鏈表中存在環,fast 以兩倍於 slow 的速度前進,那麼兩個指針,最終一定會進入環,也一定會在環中相遇。

代碼:

 static boolean linkHasCircle(Node head){
 Node fast = head;
 Node slow = head;

 while (fast != null && fast.next != null){
 fast = fast.next.next;
 slow = slow.next;
 // fast 和 slow 遇見,刪除
 if(fast == slow){
 return true;
 }
 }
 return false;
}
4.2求鏈表的中間節點

題目:求單鏈表的中間結點,如果鏈表長度爲偶數,返回兩個結點的任意一個,若爲奇數,則返回中間結點。

這道題當然也可以通過兩次遍歷的方式解決,但是這必然不是我們想要的。還是使用快慢指針,fast 指針每次移動兩步,而 slow 指針每次移動一步,等到 fast 指針指向鏈表尾結點時,slow 指針指向的正好是鏈表的中間結點。

代碼如下:

 static Node theMiddleNode(Node head){

 if(head == null){
 return null;
 }

 Node fast = head;
 Node slow = head;

 // 如果鏈表長度爲偶數,會返回兩個中間節點的第一個
 while (fast != null && fast.next != null){
 fast = fast.next.next;
 slow = slow.next;
 }

 return slow;
}

在中間節點的 while() 語句中,如果想要在鏈表爲偶數時,只需要修改循環的判斷條件即可。

4.3查找倒數第N個節點

題目:已知一個單鏈表的頭結點,找到該鏈表中,倒數第 K 個結點。

這個問題,主要的核心點在於,我們不知道單鏈表的長度,否則在遍歷的時候,用一個 index 就可以解決。當然了,也可以使用遍歷兩次的方法進行查找:第一次遍歷找到單鏈表的長度length,第二次遍歷找到(length-N)個節點就可以了!

但是用快慢指針就只可以遍歷一次。
首先定義兩個指針 fast 和 slow ,fast 和 slow 指針都指向鏈頭 head,然後 fast 指針先向前走 N 步,這樣 slow 和 fast 中間就間隔了 N 個節點,最後 slow 和 fast 同步向前移動,直到 fast 指針走到鏈表末尾,此時 slow 指針,就正好指向倒數第 N 個結點。

代碼如下:

 static Node theKthNode(Node head, int N) {
 if (N < 0) {
 return null;
 }

 Node fast = head;
 Node slow = head;
 int i = N;

 // fast 指針,先走 N 步
 for (; i > 0 && fast != null; i--) {
 fast = fast.next;
 }

 if (i > 0) {
 // 鏈表長度,小於 N
 return null;
 }

 // fast、slow 同步走
 while (fast != null){
 slow = slow.next;
 fast = fast.next;
 }
 return slow;
}

5.快慢指針的好處

  1. 查找時只用遍歷一次。
  2. 空間複雜度可以減小。
  3. 查找速度可以相對較快。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章