1. 程式人生 > 其它 >第一章--核心套路篇 之 雙指標技巧框架

第一章--核心套路篇 之 雙指標技巧框架

技術標籤:資料結構與演算法筆記

雙指標可以分成兩類,一類是快慢指標,一類是左右指標,前者主要解決連結串列中的問題,比如典型的是判定連結串列中是否存在環;後者主要是解決陣列或者字串中的問題,比如二分搜尋

快慢指標的常見演算法

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 km,也就是說從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. 滑動視窗演算法

嚴格來說,其實它是快慢指標在陣列或字串中的應用,如果掌握了滑動視窗演算法,就可以解決一大類字串匹配的問題,詳見後序的 滑動視窗演算法框架