第一章--核心套路篇 之 雙指標技巧框架
雙指標可以分成兩類,一類是快慢指標
,一類是左右指標
,前者主要解決連結串列中的問題,比如典型的是判定連結串列中是否存在環;後者主要是解決陣列或者字串中的問題,比如二分搜尋
快慢指標的常見演算法
1.判定連結串列中是否存在環
判斷一個連結串列是否存在環的一個直接了當的方式就是設定一個map表,不斷訪問連結串列,如果訪問的元素出現了重複,那麼連結串列中便存在環,否則如果訪問到了null,便不存在環,虛擬碼如下:
type Node struct{
Data interface{}
Next *Node
}
func hasCycle(head *Node) bool{
visited := make(map[*Node]bool)
for head != nil{
if visited[head]{
return true
}else{
visited[head] = true
head = head.Next
}
}
return false
}
這種方式時間複雜度為 O ( N ) O(N) O(N),但是需要額外的空間來標記訪問的元素。
經典的方式是使用雙指標,一個快指標,一個慢指標,如果不含有環,快指標會遇到nil,並且不會遇上慢指標,如果含有環,那麼快指標遲早會遇到慢指標。
func hasCycle(head *Node)bool{
fast, slow := head, head
for fast != nil && fast.Next != nil{
slow = slow.Next
fast = fast.Next.Next
if fast == slow {
retur true
}
}
return false
}
2.已知連結串列中存在環,確定這個環的起始位置
其實這個問題在上面的問題上進行一定的數學推算即可,如下連結串列中,fast一次移動兩步,slow一次移動一步。
假設慢指標移動k步之後兩個指標相遇,則
因為慢指標移動了 k k k步,則可知快指標移動了 2 k 2k 2k步,所以快指標比慢指標多移動了 k k k步,因為快指標與慢指標最後相遇,所以有 k = n r k=nr k=nr,其中 r r r為環的長度
設相遇點與環的起點距離為 m m m,那麼環的起點與頭節點head的距離為 k − m k-m k−m,也就是說從head前進k-m步就能達到環起點。
同時如果從相遇點繼續前進k-m步,也恰好到達環起點。
所以我們只需要將快慢指標中的任意一個重新指向head,然後兩個指標同時同速出發k-m步就可以相遇,也就是到達環的起點
func getCycle(head *Node) *Node{
// 首先到達相遇點
fast, slow := head, head
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
if fast == slow {
break
}
}
// 將fast重新置為head
// 然後同時同速出發,兩者相遇之時便是環的入點
fast = head
for fast != slow {
fast = fast.Next
slow = slow.Next
}
return fast
}
3.尋找無環單鏈表的中點
一個很直接的方法就是先遍歷一遍連結串列,算出連結串列的長度n,然後再一次遍歷連結串列,不過這一次只走n/2步,這樣便到了連結串列的中點。
雖然上面的這種方式比較直接,但是更加經典的方式就是使用雙指標,我們可以讓快指標一次前進兩步,慢指標一次前進一次,當快指標到達連結串列盡頭的時候,慢指標指向連結串列中間位置。
for fast != nil && fast.Next != nil{
fast = fast.Next.Next
slow = slow.Next
}
return slow
當連結串列的長度是奇數的時候,slow恰好位於終點位置,當連結串列的長度為偶數的時候,slowed位置是中間偏右
尋找連結串列中點的一個重要作用是對連結串列進行歸併排序。
4.尋找單鏈表的倒數第k個元素
這個問題也可以使用快慢指標進行求解,具體的做法是:設定快慢指標,讓快指標先走k步,然後快慢指標開始同速前進,這樣當快慢指標走到連結串列末尾的時候,慢指標所在的位置就是倒數第k個位置(假設連結串列長度大於k)
fast, slow := head, head
// fast 先行k步
for i:=0;i<k;i++{
if fast == nil{
return nil
}
fast = fast.Next
}
for fast != nil{
fast = fast.Next
slow = slow.Next
}
return slow
左右指標的常見演算法
1.二分搜尋
在二分搜尋框架中會詳細闡明如何使用,這裡只突出它的雙指標特性:
// 假設arr遞增
func binarySearch(arr []int, target int) int {
left := 0
right := len(arr) - 1
for left <= right {
mid := (left+right)/2
if arr[mid] == target{
return mid
}
if arr[mid] > target{
right = mid - 1
}
if arr[mid] < target{
left = mid + 1
}
}
return -1
}
2.有序陣列兩數之和
比如說對於一個有序排列的陣列nums和一個目標值target,在nums中需要找到兩個數使得它們相加之和為target,返回這兩個數的索引。比如nums=[2,7,11,15], target = 13,應該返回[0,2]
只要陣列有序,我們就應該想到使用雙指標的技巧進行求解。
// 假設arr是遞增序列
func twoSum(arr []int, target int) []int {
left := 0
right := len(arr) - 1
for left < right {
sum := arr[left] + arr[right]
if sum == target {
return []int{left, right}
}
// 如果結果比目標值大,那麼right縮小
if sum > target {
right--
}
if sum < target {
left++
}
}
return []int{-1, -1}
}
3.反轉陣列
func reverse(arr []int) {
left := 0
right := len(arr) - 1
for left < right {
// 交換位置
arr[left], arr[right] = arr[right], arr[left]
left++
right--
}
}
4. 滑動視窗演算法
嚴格來說,其實它是快慢指標在陣列或字串中的應用,如果掌握了滑動視窗演算法,就可以解決一大類字串匹配的問題,詳見後序的 滑動視窗演算法框架