定长滑动窗口

目录

滑动窗口

是什么?

核心思想

实际案例分享

长度最小子数组(非定长)

无重复字符的最长长度(非定长)

定长子串元音的最大数目 (定长)

​编辑

子数组最大平均数I

大小为k且平均值大于等于阈值的子数组

 2090. 半径为 k 的子数组平均值

2379. 得到 K 个黑块的最少涂色次数

 2841. 几乎唯一子数组的最大和

2461. 长度为 K 子数组中的最大和

 1423. 可获得的最大点数

 1052. 爱生气的书店老板

 1652. 拆炸弹


推荐一篇很牛的文章:讨论 - 力扣(LeetCode)

滑动窗口

是什么?

滑动窗口(Sliding Window)是一种高效的算法技巧,常用于数组或字符串的问题中,用于寻找满足特定条件的子数组或子串

核心思想

可以简单理解为快慢指针的变种。

  1. 使用两个指针(通常是 left 和 right )定义一个“窗口”。
  2. right 指针向右扩展窗口,添加新元素。
  3.  当窗口不满足条件时, left 指针向右移动,缩小窗口。
  4. 通过这种方式,避免重复计算,实现O(n)时间复杂度。每次添加或删除元素的时间复杂度都是O(1)

实际案例分享

长度最小子数组(非定长)

题目来源:209. 长度最小的子数组 - 力扣(LeetCode)

 首个题解:

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        res = float('inf')
        left = 0
        for right in range(len(nums)):
             while sum(nums[left : right + 1]) >= target:
                res = min(right + 1 - left,res)
                left += 1
        res = 0 if res == float('inf') else res
        return res 

Solution (O(n²) 时间复杂度) :在 while 循环内每次调用 sum(nums[left : right + 1]) ,这会重新计算整个子数组的和,导致嵌套循环,时间开销巨大。当 n=10^5 时,可能超时。

所以退化成了暴力破解

知识点:内置函数在数组数据很大时会很耗时

正解:

Solution (O(n) 时间复杂度) :使用累积和 s 维护窗口和,每次滑动只加减一个元素,高效。

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        res = float('inf')
        s = left = 0
        for right in range(len(nums)):
            s += nums[right]
            while s >= target:
                res = min(right + 1 - left,res)
                s -= nums[left]
                left += 1
        res = 0 if res == float('inf') else res
        return res 

无重复字符的最长长度(非定长)

题目来源:3. 无重复字符的最长子串 - 力扣(LeetCode)

解:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        res = 0
        left = 0
        no_double = []
        if len(s) == 1:
            return 1
        for right,s_ in enumerate(s):
            while s_ in no_double:
                no_double.remove(s[left])
                left += 1
            res = max(res,right + 1 - left)
            no_double.append(s_)
        
        return res

思路:

  1. 利用左右端点动态滑动窗口
  2. 如果该窗口内不存在重复字符,那么右端点右移(将该字符加入容器)
  3. 如果该窗口内存在重复字符,那么左端点右移(从容器中删除左端点对应的字符,左端点➕1)
  4. 注意:字符需要存储在一个容器中,这里我使用的是列表 in 操作判断是否已经存在,如果是那么就是在右端点右移后出现了重复字符

心得:

不是所有程序都可以一边写对,所以在有思路之后应该尝试coding,并不断debug修改直到完成!

优解:

利用计数器哈希表,记录每个字符出现的次数,如果次数大于等于2就是重复了,需要去重并右移端点

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        res = 0
        left = 0
        cnt = Counter()
        for right,s_ in enumerate(s):
            cnt[s_] += 1
            while cnt[s_] >= 2:
                cnt[s[left]] -= 1
                left += 1
            res = max(res,right + 1 - left)
        return res

定长子串元音的最大数目 (定长)

题目来源:1456. 定长子串中元音的最大数目 - 力扣(LeetCode)

我的分析:

  1. 滑动窗口定长为k,使用两个指针标记窗口的左右端点,那么两个端点之间的长度应该为k,即right - left + 1 = k,由此可以推导left = right + 1 - k
  2. 滑动窗口我习惯让右端点遍历滑动,所以第一点公式基于此
  3. 由于需要记录窗口内的元音个数,当时脑子捋顺不过来了,就使用tmp存储窗口内元音数量,然后每更新一次就添加到ans列表,最后返回max(ans)
  4. 滑动窗口关键点就是如何移动,如何移除,所以不可能不调试就能一次写完的,除非像灵茶山这样的大佬,要利用好本地的debug工具梳理逻辑,然后修改不断完善并最终实现移动移除操作

我的解:

