Python数据结构与算法篇(四)-- 滑动窗口算法

        数组和链表代表着计算机最基本的两种存储形式:顺序存储和链式存储,所以他俩可以算是最基本的数据结构。数组是一种基础数据结构,可以用来处理常见的排序和二分搜索问题,典型的处理技巧包括双指针、滑动窗口等,数组是数据结构中的基本模块之一。因为字符串是由字符数组形成的,所以二者是相似的。

1 滑动窗口

1.1 定义

        在计算机网络里经常用到滑动窗口协议(Sliding Window Protocol),该协议是 TCP协议 的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认。因此该协议可以加速数据的传输,提高网络吞吐量。

        滑动窗口算法其实和这个是一样的,只是用的地方场景不一样,可以根据需要调整窗口的大小,有时也可以是固定窗口大小。

        滑动窗口使用双指针解决问题,所以一般也叫双指针算法,因为两个指针间形成一个窗口。双指针也并不局限在数组问题,像链表场景的 “快慢指针” 也属于双指针的场景,其快慢指针滑动过程中本身就会产生一个窗口,比如当窗口收缩到某种程度,可以得到一些结论。

        什么情况适合用滑动窗口算法呢?

  • 需要输出或比较的结果在原数据结构中是连续排列的,特别是数组或链表问题;
  • 每次窗口滑动时,只需观察窗口两端元素的变化,无论窗口多长,每次只操作两个头尾元素,当用到的窗口比较长时,可以显著减少操作次数;
  • 窗口内元素的整体性比较强,窗口滑动可以只通过操作头尾两个位置的变化实现,但对比结果时往往要用到窗口中所有元素。

        滑动窗口算法常用于字符串匹配问题和子数组问题。

        滑动窗口算法在一个特定大小的字符串或数组上进行操作,而不在整个字符串和数组上操作,这样就降低了问题的复杂度,从而也达到降低了循环的嵌套深度。其实这里就可以看出来滑动窗口主要应用在数组和字符串上。

1.2 滑动窗口法的大体框架

        在介绍滑动窗口的框架时候,大家先从字面理解下:

  • 滑动:说明这个窗口是移动的,也就是移动是按照一定方向来的。
  • 窗口:窗口大小并不是固定的,可以不断扩容直到满足一定的条件;也可以不断缩小,直到找到一个满足条件的最小窗口;当然也可以是固定大小。

        为了便于理解,这里采用的是字符串来讲解。但是对于数组其实也是一样的。滑动窗口算法的思路是这样:

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
  2. 我们先不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

        这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。

1.3 滑动窗口模板

滑窗模板 Python 伪代码

class Solution:
    def problemName(self, s: str) -> int:
        # Step 1: 定义需要维护的变量们 (对于滑动窗口类题目,这些变量通常是最小长度,最大长度,或者哈希表)
        x, y = ..., ...

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end in range(len(s)):
            # Step 3: 更新需要维护的变量, 有的变量需要一个if语句来维护 (比如最大最小长度)
            x = new_x
            if condition:
                y = new_y

            '''
            ------------- 下面是两种情况,读者请根据题意二选1 -------------
            '''
            # Step 4 - 情况1
            # 如果题目的窗口长度固定:用一个if语句判断一下当前窗口长度是否达到了限定长度 
            # 如果达到了,窗口左指针前移一个单位,从而保证下一次右指针右移时,窗口长度保持不变, 
            # 左指针移动之前, 先更新Step 1定义的(部分或所有)维护变量 
            if 窗口长度达到了限定长度:
                # 更新 (部分或所有) 维护变量 
                # 窗口左指针前移一个单位保证下一次右指针右移时窗口长度保持不变

            # Step 4 - 情况2
            # 如果题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 如果当前窗口不合法时, 用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 在左指针移动之前更新Step 1定义的(部分或所有)维护变量 
            while 不合法:
                # 更新 (部分或所有) 维护变量 
                # 不断移动窗口左指针直到窗口再次合法

        # Step 5: 返回答案
        return ...

        滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针(end)作为驱动,拖着左指针(start)向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。


2 常见题型

