Leetcode刷题笔记—双指针在滑动窗口中的应用

双指针问题

素材来自网络

链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。
左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。
滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天

双指针在滑动窗口中的应用

Leetcode76-最小覆盖子串

Leetcode76-最小覆盖子串: Hard题

给你一个字符串 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中所有字符的最小子串的长度,初始化为无穷大,方便后续更新覆盖
1.当移动right扩大窗口即加入字符时,应该更新哪些数据?

右指针向右滑动遍历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  # 右指针向→滑动
2.什么条件下窗口应该暂停扩大?

右指针向右滑的过程中一旦滑动窗口子串中的元素及元素出现的次数都能匹配上即 valid == len(need),说明滑动窗口已经包含t子串,此时右指针应该暂停扩大

注:我们要的结果是在扩大窗口时还是缩小窗口还是窗口暂停扩大的时候进行更新?

本题中我们是在窗口暂停扩大的时候判断此时的窗口长度是否小于之前的记录的窗口长度,如果小于则记录下窗口的起始位置更新s中包含t子串的子串长度

while valid == len(need):
    if right - left < substring_len:
        start = left	# 记录下最小子串的起始覆盖索引
        substring_len = right - left	# 更新s中包含t子串的子串长度

注意上面的黄色字体,上面的代码逻辑发生在右指针向右滑的过程中,所以上面的代码应该嵌套在右指针向右滑动的大循环中

3.当移动left缩小窗口,即移出字符时应该更新哪些数据?

和移动右指针将元素加入到滑动窗口中的操作相同,当我们移动左指针收缩窗口将元素从窗口中移出的时候
我们不仅需要判断它是否是子串中的元素,如果是窗口内的对应元素数量 - 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       # 左指针向→滑动
Leetcode76-最小覆盖子串:Python完整代码
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-字符串的排列

给你两个字符串 s1s2 ,写一个函数来判断 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-找到字符串中所有的字母异位词

给定两个字符串 sp,找到 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

你可能感兴趣的:(Leetcode刷题笔记,leetcode,笔记,算法,python)