双指针可以分成两类,一类是快慢指针
,一类是左右指针
,前者主要解决链表中的问题,比如典型的是判定链表中是否存在环;后者主要是解决数组或者字符串中的问题,比如二分搜索
快慢指针的常见算法
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. 滑动窗口算法
严格来说,其实它是快慢指针在数组或字符串中的应用,如果掌握了滑动窗口算法,就可以解决一大类字符串匹配的问题,详见后序的 滑动窗口算法框架