2.1 题库列表

  • 643. 子数组最大平均数 I

  • 3. 无重复字符的最长子串

  • 159. 至多包含两个不同字符的最长子串

  • 209. 长度最小的子数组

  • 1695. 删除子数组的最大得分

  • 438. 找到字符串中所有字母异位词

  • 567. 字符串的排列

  • 487. 最大连续1的个数 II

  • 1004. 最大连续1的个数 III

  • 1208. 尽可能使字符串相等

  • 1052. 爱生气的书店老板

  • 1151. 最少交换次数来组合所有的1 Π

  • 76. 最小覆盖子串

  • 239. 滑动窗口最大值

2.2 真题演练

643. 子数组最大平均数 I
        题目描述:给你一个由 n 个元素组成的整数数组 nums 和一个整数 k。请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。

class Solution:
    def findMaxAverage(self, nums: List[int], k: int) -> float:
        # Step 1
        # 定义需要维护的变量
        # 本题求最大平均值 (其实就是求最大和),所以需要定义sum_sub_array, 同时定义一个max_value (初始值为负无穷)
        sum_sub_array, max_value = 0, float('-inf')

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, num in enumerate(nums):
            # Step 3: 更新需要维护的变量 (sum_sub_array, max_value), 不断把当前值积累到sum_sub_array上
            sum_sub_array += num
            if end - start + 1 == k:
                max_value = max(max_value, sum_sub_array)

            # Step 4
            # 根据题意可知窗口长度固定,所以用if
            # 窗口首指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (sum_sub_array)
            if end >= k - 1:
                sum_sub_array -= nums[start]
                start += 1
        # Step 5: 返回答案
        return max_value/k
Python数据结构与算法篇(四)-- 滑动窗口算法_第1张图片

3. 无重复字符的最长子串

        题目描述:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # Step 1: 定义需要维护的变量, 本题求最大长度,所以需要定义max_len, 该题又涉及去重,因此还需要一个哈希表
        max_len, hash_map = 0, {}

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(s):
            # Step 3
            # 更新需要维护的变量 (max_len, hashmap)
            # i.e. 把窗口末端元素加入哈希表,使其频率加1,并且更新最大长度
            hash_map[tail] = hash_map.get(tail, 0) + 1
            if len(hash_map) == end - start + 1:
                max_len = max(max_len, end - start + 1)
            
            # Step 4: 
            # 根据题意,  题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 当窗口长度大于哈希表长度时候 (说明存在重复元素),窗口不合法
            # 所以需要不断移动窗口左指针直到窗口再次合法, 同时提前更新需要维护的变量 (hashmap)
            while end - start + 1 > len(hash_map):
                head = s[start]
                hash_map[head] -= 1
                if hash_map[head] == 0:
                    del hash_map[head]
                start += 1
        # Step 5: 返回答案 (最大长度)
        return max_len
Python数据结构与算法篇(四)-- 滑动窗口算法_第2张图片

159. 至多包含两个不同字符的最长子串

        题目描述:给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t 。

class Solution:
    def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int:
        # Step 1: 
        # 定义需要维护的变量, 本题求最大长度,所以需要定义max_len,
        # 该题又涉及计算不重复元素个数,因此还需要一个哈希表
        max_len, hashmap = 0, {}

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(s):
            # Step 3
            # 更新需要维护的变量 (max_len, hashmap)
            # 首先,把当前元素的计数加一
            # 一旦哈希表长度小于等于2(之多包含2个不同元素),尝试更新最大长度
            hashmap[tail] = hashmap.get(tail, 0) + 1
            if len(hashmap) <= 2:
                max_len = max(max_len, end - start + 1)

            # Step 4: 
            # 根据题意,  题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 哈希表长度大于2的时候 (说明存在至少3个重复元素),窗口不合法
            # 所以需要不断移动窗口左指针直到窗口再次合法, 同时提前更新需要维护的变量 (hashmap)
            while len(hashmap) > 2:
                head = s[start]
                hashmap[head] -= 1
                if hashmap[head] == 0:
                    del hashmap[head]
                start += 1
        # Step 5: 返回答案 (最大长度)
        return max_len

