常用算法-双指针系列
双指针问题
本篇博客主要讲的内容都和双指针系列有关,主要介绍双指针,快慢指针,滑动窗口等技法在代码中的应用。关于滑动窗口,后面会再出一篇博客对其进行详解,本篇主要是入门
什么是双指针?
首先我们看一个问题,如何返回链表的倒数第K个节点?
可能你立马会想到,我直接反转链表,然后顺序遍历第K个节点不就行了?没错,但是来看双指针怎么做的
用双指针怎么做?准确说应该是快慢指针,两个指针一开始都指向头结点,
首先,快指针先从链表的head开始往后遍历K个节点,此时慢指针不动,仍然指向head
然后,慢指针和快指针以相同速度同时往后遍历链表,当快指针到达末尾时,慢指针所指的位置就是对应的倒数第K个节点。
(咱们换个场景,好比一个人想测一条路的第倒数39米,那么就先给这个人一把39米长的大刀,刀尖向前,此人手持刀柄,从起点开始跑,当刀尖到了路的尽头,那么刀柄就是第倒数39米,在这一题中,刀尖就是快指针,刀柄就是慢指针)
瞬间理解了双指针对吧?(其实算法就是生活中的小妙招的合集),当然双指针问题有很多版本,什么快慢指针,合并指针,滑动窗口等等。下面介绍一些和双指针相关的题目(题目都来自leetcode,可以根据题号搜索):
下面是leetcode上关于双指针系列的一些题目,大部分是初中级,有基础的朋友可以直接点击链接或者去leetcode搜索对应题号开始做题。喜欢看我内容的朋友可以继续看后面内容,后面主要是个人理解和题解。
简单篇
167 两数相加
88 合并两个有序数组
680. 验证回文字符串Ⅱ
141. 环形链表
142. 环形链表 II
680. 验证回文字符串 Ⅱ
中级篇
633. 平方数之和
524. 通过删除字母匹配到字典里最长单词
3. 无重复字符的最长子串
困难篇
76. 最小覆盖子串
接下来对上面习题讲解
NO.167 两数相加 [简单]
题目如下
给定一个已按照升序排列的整数数组numbers,请你从数组中找出两个数满足相加之和等于目标数target。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
题解(有很多种解法,这里主要介绍双指针的解法)
咱们这么理解,牛郎从鹊桥(数组)的这边出发,织女从那边出发,他们每走一步就算一下他们两路标的和,如果这个和比目标值大,那就织女走一步,如果比目标值小,那么牛郎走一步,直到遇到正好和目标值一样大的位置,如果牛郎和织女都碰面了还没遇到目标值,那就说明无解了。
同理对于排序好的数组,
那么我们可以使用两个指针,一个从左往右跑,一个从右往左跑,两个指针的值相加,得到一个和 X
X < target ?左指针右移 右指针不动
x > target ? 左指针不动,右指针左移
x == target? 返回结果
代码如下
class Solution(object):
def twoSum(self, numbers, target):
"""
:numbers: 排好序的数组
:target: 目标值
:return: 对应下标的集合
"""
left, right = 0, len(numbers) - 1
while left <= right:
s = numbers[left] + numbers[right]
if s == target:
return [left + 1, right + 1]
elif s < target:
left += 1
else:
right -= 1
NO.88 合并两个有序数组 [简单]
题目
给你两个有序整数数组nums1 和 nums2,请你将 nums2 合并到nums1中,使 nums1 成为一个有序数组。
初始化nums1 和 nums2 的元素数量分别为m 和 n 。你可以假设nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
题解
这个题就很简单了啊,哥哥弟弟从兜里掏钱,每个人的钱排好序了,先掏小的,掏出来两个人比较大小,然后按顺序扔到桶里面,最后掏完了,把这个桶给哥哥不就行了?
两个指针分别遍历两个数组,然后取值比较大小,按顺序存到临时数组中,最后临时数组赋值给nums1就行了。
代码也很简单
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
last_list = list()
p1, p2 = 0, 0
while p1 < m or p2 < n:
if p1 == m:
last_list.append(nums2[p2])
p2 += 1
elif p2 == n:
last_list.append(nums1[p1])
p1 += 1
elif nums1[p1] < nums2[p2]:
last_list.append(nums1[p1])
p1 += 1
else:
last_list.append(nums2[p2])
p2 += 1
nums1[:] = last_list
NO.141 判断链表有没有环 [中等] -- 快慢指针
题解
这道题其实有很多解法,因为主讲双指针,那我就只介绍双指针解法。
快慢指针解法:
通俗的讲,假设这个链表是一个环形链表(特殊情况哈),就好比如如何用两辆车判断地球是圆的?
两辆车沿着同一个方向跑,一辆车一天跑一千公里,一个辆车一天跑20公里,如果跑的快的车追上了跑得慢的车,那么是不是就可以判断地球是圆的了?(夸父逐日)
代码:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head or not head.next:
return False
slow ,fast= head,head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
NO.142 环列表之返回环的节点
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:你是否可以使用 O(1) 空间解决此题?
题解 (同样使用双指针咯)
这个核心思路还是用快慢指针,和上一题很像,但是需要返回入环的第一个节点,这个就需要用点心思了
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
fast, slow = head, head
while True:
if not (fast and fast.next): return
fast, slow = fast.next.next, slow.next
if fast == slow: break
fast = head
while fast != slow:
fast, slow = fast.next, slow.next
return fast
NO.3 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
题解
这也是快慢指针的一种解法,江湖人称滑动窗口,具体操作如下:
代码
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 如果字符串为空字符串 返回最长子串为0
if s == "":
return 0
# 用set来记录滑动窗口类的所有字符
char_set = set()
n = len(s)
# 设置滑动窗口的左右边界
left,right = 0,0
max_length = 0
for right in range(n):
# 当有新字符加入,和窗口里字符有重复 左窗口右移一位 直到不存在
while s[right] in char_set:
char_set.remove(s[left])
left += 1
# 将新字符加入滑动窗口
char_set.add(s[right])
# 更新最大子串的长度
max_length = max(max_length,len(char_set))
return max_length
NO.633 平方数之和[中等]
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c
示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
示例 2:
输入:c = 3
输出:false
示例 3:
输入:c = 4
输出:true
示例 4:
输入:c = 2
输出:true
示例 5:
输入:c = 1
输出:true
题解
这玩意不就是牛郎织女双指针嘛?
只是右指针的边界我们可以缩小到 C开平方根的数取整
比如给定 C = 34,那么右指针为C开方 = 5, 然后就是一起走了
代码
import math
class Solution:
def judgeSquareSum(self, c: int) -> bool:
i,j = 1,int(math.sqrt(c)) # i从1开始算,因为0*0 = 0 无意义
if j* j == c:
return True
while i <= j:
temp = i*i + j*j
if temp == c:
return True
if temp > c:
j = j -1
else:
i = i + 1
return False
NO.680 验证回文字符串 Ⅱ [简单]
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串(左右对称的字符串)。
示例 1:
输入: "aba"
输出: True (因为他本身就是回文字符串)
示例 2:
输入: "abca"
输出: True
解释: 你可以删除c字符。
题解
这道题其实应该算考查贪心算法的,但是用双指针也能做。
同样的,两个指针,一个从左往右,一个从右往前,每次移动后判断内圈是不是回文数(通过反转字符串比较实现)这个不好讲解,直接看代码吧。
代码
class Solution:
def validPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return s[left + 1:right + 1] == s[left + 1:right + 1][::-1] or s[left:right] == s[left:right][::-1]
left += 1
right -= 1
return True
NO.76 最小覆盖子串 [困难] -- 滑动窗口
这道题比较难,看不懂的先略过,咱们优先搞定<困难等级的题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
题解
假设我们子串只有一个字符的情况
这个很好理解
if len(t) == 1 :
return t if t in s else ""
代码
class Solution:
def minWindow(self, s: str, t: str) -> str:
if len(t)==1: return t if t in s else ""
m,n = len(s),len(t)
# count代表需要元素的总个数,dic代表每个元素分别还需要的个数
count,dic = n,{}
for c in t:dic[c] = dic.get(c,0)+1
num,res = m+1,""
i,j = 0,0
while j<m:
if s[j] in dic:
dic[s[j]] -= 1
if dic[s[j]]>=0:
count -=1
while count==0:
if j-i+1<num:
num = j-i+1
res = s[i:j+1]
if s[i] in dic:
dic[s[i]]+=1
if dic[s[i]]>0:
count +=1
i+=1
j+=1
return res
总结
双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。
若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。
若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。