地址:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
分析:
- 要刪除一個結點,我們知道,需要來到待刪除結點的上一個結點。
- 要刪除倒數第
N
個結點,因此我們就得站在倒數第N + 1
個結點,然後修改它的next
指針指向。
(假裝這裏有動畫,我太懶了)
- 爲此,我需要遍歷一遍鏈表,得到鏈表的額長度
len
,找到倒數第N + 1
個結點,就有得從鏈表的起始結點開始遍歷,那麼這裏要走多少步呢?我們不是靠猜。畫出一個具體的圖,就很清晰。
一共 6 個結點,N = 2
,倒數第 2 個結點,就是正數第 5 個結點,從起始結點開始,需要走 3
步;
一共 6 個結點,N = 1
,倒數第 1 個結點,就是正數第 6 個結點,從起始結點開始,需要走 4
步;
從起始結點開始,需要走 len - N - 1
步。
那麼問題又來了。
一共 6 個結點,N = 6
,倒數第 6 個結點,就是正數第 0 個結點,第 0 個結點,從起始結點開始,需要走 -1
步;,是沒有意義的,我們要刪掉的就是起始結點本身。當然我們可以對這種情況單獨做判斷,但事實上,處理鏈表起始結點的邊界問題,有一個經典的技巧,那就是設置虛擬頭結點。
虛擬頭結點
- 虛擬頭結點的存在非必要,但是可以簡化我們對鏈表問題的討論,這個技巧其實我們在插入排序的時候用過,也叫「哨兵」,只不過在鏈表中的哨兵是我們認爲引入的,它不參與鏈表的業務,只起到佔位和迴避邊界條件討論的作用;
- 在鏈表的實現中,我們一般也都會初始化“虛擬頭結點”。這是非常常見的技巧。仍然是在刪除結點的任務中,設想一下,如果鏈表刪到都沒有結點了,我怎麼還能引用到鏈表呢,又怎麼在鏈表的末尾添加結點呢。
有了虛擬頭結點以後,這道題的解決方案就可以變成:
1、先遍歷輸出鏈表的長度;
2、創建虛擬頭結點,接在原來鏈表的頭部(這一步如果大家看教科書的話,也叫做“頭插法”);
3、然後從虛擬頭結點開始遍歷,走 len - N
步,這時候,就不要減 1 了,需要多走一步,同樣,對走幾步想不清楚的朋友,還是建議在紙上寫寫畫畫,遍不難得出正確答案。
- 注意:這裏我們從虛擬頭結點開始走,就是爲了避免那個最極端的情況。還是 6 個結點,刪除倒數第 6 個,也就是第 1 個,我們走 0 步,也就是站在虛擬頭結點處,修改結點的
next
指針指向即可。怎麼樣,是不是很神奇。
在這裏再次強調:
1、虛擬頭結點是幾乎所有鏈表實現裏的實現技巧,如果要求你實現一個鏈表,一般我們都會實現帶有虛擬頭結點的鏈表;
不管是單鏈表、雙鏈表、循環鏈表、循環雙鏈表,均是如此;
2、做「力扣」裏的問題,因爲「力扣」是以結點爲視角看待鏈表的,因此虛擬頭結點有的時候非必要,但我們常常需要,當你覺得需要分類討論的時候,這個分類討論的時機往往就在邊界,不妨考慮一下,設置一個虛擬頭結點可能就能避免分類討論。
在上面的步驟中,有一點不太爽的地方是,我們需要遍歷一次鏈表,數出鏈表的長度。然後,再從頭開始遍歷。如果這個鏈表比較長,待刪除的結點正正好在末尾,此時就相當於得跑兩遍鏈表。而影響時間性能的原因是,我必須得等第一次鏈表遍歷完成以後,才能開始第 2 次遍歷。
下面再介紹一個在鏈表問題中,也是比較常見的技巧。那就是“快慢指針”。事實上也是雙指針,只不過它們是同向移動的,不過不是在數組中,也不是滑動窗口問題。
快慢指針
對於這道題,我們就讓一個指針先走 N + 1
步,注意是仍然是從虛擬頭結點開始走,然後再讓一個指針從虛擬頭結點開始,和之前的那個指針同步移動,直到之前的指針走到了鏈表的末尾,慢指針就剛剛好來到了倒數第 N + 1
個結點。
(至於爲什麼是從虛擬頭結點開始走 N + 1
步,這一個操作沒有想明白的朋友依然是建議自己畫一個圖,舉幾個簡單的例子幫助分析,找出規律,一點都不難。)
這樣做的好處是,兩個指針可以一起走,相當於我們實現了一個並行的操作,節約了一些時間。
下面我們來看一下代碼。
- 注意:快指針可以移動的條件。
Java 代碼:
/**
* 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 dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode fastNode = dummyNode;
for (int i = 0; i < n + 1; i++) {
fastNode = fastNode.next;
}
ListNode slowNode = dummyNode;
while (fastNode != null) {
fastNode = fastNode.next;
slowNode = slowNode.next;
}
// 此時 slowNode 來到了待刪除的結點的上一個結點
ListNode deleteNode = slowNode.next;
slowNode.next = deleteNode.next;
deleteNode.next = null;
return dummyNode.next;
}
}