209. 长度最小的子数组

        题目描述:给定一个含有 n 个正整数的数组和一个正整数 target。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [ n u m s l , n u m s l + 1 , . . . , n u m s r − 1 , n u m s r ] [nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r] [numsl,numsl+1,...,numsr1,numsr],并返回其长度。如果不存在符合条件的子数组,返回 0。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        # Step 1: 定义需要维护的变量, 本题求最小长度,所以需要定义min_len, 本题又涉及求和,因此还需要一个sum变量
        min_len, sum_sub_array = math.inf, 0

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, num in enumerate(nums):
            # Step 3: 更新需要维护的变量 (min_len, sum_sub_array)
            sum_sub_array += num

            # 这一段可以删除,因为下面的while已经handle了这一块儿逻辑,不过写在这也没影响
            if sum_sub_array >= target:
                min_len = min(min_len, end - start + 1)

            # Step 4
            # 这一题这里稍微有一点特别: sum_sub_array >= target其实是合法的,但由于我们要求的是最小长度,
            # 所以当sum_sub_array已经大于target的时候继续移动右指针没有意义,因此还是需要移动左指针慢慢逼近答案
            # 由于左指针的移动可能影响min_len和sum_sub_array的值,因此需要在移动前将它们更新
            while sum_sub_array >= target:
                min_len = min(min_len, end - start + 1)
                sum_sub_array -= nums[start]
                start += 1
        # Step 5:返回答案 (最小长度)
        if min_len == math.inf:
            return 0
        return min_len
Python数据结构与算法篇(四)-- 滑动窗口算法_第3张图片

1695. 删除子数组的最大得分

        题目描述:给你一个正整数数组 nums ,请你从中删除一个含有 若干不同元素 的子数组。删除子数组的 得分 就是子数组各元素之 和。返回 只删除一个 子数组可获得的 最大得分。如果数组 b 是数组 a 的一个连续子序列,即如果它等于 a[l],a[l+1],…,a[r] ,那么它就是 a 的一个子数组。

class Solution:
    def maximumUniqueSubarray(self, nums: List[int]) -> int:
        # Step 1
        # 定义需要维护的变量, 本题最大得分,所以需要定义当前得分sum_sub_array和最大得分max_sum
        # 本题又涉及去重 (题目规定子数组不能有重复),因此还需要一个哈希表
        sum_sub_array, max_sum, hashmap = 0, 0, {}

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(nums):
            # Step 3
            # 更新需要维护的变量 (sum_sub_array, hashmap)
            # sum和hashmap需要更新就不说了,max_sum当且仅当哈希表里面没有重复元素时 (end - start + 1 == len(hashmap)) 更新
            sum_sub_array += tail
            hashmap[tail] = hashmap.get(tail, 0) + 1
            if end - start + 1 == len(hashmap):
                max_sum = max(max_sum, sum_sub_array)
            
            # Step 4
            # 根据题意,  题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 哈希表里面有重复元素时 (end - start + 1 > len(hashmap)) 窗口不合法
            # 所以需要不断移动窗口左指针直到窗口再次合法, 同时提前更新需要维护的变量 (hashmap, sum_sub_array)
            while end - start + 1 > len(hashmap):
                head = nums[start]
                hashmap[head] -= 1
                if hashmap[head] == 0:
                    del hashmap[head]
                sum_sub_array -= nums[start]
                start += 1
        # Step 5: 返回答案
        return max_sum

438. 找到字符串中所有字母异位词

        题目描述:给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        # Step 1: 
        # 定义需要维护的变量
        # 本文需要对比两组字符串是否为异位词,所以用哈希表 (abc和bac是异位词是因为他们对应的哈希表相等)
        # 同时我们需要找到所有合法解,所以还需要一个ans数组
        ans, hashmap_s = [], {}

        # Step 1.1: 同时把p的哈希表也建立了 (这个哈希表不需要维护,为定值)
        hashmap_p = Counter(p)

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(s):
            # Step 3: 更新需要维护的变量 (hashmap), 如果hashmap == hashmap_p,代表找到了一个解,加入到ans
            hashmap_s[tail] = hashmap_s.get(tail, 0) + 1
            if hashmap_s == hashmap_p:
                ans.append(start)

            # Step 4 
            # 根据题意可知窗口长度固定,所以用if
            # 窗口左指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (hashmap)
            if end >= len(p) - 1:
                head = s[start]
                hashmap_s[head] -= 1
                if hashmap_s[head] == 0:
                    del hashmap_s[head]
                start += 1
        # Step 5: 返回答案
        return ans
