上期我們探討了使用Swift如何破解陣列、字串、集合、字典相關的演算法題。本期我們一起來講講用Swift如何實現連結串列以及連結串列相關的技巧。本期主要內容有:
- 連結串列基本結構
- Dummy節點
- 尾插法
- 快行指標
基本結構
對於連結串列的概念,實在是基本概念太多,這裡不做贅述。我們直接來實現連結串列節點。
1 2 3 4 5 6 7 8 9 |
class ListNode { var val: Int var next: ListNode? init(_ val: Int) { self.val = val self.next = nil } } |
有了節點,就可以實現連結串列了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class List { var head: ListNode? var tail: ListNode? // 尾插法 func appendToTail(val: Int) { if tail == nil { tail = ListNode(val) head = tail } else { tail!.next = ListNode(val) tail = tail!.next } } // 頭插法 func appendToHead(val: Int) { if head == nil { head = ListNode(val) tail = head } else { let temp = ListNode(val) temp.next = head head = temp } } } |
有了上面的基本操作,我們來看如何解決複雜的問題。
Dummy節點和尾插法
話不多說,我們直接先來看下面一道題目。
給一個連結串列和一個值x,要求將連結串列中所有小於x的值放到左邊,所有大於等於x的值放到右邊。原連結串列的節點順序不能變。
例:1->5->3->2->4->2,給定x = 3。則我們要返回 1->2->2->5->3->4
直覺告訴我們,這題要先處理左邊(比x小的節點),然後再處理右邊(比x大的節點),最後再把左右兩邊拼起來。
思路有了,再把題目抽象一下,就是要實現這樣一個函式:
1 |
func partition(head: ListNode?, _ x: Int) -> ListNode? {} |
即我們有給定連結串列的頭節點,有給定的x值,要求返回新連結串列的頭結點。接下來我們要想:怎麼處理左邊?怎麼處理右邊?處理完後怎麼拼接?
先來看怎麼處理左邊。我們不妨把這個題目先變簡單一點:
給一個連結串列和一個值x,要求只保留連結串列中所有小於x的值,原連結串列的節點順序不能變。
例:1->5->3->2->4->2,給定x = 3。則我們要返回 1->2->2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func getLeftList(head: ListNode?, _ x: Int) -> ListNode? { let dummy = ListNode(0) var pre = dummy var node = head while node != nil { if node!.val < x { pre.next = node pre = node! } node = node!.next } return dummy.next } |
現在我們解決了左邊,右邊也是同樣處理。接著只要讓左邊的尾節點指向右邊的頭結點即可。全部程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func partition(head: ListNode?, _ x: Int) -> ListNode? { // 引入Dummy節點 let prevDummy = ListNode(0) var prev = prevDummy let postDummy = ListNode(0) var post = postDummy var node = head // 用尾插法處理左邊和右邊 while node != nil { if node!.val < x { prev.next = node prev = node! } else { post.next = node post = node! } node = node!.next } // 左右拼接 post.next = nil prev.next = postDummy.next return prevDummy.next } |
注意這句
post.next = nil
,這是為了防止連結串列迴圈指向構成環,是必須的但是很容易忽略的一步。剛才我們提到了環,那麼怎麼檢測連結串列中是否有環存在呢?
快行指標
筆者理解快行指標,就是兩個指標訪問連結串列,一個在前一個在後,或者一個移動快另一個移動慢,這就是快行指標。所以如何檢測一個連結串列中是否有環?用兩個指標同時訪問連結串列,其中一個的速度是另一個的2倍,如果他們相等了,那麼這個連結串列就有環了。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func hasCycle(head: ListNode?) -> Bool { var slow = head var fast = head while fast != nil && fast!.next != nil { slow = slow!.next fast = fast!.next!.next if slow === fast { return true } } return false } |
刪除連結串列中倒數第n個節點。例:1->2->3->4->5,n = 2。返回1->2->3->5。
注意:給定n的長度小於等於連結串列的長度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? { guard let head = head else { return nil } let dummy = ListNode(0) dummy.next = head var prev: ListNode? = dummy var post: ListNode? = dummy // 設定後一個節點初始位置 for _ in 0 ..< n { if post == nil { break } post = post!.next } // 同時移動前後節點 while post != nil && post!.next != nil { prev = prev!.next post = post!.next } // 刪除節點 prev!.next = prev!.next!.next return dummy.next } |
這裡還用到了Dummy節點,因為有可能我們要刪除的是頭結點。
總結
這次我們用Swift實現了連結串列的基本結構,並且實戰了連結串列的幾個技巧。在結尾處,我還想強調一下Swift處理連結串列問題的兩個細節問題:
- 一定要注意頭結點可能就是nil。所以給定連結串列,我們要看清楚head是不是optional,在判斷是不是要處理這種邊界條件。
- 注意每個節點的next可能是nil。如果不為nil,請用”!”修飾變數。在賦值的時候,也請注意”!”將optional節點傳給非optional節點的情況。