【labuladong】滑动窗口

本文为我写了首诗,把滑动窗口算法变成了默写题的笔记。

因此,滑动窗口属于双指针的第3种情况。

滑动窗口框架概览

滑动窗口算法技巧主要用来解决子串、子数组问题,比如让你寻找符合某个条件的最长/最短子数组

滑动窗口算法大致逻辑如下:(C++伪代码)

int left = 0, right = 0;

while (right < nums.size()) {
    // 增大窗口
    window.add(nums[right]);
    right++;
    
    while (window needs shrink) {
        // 缩小窗口
        window.remove(nums[left]);
        left++;
    }
}

基于滑动窗口算法框架写出的代码,时间复杂度是 O(N),比嵌套 for 循环的暴力解法效率高。

下面是滑动窗口算法的代码框架,连在哪里做输出 debug 都给你写好了,以后遇到相关的问题,就默写出来如下框架然后改三个地方就行,保证不会出 bug。

def slidingWindow(s: str):

    # 用合适的数据结构记录窗口中的数据,根据具体场景变通
    # 比如说,我想记录窗口中元素出现的次数,就用 map
    # 我想记录窗口中的元素和,就用 int
    window = dict()

    # left和right均初始化在最左边
    left = 0
    right = 0
    # right达到s字符串的尽头
    while right < len(s):
        # c 是将移入窗口的字符
        c = s[right]
        window[c] = window.get(c, 0) + 1
        # 增大窗口
        right += 1
        # 进行窗口内数据的一系列更新
        #...

        #/*** debug 输出的位置 ***/
        # 注意在最终的解法代码中不要 print
        # 因为 IO 操作很耗时,可能导致超时
        # print(f"window: [{left}, {right})")
        #/********************/

        # 判断左侧窗口是否要收缩
        while left < right and "window needs shrink":
            # d 是将移出窗口的字符
            d = s[left]
            window[d] -= 1
            if window[d] == 0:
                del window[d]
            # 缩小窗口
            left += 1
            # 进行窗口内数据的一系列更新
            #...

这两个 ... 处的操作分别是扩大和缩小窗口的更新操作,它们操作是完全对称的。

思路版本:

def slidingWindow(s: str):

    # 用合适的数据结构记录窗口中的数据,根据具体场景变通map, dict, int


    # 将left和right均初始化在最左边    
    # 判断right是否达到s字符串的尽头:
        # c 是将移入窗口的字符
        # 增大窗口
        # 进行窗口内数据的一系列更新

        #/*** debug 输出的位置 ***/
        #/********************/

        # 判断左侧窗口是否要收缩:
            # d 是将移出窗口的字符
            # 缩小窗口
            # 进行窗口内数据的一系列更新

76. 最小覆盖子串

滑动窗口算法的思路

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

💡为什么要「左闭右开」区间

理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的

因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。

如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。

     2. 先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

     3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

     4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。

左右指针轮流前进,窗口大小增增减减,就好像一条毛毛虫,一伸一缩,不断向右滑动,这就是「滑动窗口」这个名字的来历。——因此,滑动窗口总是初始化为 left = right = 0 的。

Python如何判断字符串条件

对于这点,labuladong的代码没有Python特性。参考【代码随想录】-哈希表242.有效的字母异位词

Counter不知道是否便于window手动操作,先用defaultdict试一下。——不行,return s_counter > t_counter 是无效的。

由于滑动窗口已经很复杂了,直接用3种方法中最简单的Counter。测试了一下,return s_counter > t_counter 是有效的。而且用Counter也可以对window的单个元素手动操作。

用242尝试了。

from collections import Counter

s_counter = Counter(s)
t_counter = Counter(t)

lst = []

for i in s_counter:
    if i == "g":
        s_counter[i] -= 1

return s_counter

结果:

{"a":4,"n":1,"g":0,"r":1,"m":1,"x":1}

套用滑动窗口模板

现在开始套模板,只需要思考以下几个问题

  1. 什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?
  2. 什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?
  3. 我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

——如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 字符串条件 满足 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

我的初始代码

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        from collections import Counter
        window = Counter()
        t_dict = Counter(t)
        min_str = s

        left = right = 0
        while right < len(s):
            c = s[right]  # 不是right+1,而是right. 因为此时window里面没有元素。         
            window[c] += 1
            right += 1

            # debug
            print(f"window: [{left}, {right})", window)

            while (left < right and window > t_dict):
                # 收缩窗口前更新min_str
                if right - left < len(min_str):
                    min_str = s[left:right]
                    print(f"min_str: {min_str}")
                d = s[left]
                window[d] -= 1
                if window[d] == 0:
                    del window[d]
                left += 1
                # 收缩窗口后更新min_str


            # print(f"min_str: {min_str}")

        return min_str

可以试一下,返回结果是不对的。

? 不想用一个计数器valid判断,只用Counter判断可不可以呢?


应该是不可以的。

while (left < right and window > t_dict): 这不是判断left是否收缩的条件,所以整个代码都不对。

照着labuladong修改了一下:

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        from collections import Counter
        window = Counter()
        t_dict = Counter(t)
        min_str = s
        valid = 0

        left = right = 0
        while right < len(s):
            c = s[right]  # 不是right+1,而是right. 因为此时window里面没有元素。
            if c in t_dict:         
                window[c] += 1
                if window[c] == t_dict[c]:
                    valid += 1
            right += 1

            # debug
            print(f"window: [{left}, {right})", window)

            # 不需要left < right了吗?——试一下
            while (left < right and valid == len(t_dict)):
                # 收缩窗口前更新min_str
                if right - left < len(min_str):
                    min_str = s[left:right]
                    print(f"min_str: {min_str}")
                d = s[left]
                if d in t_dict:
                    if window[d] == t_dict[d]:
                        valid -= 1
                    window[d] -= 1
                left += 1


            # print(f"min_str: {min_str}")

        return min_str

返回结果:

t 中是重复字符的时候就不对了。

提交一下labuladong的代码是可以通过的。

差在哪里?

——结尾不一样:

return 改成 return "" if len(min_str) == len(s) else min_str

这样的话case 2又不一致了。

还是要改成初始化length = float('inf'). min_strs[start:start+length]表示。

为什么不用start就不行?

        判断条件不一样,left是在第二个while的判断条件下面,是会变的,用一个start就是记录下来不变的最小length

完整代码:

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        from collections import Counter
        window = Counter()
        t_dict = Counter(t)
        start, length = 0, float('inf')
        valid = 0

        left = right = 0
        while right < len(s):
            c = s[right]  # 不是right+1,而是right. 因为此时window里面没有元素。
            if c in t_dict:         
                window[c] += 1
                if window[c] == t_dict[c]:
                    valid += 1
            right += 1

            # 不需要left < right了吗?——试一下
            while (left < right and valid == len(t_dict)):
                # 收缩窗口前更新min_str
                if right - left < length:
                    start = left
                    length = right - left
                d = s[left]
                if d in t_dict:
                    if window[d] == t_dict[d]:
                        valid -= 1
                    window[d] -= 1
                left += 1

        return "" if length == float('inf') else s[start:start+length]

顺便说一下,刷完滑动窗口就少刷labuladong的题,讲的没有代码随想录好,而且选的hard题目,很浪费时间,不适合我目前的阶段。

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

滑动窗口框架+收缩窗口的判断条件用Python哈希表Counter——AC! 和 76解法一样。但76不愧是hard.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值