「力扣」第 19 題:刪除鏈表的倒數第 N 個節點(雙指針)

地址: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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章