Python数据结构与算法篇(四)-- 滑动窗口算法_第4张图片

567. 字符串的排列

        题目描述:给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        # Step 1
        # 定义需要维护的变量
        # 因为和排列相关 (元素相同,顺序可以不同),使用哈希表
        hash_map_s2 = {}

        # Step 1.1: 同时建立s1的哈希表 (这个哈希表不需要维护,为定值)
        hash_map_s1 = Counter(s1)
        
        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(s2):
            # Step 3: 更新需要维护的变量 (hash_map_s2), 如果hash_map_s1 == hash_map_s2,代表s2包含s1的排列,直接return
            hash_map_s2[tail] = hash_map_s2.get(tail, 0) + 1
            if hash_map_s1 == hash_map_s2:
                    return True

            # Step 4: 
            # 根据题意可知窗口长度固定,所以用if
            # 窗口左指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (hash_map_s2)
            if end >= len(s1) - 1:
                head = s2[start]
                hash_map_s2[head] -= 1
                if hash_map_s2[head] == 0:
                    del hash_map_s2[head]
                start += 1
        # Step 5: 没有在s2中找到s1的排列,返回False
        return False
Python数据结构与算法篇(四)-- 滑动窗口算法_第5张图片

487. 最大连续1的个数 II

        题目描述:给定一个二进制数组,你可以最多将 1 个 0 翻转为 1,找出其中最大连续 1 的个数。

class Solution:
    def findMaxConsecutiveOnes(self, nums: List[int]) -> int:
        # Step 1
        # 定义需要维护的变量
        # 因为是求最大长度,所以有max_len,又同时涉及计数 (0的个数不能超过1个),所以还要一个哈希表
        max_len, hash_map = 0, {}

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(nums):
            # Step 3: 更新需要维护的变量 (hash_map, max_len)
            hash_map[tail] = hash_map.get(tail, 0) + 1
            if hash_map.get(0, 0) <= 1:
                max_len = max(max_len, end - start + 1)

            # Step 4
            # 根据题意,  题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 当hash_map里面0的个数大于1的时候,窗口不合法
            # 所以需要不断移动窗口左指针直到窗口再次合法, 同时提前更新需要维护的变量 (hash_map)
            while hash_map.get(0, 0) > 1:
                head = nums[start]
                hash_map[head] -= 1
                start += 1
        # Step 5: 返回答案 (最大长度)
        return max_len

1004. 最大连续1的个数 III
        题目描述:给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。

class Solution:
    def longestOnes(self, nums: List[int], k: int) -> int:
        max_len, start = 0, 0
        hash_map = {}
        for end, tail in enumerate(nums):
            hash_map[tail] = hash_map.get(tail, 0) + 1
            if hash_map.get(0, 0) <= k:     # 相比较于上一题,只需要把1改成k
                max_len = max(max_len, end-start+1)
            while hash_map.get(0, 0) > k:
                head = nums[start]
                hash_map[head] -= 1
                start += 1
        return max_len
Python数据结构与算法篇(四)-- 滑动窗口算法_第6张图片

1208. 尽可能使字符串相等
        题目描述:给你两个长度相同的字符串,s 和 t。将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。

class Solution:
    def equalSubstring(self, s: str, t: str, max_cost: int) -> int:
        # Step 1: 定义需要维护的变量
        # 因为是求最大长度,所以有max_len,又同时涉及计算开销 (和求和一个道理), 所以还要一个cur_cost
        cur_cost, max_len = 0, 0
        
        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end in range(len(t)):
            # Step 3
            # 更新需要维护的变量 (cur_cost)
            # 每一对字符的order差值就是当前时间点的开销,直接累积在cur_cost上即可
            # cur_cost只要不超过最大开销,就更新max_len
            cur_cost += abs(ord(s[end]) - ord(t[end]))
            if cur_cost <= max_cost:
                max_len = max(max_len, end - start + 1)

            # Step 4
            # 根据题意,  题目的窗口长度可变: 这个时候一般涉及到窗口是否合法的问题
            # 这时要用一个while去不断移动窗口左指针, 从而剔除非法元素直到窗口再次合法
            # 当cur_cost大于最大开销时候,窗口不合法
            # 所以需要不断移动窗口左指针直到窗口再次合法 (cur_cost <= max_cost)
            while cur_cost > max_cost:
                cur_cost -= abs(ord(s[start])-  ord(t[start]))
                start += 1
        # Step 5: 返回答案 (最大长度)
        return max_len
