素材来自网络
链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。
左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。
滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天
Leetcode76-最小覆盖子串
给你一个字符串
s
,一个字符串t
,请你在字符串s
中找出包含t
所有字母的最小子串,如果s
中不存在涵盖t所有字符的子串则返回空串
注: 对于t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量
分析
由于 t
中可能存在重复字符,所以我们可以借助字典(哈希表)来对t
中的字符和字符出现的次数进行计数,这里我们借助于python
的内置collections
库中的Counter
,其实用字典也一样,这里纯属偷懒
from collections import Counter
need = Counter(t)
对子串做好记录以后我们还需对左右指针维护的滑动窗口内的元素和每个元素的出现次数进行统计
window = Counter() # 初始化空字典用来统计左右指针滑动的过程中滑动窗口内的元素和元素出现的次数
# 双指针
left, right = 0, 0
valid = 0 # 有效匹配字符个数,滑窗内的字符以及该字符出现的次数和==子串中的对应字符valid才+1
start = 0 # 用来记录最小子串的起始覆盖索引
substring_len = float('inf') # s中包含t中所有字符的最小子串的长度,初始化为无穷大,方便后续更新覆盖
右指针向右滑动遍历s串
,窗口扩大,window
记录窗口内的元素及元素出现的次数
对于即将加入窗口的新元素(即right指针所指的元素)
我们不仅需要判断它是否是子串中的元素,如果是窗口内的对应元素数量+1
,
还要判断滑动窗口
中元素的数量和子串中对应元素的数量相等,则有效匹配字符个数valid+=1
while right < len(s):
if s[right] in list(need.keys()):
window[s[right]] += 1 # 窗口内对应元素数量+1
if window[s[right]] == need[s[right]]: # 如果对应元素数量相等则对valid+1
valid += 1
right += 1 # 右指针向→滑动
在右指针向右滑的过程中一旦滑动窗口
和子串
中的元素及元素出现的次数都能匹配上即 valid == len(need)
,说明滑动窗口
已经包含t子串
,此时右指针应该暂停扩大
注:我们要的结果是在扩大窗口时还是缩小窗口还是窗口暂停扩大的时候进行更新?
本题中我们是在窗口暂停扩大的时候判断此时的窗口长度是否小于之前的记录的窗口长度,如果小于则记录下
窗口的起始位置
和更新s中包含t子串的子串长度
while valid == len(need):
if right - left < substring_len:
start = left # 记录下最小子串的起始覆盖索引
substring_len = right - left # 更新s中包含t子串的子串长度
注意上面的黄色字体,上面的代码逻辑发生在右指针向右滑的过程中,所以上面的代码应该
嵌套在右指针向右滑动的大循环中
和移动右指针将元素加入到滑动窗口
中的操作相同,当我们移动左指针收缩窗口将元素从窗口中移出的时候
我们不仅需要判断它是否是子串中的元素,如果是窗口内的对应元素数量 - 1
,
还要判断滑动窗口
中该元素的数量和子串中对应该元素的数量相等,如果相等那么移出该元素必定导致有效匹配字符个数-1即valid -= 1
if s[left] in list(need.keys()):
if window[s[left]] == need[s[left]]: # 因为移出字符在
valid -= 1
window[s[left]] -= 1 # 滑动窗口内对应元素的数量-1
left += 1 # 左指针向→滑动
class Solution:
def minWindow(self, s: str, t: str) -> str:
need, window = Counter(t), Counter()
left, right = 0, 0
valid = 0
start = 0 # 记录最小子串的起始覆盖索引,初始化为0
substring_len = float('inf') # 将长度初始化为无穷大
while right < len(s):
if s[right] in list(need.keys()):
window[s[right]] += 1
if window[s[right]] == need[s[right]]:
valid += 1
right += 1 # 右指针向→滑动
# 经过上面的处理之后已经可以将
while valid == len(need):
if right - left < substring_len:
start = left # 记录下最小子串的起始覆盖索引
substring_len = right - left
if s[left] in list(need.keys()):
if window[s[left]] == need[s[left]]:
valid -= 1
window[s[left]] -= 1
left += 1 # 左指针向→滑动
return s[start:start + substring_len] if substring_len != float('inf') else ""
我们接下来看几道类似的壳子题
Leetcode567-字符串的排列
给你两个字符串
s1
和s2
,写一个函数来判断s2
是否包含s1
的排列。如果是,返回true
;否则,返回false
学过排列组合的同学可以知道
如:ABC
的排列有 ABC
,ACB
,BCA
,BAC
,CAB
,CBA
6种结果
题目要求 s2
中是否包含 s1
的排列,所以我们应在 滑动窗口长度即right - left
等于 s1
子串的长度时停止扩大进行检查
相较于上题,窗口在停止扩大的时候更新结果,我们比较窗口内的有效字符
个数即可断定 s2
中是否包含 s1
中的排列
while right - left == len(s1):
if valid == len(need):
return True
直接附上该题完整解法的Python代码
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
need, window = Counter(s1), Counter()
left, right = 0, 0
valid = 0
while right < len(s2):
if s2[right] in need.keys():
window[s2[right]] += 1
if window[s2[right]] == need[s2[right]]:
valid += 1
right += 1 # 右指针向→滑动
while right - left == len(s1):
if valid == len(need):
return True
if s2[left] in need.keys():
if window[s2[left]] == need[s2[left]]:
valid -= 1
window[s2[left]] -= 1
left += 1 # 左指针向→滑动
return False # 如果遍历完整个s2都没有找到s1的一个排列则返回False
Leetcode438-找到字符串中所有的字母异位词
给定两个字符串
s
和p
,找到s
中所有p
的异位词的子串,返回这些子串的起始索引
注: 异位词是指有相同字母重新排列形成的字符串(包括相同的字符串)
字母异位词的本质就是排列,换了一种说法而已
本题相较于上一题字符串的排列
改动点如下,我们只需在 s
中找到一个 p
的排列后记录下这个位置索引,跟上题不能说差不多简直就是一模一样,在滑动窗口长度
等于p的长度
窗口暂停扩大的时候更新结果
result = []
while right - left == len(t):
if valid == len(need):
result.append(left)
附上该题完整的Python代码
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
need, window = Counter(p), Counter()
left, right = 0, 0
valid = 0
result = []
while right < len(s):
if s[right] in need.keys():
window[s[right]] += 1
if window[s[right]] == need[s[right]]:
valid += 1
right += 1 # 右指针向→滑动
while right - left == len(p):
if valid == len(need):
result.append(left)
if s[left] in need.keys():
if window[s[left]] == need[s[left]]:
valid -= 1
window[s[left]] -= 1
left += 1 # 左指针向→滑动
return result
Leetcode3-无重复字符的最长子串
这道题更简单了,还是按我们的老规矩来分析
1.当移动right扩大窗口即加入字符时,应该更新哪些数据?
初始化Counter(用字典也行)
记录滑动窗口中的元素以及元素出现的次数,
right
向右滑扫描s
的过程中将元素和元素出现的次数记录在Counter
中
2.什么条件下窗口应该暂停扩大?
当滑动窗口中出现次数大于1的元素(即出现重复字符时)窗口应该停止扩大
3.当移动left缩小窗口,即移出字符时应该更新哪些数据?
left
指向的元素时需要移出滑动窗口
的元素,把他在滑动窗口
中出现的次数 -1
4.我们要的结果是在扩大窗口时还是缩小窗口还是窗口暂停扩大的时候进行更新?
在窗口进行扩大的时候进行更新,题目求无重复字符最长子串,故我们在这里对上次记录的结果和新的满足条件的窗口(right - left)
取一个最大值
from collections import Counter
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
window = Counter()
left, right = 0, 0
res = 0
while right < len(s):
window[s[right]] += 1
while window[s[right]] > 1:
window[s[left]] -= 1
left += 1 # 左指针向→滑动
right += 1 # 右指针向→滑动
res = max(res, right - left)
return res
在我们对滑动窗口进行了由浅入深、由难到易的介绍后,博主在最后为大家介绍一些高频面试练手题,算法题没有固定模板,对于可以总结框架的题型我们对它进行分析打磨,节省大家在刷题准备面试过程中的时间成本,提高大家的效率
Leetcode209-长度最小的子数组
Leetcode1004-最大连续1的个数III