1.鏈表和數組的區別
鏈表和數組都是基本的數據結構,但是二者的存儲方式不同。
數組:
數組 在內存中存儲,需要一塊連續的內存空間,對內存的要求較高,比如需要200MB大小的存儲空間,那麼這200MB必須是連續的。
鏈表:
鏈表的存儲不需要連續的內存空間,它是通過指針來指定相應的內存空間,也就是通過“指針”,將一組零散的內存塊串聯起來。所以完全不用擔心連續內存空間以及擴容等問題。
鏈表中的每個結點,都存儲了下一個結點的內存地址的指針。所以鏈表在聲明時,不需要和數組一樣連續的內存空間,但是它需要額外的空間來存儲與之關聯的結點指針,通常就會認爲,鏈表比數組會消耗更多的內存空間。
但是其實在我們正常的編碼過程中,鏈表中存儲的每個結點,其實是遠大於指針存儲所消耗的空間,這點消耗,基本上是可以忽略不計的。
2.鏈表的類型
鏈表根據指向的不同以及每個節點存儲的指針關係,可分爲:
- 單鏈表:每個結點存在一個 next 指針,稱爲後續指針, next 指針指向後一個結點,尾結點的 next 指針,指向 NULL。
- 單向循環鏈表:和單鏈表類似,但是尾結點的 next 指針,指向頭結點。
- 雙向鏈表:在單鏈表的基礎上,爲每個結點增加一個 prev 指針,指向該結點在鏈表中的前驅結點。
- ·雙向循環鏈表:和雙向鏈表類似,但是頭結點的 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.快慢指針的好處
- 查找時只用遍歷一次。
- 空間複雜度可以減小。
- 查找速度可以相對較快。