Python数据结构与算法篇(四)-- 滑动窗口算法_第7张图片

1052. 爱生气的书店老板

题目描述
Python数据结构与算法篇(四)-- 滑动窗口算法_第8张图片

class Solution:
    def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int:
        # Step 1
        # 定义需要维护的变量,
        # 因为涉及求和所以定义sum_sub_array和max_sum, 同时需要知道老板什么时候'发动技能',再定义一个max_start
        sum_sub_array, max_sum, max_start = 0, 0, 0
  
        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(customers):
            # Step 3
            # 更新需要维护的变量 (sum_sub_array)
            # 注意:这里只要当老板在当前时间点会发脾气的时候才维护
            # sum_sub_array就不说了,和前面N道题的维护方法一样,新多出来的max_start也就是记录一样时间点而已,没什么fancy的
            if grumpy[end] == 1:
                sum_sub_array += tail
            if sum_sub_array > max_sum:
                max_sum = sum_sub_array
                max_start = start

            # Step 4
            # 根据题意可知窗口长度固定 (老板技能持续时间固定),所以用if
            # 窗口左指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (sum_sub_array, max_avg)
            if end >= minutes - 1:
                if grumpy[start]:
                    sum_sub_array -= customers[start]
                start += 1

        # 这里对比其他题目多了一小步: 在找到老板发动技能的最大收益时间点(max_start)后
        # 需要把受技能影响时间段中的grumpy全部置0 - 代表老板成功压制了自己的怒火
        grumpy[max_start:max_start+minutes] = [0] * minutes

        # Step 5: 再遍历一遍数组求customer总数量并且返回结果   
        res = 0
        for customer, grum in zip(customers, grumpy):
            if grum == 0:
                res += customer
        return res
Python数据结构与算法篇(四)-- 滑动窗口算法_第9张图片

1423. 可获得的最大点数

题目描述
Python数据结构与算法篇(四)-- 滑动窗口算法_第10张图片

class Solution:
    # 这题相比前面的题目加了一丢丢小的变通: 题目要求首尾串最大点数,其实就是求非首尾串的连续序列的最小点数
    def maxScore(self, cardPoints: List[int], k: int) -> int:
        # Step 1
        # 定义需要维护的变量,因为涉及求和所以定义sum_sub_array和min_sum
        sum_sub_array, min_sum = 0, float('inf')

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end, tail in enumerate(cardPoints):
            # Step 3
            # 更新需要维护的变量 (sum_sub_array)
            sum_sub_array += tail
            if len(cardPoints)-k == end - start + 1:
                min_sum = min(min_sum, sum_sub_array)
            # Step 4
            # 根据题意可知窗口长度固定,所以用if
            # 窗口左指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (min_sum, sum_sub_array)
            if end >= len(cardPoints) - k - 1:
                sum_sub_array -= cardPoints[start]
                start += 1
        # Step 5: 返回答案 (总点数减去非首尾串的连续序列的最小点数就可以得到首尾串的最大点数)
        return sum(cardPoints) - min_sum if min_sum != inf else sum(cardPoints)
Python数据结构与算法篇(四)-- 滑动窗口算法_第11张图片

1151. 最少交换次数来组合所有的1 Π

        题目描述:给出一个二进制数组data,你需要通过交换位置,将数组中任何位置上的1组合到一起,并返回所有可能中所需最少的交换次数。

Python数据结构与算法篇(四)-- 滑动窗口算法_第12张图片
class Solution:
    def minSwaps(self, data: List[int]) -> int:
        # 先数出一共有多少个1,输出来的个数就是窗口的长度
        num_ones = data.count(1)

        # Step 1
        # 定义需要维护的变量,求最小swap次数其实就是求窗口中0个数的最小值,因此定义num_zeros, min_num_zeros
        num_zeros, min_num_zeros = 0, math.inf

        # Step 2: 定义窗口的首尾端 (start, end), 然后滑动窗口
        start = 0
        for end in range(len(data)):
            # Step 3
            # 更新需要维护的变量 (num_zeros, min_num_zeros)
            if data[end] == 0:
                num_zeros += 1
            if end - start + 1 == num_ones:
                min_num_zeros = min(min_num_zeros, num_zeros)

            # Step 4
            # 根据题意可知窗口长度固定 (数组1的总个数),所以用if
            # 窗口左指针前移一个单位保证窗口长度固定, 同时提前更新需要维护的变量 (num_zeros)
            if end >= num_ones - 1:
                if data[start] == 0:
                    num_zeros -= 1
                start += 1
        
        # Step 5: 返回答案 (如果min_num_zeros依旧是math.inf说明数组没有1存在,不能swap,返回0即可)
        return min_num_zeros if min_num_zeros != math.inf else 0

