基础算法8 - 滑动窗口

363 阅读7分钟

概念 & 应用

sliding window可以解决数组 or 字符串的子元素问题:

  • 将嵌套的循环问题,转换为单循环问题 -> 降低时间复杂度,一般为O(N)
  • 应用:
    • 题干:
      • 连续的元素:string / subarray / linkedList
      • 最值:min / max / longest / shortest / keyword
    • 一般 string 使用 map 作为window(如果说明了只有小写字母也可以使用int[26]
    • 字母类可若暴力尝试 26 个字母(比如1-unique,2-unque,……),然后套模版(ie:LC395
    • 套娃题:exact(K) = atMost(K) - atMost(K-1)
    • 多重限制的压轴题需要考虑是否为单调队列


模版

  • 本质上仍是two pointer:左边left & 右边right = iterator(i)
  • 三步走:进 -> 出 -> 算

❤️❤️ Longest:

    while 不符合条件:
        left+=1

❤️❤️ Shortest:

    if 符合条件:
        while left指向的元素还可以删除:
              删除left指向的元素
              left+=1
class Solution:
    # 本质上仍是two pointer:左边left,右边iterator(i)
    def lengthOfLongestSubstringDistinct(self, s: str, k: int) -> int:
        dic = {}
        l, r, res = 0, 0, 0
        for i, c in enumerate(s):
            dic[c] = dic.get(c, 0) + 1  # 1 进:当前遍历的i进入窗口,更新map
            while len(dic) > k:  # 2 出:当窗口不符合条件时,left持续退出窗口
                dic[s[l]] -= 1
                if dic[s[l]] == 0:
                    del dic[s[l]]
                l += 1
            res = max(res, i - l + 1)  # 3 算:现在窗口valid了,计算结果
        return res


*题型1:size fixed(Easy)

  • 窗口长度确定
    • 比如:max sum of size = k

*题型2:size可变,单限制条件(Medium)

  • 比如找到subarray sum 比target大一点点

❤️ 题型3:size可变,双限制条件(Medium)

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

image.png

Solu 1:Set去重

套模版,略

  • 因为只要求distinct(没有frequency的要求),直接用set就可以

Code 1:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        seen = set()
        l, res = 0, 0
        for i, c in enumerate(s):
            while c in seen:
                seen.remove(s[l])
                l += 1
            seen.add(c)
            res = max(res, i - l + 1)
        return res

Solu 2:dictionary优化 ❤️

  • dict = <current character : cur上一次出现的idx>

Code 2:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        dic = {}
        max_len, l = 0, 0
        # left = 上一次current character出现的index
        for i, c in enumerate(s):
            if c in dic: # cur之前就出现过,但不确定是否在当前窗口内
                l = max(l, dic[c] + 1)
            dic[c] = i
            max_len = max(max_len, i - l + 1)
        return max_len


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

image.png

Solu:

套模版,k = 2

  • 进:当前character进入map
  • 出:移除window内多余的character
  • 算:计算长度结果

Code:

class Solution:
    def lengthOfLongestSubstringTwoDistinct(self, s: str) -> int:
        dic = {}
        l, res = 0, 0
        for i, c in enumerate(s):
            dic[c] = dic.get(c, 0) + 1  # 1 进
            while len(dic) > 2:
                dic[s[l]] -= 1  # 2 出
                if dic[s[l]] == 0:
                    del dic[s[l]]
                l += 1
            res = max(res, i - l + 1)  # 3 算
        return res


340. 至多包含 K 个不同字符的最长子串(Medium)

image.png

Solu:

  • dic = <cur_character : frequency of cur>
  • 直接把len(dic)k做比较即可

Code:

class Solution:
    def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
        dic = {}
        l, res = 0, 0
        for r, c in enumerate(s):
            dic[c] = dic.get(c, 0) + 1
            while len(dic) > k:
                dic[s[l]] -= 1
                if dic[s[l]] == 0:
                    del dic[s[l]]
                l += 1
            res = max(res, r - l + 1)
        return res


76. 最小覆盖子串(Hard)

image.png

Solu:

  • 饱和count的valid subarray不值得去计算(即:#char in s[l:r] > #char in t)。只考虑对于当前left,第一次遇到valid subarray s[left, right]时的case:再进行收缩窗口&计算长度

Code:

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        dic = collections.Counter(t)  # <character - 还剩多少个需要抵消>
        l, ans, k = 0, '', len(t)
        for r, c in enumerate(s):
            if c in dic:
                if dic[c] > 0:  # cur还没抵消完
                    k -= 1
                dic[c] -= 1
            if k == 0:  # valid substring
                while k == 0:  # 收缩窗口
                    if s[l] in dic:
                        dic[s[l]] += 1
                        if dic[s[l]] > 0:  # s[l]没有抵消完
                            k += 1
                    l += 1
                if r - l + 1 < len(ans) or not ans:
                    ans = s[l - 1:r + 1]
        return ans


❤️ 395. 至少有 K 个重复字符的最长子串(Medium)

image.png

Solu:

  • def longest_substring_with_n_unique(s: str, k: int, unique: int) -> int: 找出s中的最长子串, 要求该子串具有unique个unique character 且 其中的每一字符出现frequency ≥ k
  • 最多有26个unique character -> longest_substring(s: str, n: int) = max{longest_substring_with_n_unique(s, k, unique)} (1 ≤ unique ≤ #distinct char in s)
    • 关心的是一个char最少出现k次,所以只有第k次出现这个char的时候才需要count

Code:

class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        def longestSubstringNUnique(unique: int) -> int:
            '''
            Find the longest substring with exactly unique characters, and each distinct character is at least k times.
            :param unique: number of unique characters
            :return: length of longest substring
            '''
            dic = {}
            l, res, validCount = 0, 0, 0
            for r, c in enumerate(s):
                dic[c] = dic.get(c, 0) + 1  # <char : freq>
                if dic[c] == k:
                    validCount += 1  # 第k次出现某个unique char 的 次数
                while len(dic) > unique:  # 保证当前unique char 的个数不超过'unique'
                    dic[s[l]] -= 1
                    if dic[s[l]] == k - 1:
                        validCount -= 1
                    if dic[s[l]] == 0:
                        del dic[s[l]]
                    l += 1
                if validCount == unique:
                    res = max(res, r - l + 1)
            return res
        
        return max(longestSubstringNUnique(unique) for unique in range(1, len(collections.Counter(s)) + 1))


209. 长度最小的子数组(Medium)

image.png

Solu:

  • 保持sum = ∑(s[l:r])r = 当前for-loop到的idx)

Code:

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        sum = 0
        l, res = 0, len(nums) + 1
        for r, num in enumerate(nums):
            sum += num  # 进
            if sum >= target:
                while sum - nums[l] >= target:
                    sum -= nums[l]  # 出
                    l += 1
                res = min(res, r - l + 1)  # 算
        return res if res <= len(nums) else 0


424. 替换后的最长重复字符(Medium)

image.png

Solu:

  • 进:iterator不断前进
  • 出:如果 #使得当前window符合条件所需要的改动次数 过多(即,[window.size] - [window's most frequent char's frequency] > k),则不断收缩窗口左边界
    • 不用真的去修改string
  • 算:最后计算最长值

Code:

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count_array = [0] * 26
        l, res = 0, 0,
        for r, c in enumerate(s):
            count_array[ord(c) - ord('A')] += 1  # 进
            while (r - l + 1) - max(count_array) > k:  # 出
                count_array[ord(s[l]) - ord('A')] -= 1
                l += 1
            res = max(res, r - l + 1)  # 算
        return res


❤️ 1234. 替换子串得到平衡字符串(Medium)

image.png

Solu:滑动窗口

  • 滑动窗口内:需要被替换的子串
  • 滑动窗口外:符合条件的剩余字串(即:#each character ≤ n/4
  • 因为需要获得min{len(被替换子串)},所以一旦窗口外的部分满足条件了,就尝试收缩窗口左边界

Code:

class Solution:
    def balancedString(self, s: str) -> int:
        def isValid(dic, taregt):
            for k, v in dic.items():
                if v > taregt:
                    return False
            return True
        
        count = collections.Counter(s)
        target = len(s) // 4
        l, res = 0, len(s)
        for r, c in enumerate(s):
            if isValid(count, target):
                return 0
            count[c] -= 1
            while l <= r and isValid(count, target):
                res = min(res, r - l + 1)
                count[s[l]] += 1
                l += 1
        return res


❤️ 632. 最小区间(Hard)

image.png

Solu:排序 + HashMap + 滑动窗口

  • 首先将k组数据升序合并成一组,并记录每个数字所属的组
    • 例如:对于[[4,10,15,24,26], [0,9,12,20], [5,18,22,30]][[4,10,15,24,26],[0,9,12,20],[5,18,22,30]],合并升序后可以得到: [(0, 1), (4, 0), (5, 2), (9, 1), (10, 0), (12, 1), (15, 0), (18, 2), (20, 1), (22, 2), (24, 0), (26, 0), (30, 2)][(0,1),(4,0),(5,2),(9,1),(10,0),(12,1),(15,0),(18,2),(20,1),(22,2),(24,0),(26,0),(30,2)]
  • 然后只看所属组的话,那么有[1, 0, 2, 1, 0, 1, 0, 2, 1, 2, 0, 0, 2][1,0,2,1,0,1,0,2,1,2,0,0,2]
  • 按组进行“滑动窗口”,找到满足正好窗口内有k组的最小窗口
    • strategy:如果当前l指向的group的数量仍>1,那么就代表还可以继续收缩窗口

Code:

class Solution:
    def smallestRange(self, nums: List[List[int]]) -> List[int]:
        numGroups = len(nums)
        ordered = sorted((j, i) for i, points in enumerate(nums) for j in points)  # val:groupID
        dic = {}
        l, res = 0, [ordered[0][0], ordered[-1][0]]
        for r in range(len(ordered)):
            dic[ordered[r][1]] = dic.get(ordered[r][1], 0) + 1
            if len(dic) == numGroups:
                while dic[ordered[l][1]] > 1:  # 如果当前l指向的group的数量仍>1,那么就代表还可以收缩窗口
                    dic[ordered[l][1]] -= 1
                    l += 1
                if ordered[r][0] - ordered[l][0] < res[1] - res[0]:
                    res = [ordered[l][0], ordered[r][0]]
        return res


❤️ 套娃题:exact(K)

  • exactt(K) = atMost(K) - atMost(K-1)
    • atMost(K)的累加过程当成longest来做!!

992. K 个不同整数的子数组(Hard)

image.png

Solu:

  • exactly(K) = atMost(K) - atMost(K-1)
  • 对于每个右边界r,设:
    • nums[l1 : r]是满足#distinct num ≤ k的最长的subarray
    • nums[l2 : r]是满足#distinct num ≤ k-1的最长的subarray
    • #以 r 为右边界的满足有正好 k 个不同数字的subarray = #nums[l : r](其中l1 ≤ l < l2) = l2 - l1
      • exactly(K) = ∑(l2 - l1) = atMost(K) - atMost(K-1)

Code:

class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
        def atMostK(unique: int) -> int:
            dic = {}
            l, res = 0, 0
            for r, num in enumerate(nums):
                dic[num] = dic.get(num, 0) + 1
                while len(dic) > unique:
                    dic[nums[l]] -= 1
                    if dic[nums[l]] == 0:
                        del dic[nums[l]]
                    l += 1
                res += r - l + 1  # nums[l:r+1]中所有的subarray有1,2,...,unique个不同的元素
            return res
        
        return atMostK(k) - atMostK(k - 1)


1248. 统计「优美子数组」(Medium)

image.png

Solu:

  • exactt(K) = atMost(K) - atMost(K-1)
  • atMost(K) = #至多有K个奇数的subarray

Code:

class Solution:
    def numberOfSubarrays(self, nums: List[int], k: int) -> int:
        def atMostK(odd: int) -> int:
            cnt, l, res = 0, 0, 0
            for r, n in enumerate(nums):
                cnt += n % 2
                while cnt > odd:
                    cnt -= nums[l] % 2
                    l += 1
                res += r - l + 1
            return res
        
        return atMostK(k) - atMostK(k - 1)


*题型4:size fixed,单限制条件(Hard)

  • 比如sliding window maximum,考察单调队列