class Solution:
    def maxVowels(self, s: str, k: int) -> int:
            yuan = 'aeiou'
            left = 0
            ans = []
            tmp = 0 
            for right in range(len(s)):
                if s[right] in yuan:
                    tmp += 1
                left = right +1 - k
                if left < 0:
                    continue
                ans.append(tmp)   #这里可以直接 ans = max(ans,tmp)当时脑子抽抽了
                if s[left] in yuan:
                    tmp -= 1
            return max(ans)

大佬的解答:

class Solution:
    def maxVowels(self, s: str, k: int) -> int:
        ans = vowel = 0
        for i, c in enumerate(s):
            if c in "aeiou":
                vowel += 1
            if i < k - 1:  
                continue
            ans = max(ans, vowel)
            if s[i - k + 1] in "aeiou":
                vowel -= 1
        return ans
作者:灵茶山艾府

总结:

其实我的代码结构是没问题的,但是在写代码过程中脑子有点小乱,所以没有及时发现max(ans,tmp)方法

子数组最大平均数I

题目来源:643. 子数组最大平均数 I - 力扣(LeetCode)

我的分析:

  1. 定长滑动窗口,加数----->更新ans----->移除末尾数
  2. 需要注意的是,整数数组中存在负数,所以ans定义不能是0然后使用max获取,而是要定义为无穷小,即-inf或-float('inf') 

我的解:

class Solution:
    def findMaxAverage(self, nums: List[int], k: int) -> float:
        ans = -float('inf')
        left = 0
        tmp = 0
        for right in range(len(nums)):
            tmp += nums[right]
            left = right + 1 - k
            if left < 0:
                continue
            ans = max(ans,tmp/k)
            tmp -= nums[left]
        return ans

大小为k且平均值大于等于阈值的子数组

 滑动窗口介绍见:算法学习--滑动窗口-CSDN博客

解答:

class Solution:
    def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
        ans = left = 0
        tmp = 0
        for right in range(len(arr)):
            tmp += arr[right]
            left = right + 1 - k
            if left < 0:
                continue
            if tmp / k >= threshold:
                ans += 1
            tmp -= arr[left]
        return ans
        

 2090. 半径为 k 的子数组平均值

题目来源:2090. 半径为 k 的子数组平均值 - 力扣(LeetCode)

题目:

分析:

  1. 由于半径为k,那么window就是 2 *k + 1,所以这是一个定长滑动窗口
  2. 那么定长滑动窗口端点指针的关系就是 :
    1. right - left +1 = 2*k +1
    2. left = right - 2*k
  3. 由于i索引前后半径不够k avg直接赋值为-1:
    1. 如果len(nums) < len(window) return [i for i in range(len(nums))]
    2. 如果条件一满足,那么left < 0 or right > len(nums) -1 时,avg = -1
  4. 因为right作为window右端点,那么如果加和随着索引i相加就会遗漏sum(nums[k:right + 1]),所以我首先想到了随right索引加和,将加和结果变量初始化为sum(nums[:k]),这样就可以实现滑动窗口的移动和移除

我的解:

class Solution:
    def getAverages(self, nums: List[int], k: int) -> List[int]:
        # 窗口长度为 2k + 1
        windows = 2*k + 1
        ans = list()
        length = len(nums)
        # 前或后不足 k 个元素 半径为 k 的子数组平均值 是 -1 
        if length < windows:
            return [-1 for i in range(length)]
        left = 0
        # 初始化 
        add = sum(nums[:k])
        for i in range(length):
            right = i + k
            left = right - 2*k
            if right >= length:
                ans.append(-1)
                continue
            else:
                add += nums[right]
            if left < 0 :
                ans.append(-1)
                continue
            # 更新 ans 数组
            ans.append(add//len(nums[left:right+1]))
            # 移除 windows末端数值
            add -= nums[left]
        return ans

题解:

class Solution:
    def getAverages(self, nums: List[int], k: int) -> List[int]:
        avgs = [-1] * len(nums)
        s = 0  # 维护窗口元素和
        for i, x in enumerate(nums):
            # 1. 进入窗口
            s += x
            if i < k * 2:  # 窗口大小不足 2k+1
                continue
            # 2. 记录答案
            avgs[i - k] = s // (k * 2 + 1)
            # 3. 离开窗口
            s -= nums[i - k * 2]
        return avgs

 结论:

  1. avgs = [-1] * len(nums) 初始化只需要O(1),后面遍历处理的数据量小于n
  2. 我的代码中初始化求sum以及根据窗口判断更新ans都会增加耗时

2379. 得到 K 个黑块的最少涂色次数

题目来源:2379. 得到 K 个黑块的最少涂色次数 - 力扣(LeetCode)

解答:

class Solution:
    def minimumRecolors(self, blocks: str, k: int) -> int:
        ans = inf
        left = 0 
        length = len(blocks)
        data = {'W':0,'B':0}
        for right in range(length):
            data[blocks[right]] += 1
            left = right + 1 - k
            if left < 0:
                continue
            ans = min(ans,data['W'])
            data[blocks[left]] -= 1
        return ans
            
        

 2841. 几乎唯一子数组的最大和

题目来源:2841. 几乎唯一子数组的最大和 - 力扣(LeetCode)

分析:

  1. 定长滑动窗口 right遍历元素,left = rigth + 1 - k(通过定长k推导,right - left + 1 = k)
  2. 除了窗口固定外,还需要保证窗口内的不同元素数 >= m,我选择使用字典来存储,当nums[left]在字典中value == 1的时候,需要将该元素移除,如果大于1,那么value减去1,不同元素数量不变

我的解:

class Solution:
    def maxSum(self, nums: List[int], m: int, k: int) -> int:
        ans = 0
        if len(nums) < k:
            return 0
        dic = dict()
        # m 必然小于 k
        ans_tmp = 0
        m_tmp = 0
        for right in range(len(nums)):
            ans_tmp += nums[right]
            if nums[right] not in dic:
                dic[nums[right]] = 0
                m_tmp += 1
            dic[nums[right]]  += 1
            left = right + 1 - k
            if left < 0:
                continue
            if m_tmp >= m:
                ans = max(ans,ans_tmp)
            ans_tmp -= nums[left]
            if dic[nums[left]] == 1:
                dic.pop(nums[left])
                m_tmp -= 1
            else:
                dic[nums[left]] -= 1
        return int(ans)

题解:

class Solution:
    def maxSum(self, nums: List[int], m: int, k: int) -> int:
        ans = s = 0
        cnt = defaultdict(int)
        for i, x in enumerate(nums):
            # 1. 进入窗口
            s += x
            cnt[x] += 1

            left = i - k + 1
            if left < 0:  # 窗口大小不足 k
                continue

            # 2. 更新答案
            if len(cnt) >= m:
                ans = max(ans, s)

            # 3. 离开窗口
            out = nums[left]
            s -= out
            cnt[out] -= 1
            if cnt[out] == 0:
                del cnt[out]

        return ans

作者:灵茶山艾府

2461. 长度为 K 子数组中的最大和

题目来源:2461. 长度为 K 子数组中的最大和 - 力扣(LeetCode)

我的解: 

class Solution:
    def maximumSubarraySum(self, nums: List[int], k: int) -> int:
        from math import inf
        dic = defaultdict(int)
        left = 0
        ans = -inf
        ans_tmp = 0
        # 存储不同元素数
        element = 0
        for right,num in enumerate(nums):
            if dic[num] == 0:
                element += 1
            ans_tmp += num
            dic[num] += 1
            left = right + 1 - k
            if left < 0:
                continue
            if element == k:
                ans = max(ans,ans_tmp)
            if dic[nums[left]] > 1:
                dic[nums[left]] -= 1
            elif dic[nums[left]] == 1:
                element -= 1
                dic[nums[left]] -= 1
            ans_tmp -= nums[left]
        if ans == -inf :
            return 0
        return ans 

题解:

class Solution:
    def maximumSubarraySum(self, nums: List[int], k: int) -> int:
        ans = s = 0
        cnt = defaultdict(int)
        for i, x in enumerate(nums):
            # 1. 进入窗口
            s += x
            cnt[x] += 1

            left = i - k + 1
            if left < 0:  # 窗口大小不足 k
                continue

            # 2. 更新答案
            if len(cnt) == k:
                ans = max(ans, s)

            # 3. 离开窗口
            out = nums[left]
            s -= out
            cnt[out] -= 1
            if cnt[out] == 0:
                del cnt[out]

        return ans

作者:灵茶山艾府

总结:
1. 不同元素的个数完全可以通过len(dic)来获取,没必要单独搞一个变量,同时不用担心时间开销,因为len操作时间复杂度为O(1)

2. 要常看题解

 1423. 可获得的最大点数

思路:

  1. 从所有能够取到的长度为k的子数组中进行遍历,返回子数组最大和
  2. 能够取到的子数组根据题目条件判断

我的解:

class Solution:
    def maxScore(self, cardPoints: List[int], k: int) -> int:
        s = sum(cardPoints[:k])
        right = 0
        ans = s
        if len(cardPoints) <= k:
            return sum(cardPoints)
        for left in range(k - 1, -1 ,-1):
            right = left + 1 - k
            s += cardPoints[right - 1]
            s -= cardPoints[left]
            ans = max(ans,s)
        ans = max(ans,sum(cardPoints[-k:]))
        return ans
        

 1052. 爱生气的书店老板

题目来源:1052. 爱生气的书店老板 - 力扣(LeetCode)

 分析:

  1. 求不生气的时间最多顾客数量
  2. 将原始本来就不生气的时间内所有顾客累加 res 
  3. 通过滑动grumpy来获取,在连续minutes时间内不生气导致的满意顾客增量 ans
  4. 最后 res += ans即时结果

答案:

class Solution:
    def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int:
        '''
        customers = [1,0,1,2,1,1,7,5], 
        grumpy =    [0,1,0,1,0,1,0,1], 
        minutes =   3
        遍历grumpy 滑动长度为3窗口  求生气窗口最大增值 
        所有不生气的时间都顾客数量 加上 最大增值 即时最后结果
        '''
        # 增值 
        ans = ans_tmp = 0
        left = 0
        res = 0
        for right,gru in enumerate(grumpy):
            # -----> 移入
            if gru == 0:
                res += customers[right]  # 加和所有不生气的人数
            # 计算窗口增值
            if gru == 1:
                ans_tmp += customers[right]
            left = right + 1 - minutes
            if left < 0 :
                continue
            # 更新增值  以及对应的窗口左端点 作为该窗口标识
            if ans < ans_tmp:
                ans = max(ans,ans_tmp)
            # -----------> 移除
            if grumpy[left] == 1:
                tmp = customers[left]
                ans_tmp -= tmp
        res += ans
        return res

灵神:

class Solution:
    def maxSatisfied(self, customers: List[int], grumpy: List[int], minutes: int) -> int:
        s = [0, 0]
        max_s1 = 0
        for i, (c, g) in enumerate(zip(customers, grumpy)):
            s[g] += c
            if i < minutes - 1:  # 窗口长度不足 minutes
                continue
            max_s1 = max(max_s1, s[1])
            if grumpy[i - minutes + 1]:
                s[1] -= customers[i - minutes + 1]  # 窗口最左边元素离开窗口
        return s[0] + max_s1

作者:灵茶山艾府

 1652. 拆炸弹

题目来源:1652. 拆炸弹 - 力扣(LeetCode)

分析:

        1. 窗口固定为k 只不过移动方向随k对正负变化

        2. 我区分了 k > 0 and k < 0 的情况

        3. 在不同方向上进行逻辑移动,不用方向也移动的逻辑还不同,所以花费了比较多的时间来做这道“简单题”

我的解答:

class Solution:
    def decrypt(self, code: List[int], k: int) -> List[int]:
        if k == 0:
            return [0 for i in range(len(code))]
        
        if k > 0:
            ans = []
            ans_tmp = 0
            last_num = 0
            left = 0
            for right,num in enumerate(code):
                # in
                ans_tmp += num
                left = right + 1 - k
                if left < 0:
                    continue
                # update
                if left == 0:
                    last_num = ans_tmp
                else:
                    ans.append(ans_tmp)
                # out
                ans_tmp -= code[left]

            for right in range(k - 1):
                # in 
                ans_tmp += code[right]
                left = right + 1 - k
                ans.append(ans_tmp)
                ans_tmp -= code[left]
            ans.append(last_num)
            return ans
        else:
            ans = [0 for i in range(len(code))]
            ans_tmp = 0
            left = -1
            for right in range(-1,-len(code),-1):
                # in
                ans_tmp += code[right]
                left = right - 1 - k
                if left >= 0:
                    continue
                ans[left + 1] = ans_tmp
                ans_tmp -= code[left ] 
            for right in range(0,k,-1):
                ans_tmp += code[right]
                left = right - k
                ans[left] = ans_tmp
                ans_tmp -= code[left - 1]
            return ans

题解:

class Solution:
    def decrypt(self, code: List[int], k: int) -> List[int]:
        n = len(code)
        ans = [0] * n
        r = k + 1 if k > 0 else n  # 第一个窗口的右开端点
        k = abs(k)
        s = sum(code[r - k: r])  # ans[0]
        for i in range(n):
            ans[i] = s
            s += code[r % n] - code[(r - k) % n]
            r += 1
        return ans

 总结:

        1. 找规律

        2. 我想我没有找到题解这个规律,应该是刷的题目,太少了,就导致解题思路以及思维方式比较局限

        3. 题解上面还有一个作者的分析思路,我认为该分析方法十分重要,通过手绘来寻找算法规律,然后开始敲代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值