76. 最小覆盖子串

        题目描述:给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

from collections import Counter
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need_dict = Counter(t)                      # 哈希表:记录需要匹配到的各个元素的数目
        need_count = len(t)                         # 记录需要匹配到的字符总数(need=0表示匹配到了)
        left = 0                                    # 窗口的左边界
        res = (0, inf)                              # 记录目标子串 s[res[0], res[1]+1] 的起始和结尾
        for right, tail in enumerate(s):             # 窗口右边界右移一位,窗口中增加的字符 tail
            if tail in need_dict:                    # 窗口新加入的字符位于t中
                if need_dict[tail] > 0:              # 对当前字符还有需求
                    need_count -= 1                 # 此时新加入窗口中的 tail 对 need_count 有影响
                need_dict[tail] -= 1
            while need_count == 0:                  # 窗口左边界持续右移,need_count=0,当前窗口完全覆盖了 t
                if res[1] - res[0] > right - left:  # 出现了更短的字符串
                    res = (left, right)
                head = s[left]                       # 窗口中要滑出的字符 head
                if head in need_dict:                # 刚滑出的字符 head 位于 t 中
                    if need_dict[head] >= 0:         # 对当前字符ch还有需求,或刚好无需求(其实此时只有=0的情况)
                        need_count += 1             # 此时滑出窗口的 head 会对 need_count 有影响
                    need_dict[head] += 1
                left += 1                           # 窗口左边界+1
        return s[res[0]:res[1]+1] if res[1] < len(s) else ""

239. 滑动窗口最大值

题目描述
Python数据结构与算法篇(四)-- 滑动窗口算法_第13张图片

from collections import deque
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        # 用双端队列来存储数组的下标,为什么要存下标而不是存数值?
        # 因为存下标可以更方便的来确定元素是否需要移出滑动窗口
        # 判断下标是否合法来确定是否要移出
        if k == 1:
            return nums
        if k == len(nums):
            return [max(nums)]
        result, queue = [], deque()             # 使用collections内置的双端队列,加快运行速度
        for i in range(len(nums)):
            # 如果当前队列最左侧存储的下标等于 i-k 的值,代表目前队列已满。
            # 但是新元素需要进来,所以列表最左侧的下标出队列
            if queue and queue[0] == i - k:           
                queue.popleft()
            while queue and nums[queue[-1]] < nums[i]:      # 对于新进入的元素,如果队列前面的数比它小,那么前面的都出队列
                queue.pop()
            queue.append(i)         # 新元素入队列
            if i >= k-1:            # 当前的大值加入到结果数组中
                result.append(nums[queue[0]])
        
        return result

滑动窗口暂时告一段落,后面在学习中持续补充,谢谢大家的鼓励和支持!


参考

  • 双指针套路总结:https://zhuanlan.zhihu.com/p/95747836
  • 数组+常见题型与解题策略:https://blog.csdn.net/qq_42647903/article/details/120594856
  • 算法与数据结构(一):滑动窗口法总结:https://blog.csdn.net/Dby_freedom/article/details/89066140
  • 滑动窗口法python模板写法:https://blog.csdn.net/weixin_44414948/article/details/113862173
  • 滑动窗口的应用:https://leetcode.cn/problems/minimum-window-substring/solutions/1503454/by-flix-1kac/
  • 秒杀12道中档题————滑动窗口:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/876061/yi-ge-mo-ban-miao-sha-10dao-zhong-deng-n-sb0x/

你可能感兴趣的:(数据结构与算法,LC,PAT,数组,滑动窗口,最小覆盖子串,最长子串,最小/最大子数组)