雙指針技巧與鏈表的經典問題
虛心接受批評和指正,互相成就,共勉!
從這篇文章你會收穫什麼?
- 雙指針技巧;
- 雙指針的常見用法;
- 經典問題;
- 總結
目錄
- 雙指針技巧;
- 環形鏈表
- 環形鏈表II
- 相交鏈表
- 刪除鏈表的倒數第N個節點
- 注意
指針:c語言中指針變量是用來存放內存地址的變量
兩種常用雙指針技巧的情景
- 兩個指針
從不同位置出發
:一個從起點開始,一個從終點開始; - 兩個指針
以不同速度出發
:一個指針快一些,一個指針慢一些;
在單鏈表中,不同位置出發毫無意義,但是不同速度出發,卻可以讓我們找到一些規律,這種技巧也叫快慢指針
話不多說,我們上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;
}
注意
我們要注意一些事情:
- 在調用 next 字段之前,始終檢查節點是否爲空。
獲取空節點的下一個節點將導致空指針錯誤。例如,在我們運行 fast = fast.next.next 之前,需要檢查 fast 和 fast.next 不爲空。
- 仔細定義循環的結束條件。
運行幾個示例,以確保你的結束條件不會導致無限循環。在定義結束條件時,你必須考慮我們的第一點提示。
複雜度分析
-
空間複雜度分析容易。如果只使用指針,而不使用任何其他額外的空間,那麼空間複雜度將是 O(1)。但是,時間複雜度的分析比較困難。爲了得到答案,我們需要分析運行循環的次數。
-
在前面的查找循環示例中
- 假設我們每次移動較快的指針 2 步,每次移動較慢的指針 1 步。
- 如果沒有循環,快指針需要 N/2 次才能到達鏈表的末尾,其中 N 是鏈表的長度。
- 如果存在循環,則快指針需要 M 次才能趕上慢指針,其中 M 是列表中循環的長度。
- 顯然,M <= N 。所以我們將循環運行 N 次。對於每次循環,我們只需要常量級的時間。
- 因此,該算法的時間複雜度總共爲 O(N)。
下一篇,我們來介紹一下雙鏈表的原理,及Js的基本實現,共勉!