第一章--核心套路篇 之 双指针技巧框架

双指针可以分成两类,一类是快慢指针,一类是左右指针,前者主要解决链表中的问题,比如典型的是判定链表中是否存在环;后者主要是解决数组或者字符串中的问题,比如二分搜索

快慢指针的常见算法

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. 滑动窗口算法

严格来说,其实它是快慢指针在数组或字符串中的应用,如果掌握了滑动窗口算法,就可以解决一大类字符串匹配的问题,详见后序的 滑动窗口算法框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值