別說鏈表不重要(二):雙指針巧解鏈表經典問題,環形鏈表、相交鏈表

雙指針技巧與鏈表的經典問題

虛心接受批評和指正,互相成就,共勉!

從這篇文章你會收穫什麼?

  • 雙指針技巧;
  • 雙指針的常見用法;
  • 經典問題;
  • 總結

目錄

  • 雙指針技巧;
    • 環形鏈表
    • 環形鏈表II
    • 相交鏈表
    • 刪除鏈表的倒數第N個節點
  • 注意

指針:c語言中指針變量是用來存放內存地址的變量

兩種常用雙指針技巧的情景

  1. 兩個指針從不同位置出發:一個從起點開始,一個從終點開始;
  2. 兩個指針以不同速度出發:一個指針快一些,一個指針慢一些;

在單鏈表中,不同位置出發毫無意義,但是不同速度出發,卻可以讓我們找到一些規律,這種技巧也叫快慢指針

話不多說,我們上LeetCode上鍊表的經典題去理解快慢指針的作用

環形鏈表

給定一個鏈表,判斷鏈表中是否有環。

爲了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos-1,則在該鏈表中沒有環。

思路

  • 一個有環的鏈表,比作一個環形跑道,跑的快的運動員遲早會再次和跑得慢的運動員相遇
  • 慢指針一次一定1位,快指針一次移動2位,快指針如果跑道了終點,證明無環,否則一定會相遇

代碼

var hasCycle = function(head) {
    if(!head || !head.next) return false;
    var slow = head
    var fast = head.next
    while( slow != fast ){
        if(fast == null || fast.next == null){
            return false
        }
        slow = slow.next
        fast = fast.next.next
    }
    return true
};

環形鏈表II

思路

  • 第一階段,我們先確定當前鏈表是否存在環;
  • 第二階段,如果存在環,我們要確定入口的位置:
    • 入口前節點個數爲a,環節點個數爲b;
    • 快節點走過的節點數 = 2 * 慢節點走過的節點數,即 f = 2s;
    • 快節點走過的節點數 = s + nb(多揍了n倍的環)(相遇時);
    • 由此得出:s = nb(相遇時);
    • 如果說慢指針走到入口的距離 k = a + nb,兩點相遇時s = nb,即再走a步即可;
    • 我們重新確定雙指針(速度要相同),新指針走 a 步應該到達入口,慢指針走a步也會到達入口;
    • 新指針推斷爲 頭節點,兩者相遇的節點爲入口;

代碼

var hasCycle = function(head) {
    if(!head || !head.next) return false;
    var slow = head
    var fast = head.next
    while( slow != fast ){
        if(fast == null || fast.next == null){
            return false
        }
        slow = slow.next
        fast = fast.next.next
    }
    return true
};

相交鏈表

給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null。如果有環,則確定它的入口位置。

思路

  • 定義A、B節點對應headA、headB鏈表
  • 如果兩個鏈表相交,那麼他們的共同點就是相交節點及以後的節點相等;
  • 在未知長度的鏈表中,爲了保證兩個節點走相同的路程。
    • A節點走完後,走B節點,假設A鏈表交點前的節點數尾a
    • B節點走完後,走A節點,假設A鏈表交點前的節點數尾b
    • 如果存在交點:A和B在相遇前都走了 a+共同節點+b,此時A === B,並指向入口位置

代碼

var getIntersectionNode = function(headA, headB) {
    var A = headA
    var B = headB
    while(A || B){
        if(A === B) return A
        A = A ? A.next : headB
        B = B ? B.next : headA
    }
    return null
};

刪除鏈表的倒數第N個節點

思路

這裏利用的思想是,不同位置,相同速度的雙指針思想

  • 倒數N個節點,可以理解成正數第鏈表長度 - N節點。
  • 所以我們要找到頭節點之後的第 鏈表長度 - N - 1個節點
  • 讓它跳過下一節點即可
    • 讓快節點先走N+1步
    • 之後,讓慢節點從頭節點與快節點一起移動,
    • 快節點走到尾端時,慢節點走了 鏈表長度 - N - 1
    • 做刪除操作即可

代碼

var removeNthFromEnd = function(head, n) {
    var current = { val: '', next: head }
    var fast = current
    var slow = current

    for (let i = 1; i <= n + 1; i++) {
        fast = fast.next;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return current.next;
}

注意

我們要注意一些事情:

  1. 在調用 next 字段之前,始終檢查節點是否爲空。

獲取空節點的下一個節點將導致空指針錯誤。例如,在我們運行 fast = fast.next.next 之前,需要檢查 fast 和 fast.next 不爲空。

  1. 仔細定義循環的結束條件。

運行幾個示例,以確保你的結束條件不會導致無限循環。在定義結束條件時,你必須考慮我們的第一點提示。

複雜度分析

  • 空間複雜度分析容易。如果只使用指針,而不使用任何其他額外的空間,那麼空間複雜度將是 O(1)。但是,時間複雜度的分析比較困難。爲了得到答案,我們需要分析運行循環的次數。

  • 在前面的查找循環示例中

    • 假設我們每次移動較快的指針 2 步,每次移動較慢的指針 1 步。
    • 如果沒有循環,快指針需要 N/2 次才能到達鏈表的末尾,其中 N 是鏈表的長度。
    • 如果存在循環,則快指針需要 M 次才能趕上慢指針,其中 M 是列表中循環的長度。
    • 顯然,M <= N 。所以我們將循環運行 N 次。對於每次循環,我們只需要常量級的時間。
    • 因此,該算法的時間複雜度總共爲 O(N)。

下一篇,我們來介紹一下雙鏈表的原理,及Js的基本實現,共勉!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章