LeetCode专项练习之滑动窗口(Sliding window)笔记

本文是根据穷码农的LeetCode刷题建议而进行专项练习时记录的心得。

滑动窗口,笼统来讲就是在列表(数组)里建立一个滑动窗口,并往其中塞入满足条件的元素。每循环一次,右指针便会右移,同时依据情况移动左指针。

今天的笔记包含滑动窗口(Sliding Window)类型下的7个题目,它们在leetcode上的编号和题名分别是:

  • 3 - Longest Substring Without Repeating Characters
  • 30 - Substring with Concatenation of All Words
  • 76 - Minimum Window Substring
  • 209 - Minimum Size Subarray Sum 
  • 239 - Sliding Window Maximum
  • 424 - Longest Repeating Character Replacement
  • 904 - Fruit into Baskets

下面将根据以上顺序分别记录代码和对应心得,使用的编译器为Python3。


Longest Substring Without Repeating Characters

Given a string, find the length of the longest substring without repeating characters.

Example 1:
Input: "abcabcbb"
Output: 3 
Explanation: The answer is "abc", with the length of 3. 

Example 2:
Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:
Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3. 

Note that the answer must be a substring, "pwke" is a subsequence and not a substring.

此题的关键是使用队列。在新增遇到重复字符前,记录并更新最长长度,遇到时队列另一测就开始出队,直到遇到重复字符为止(重复字符串也得出队)。个人认为难度较低。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # solution: 队列 (滑动窗口)
        result = []
        max = 0
        for element in s:
            if len(result) == 0 or element not in result:
                result.append(element)
                cur = len(result)
                if cur > max:
                    max = cur
            else:
                while result[0] != element:

                    result.remove(result[0])

                result.remove(element)
                result.append(element)

        return max

 

Substring with Concatenation of All Words

You are given a string, s, and a list of words, words, that are all of the same length. Find all starting indices of substring(s) in s that is a concatenation of each word in words exactly once and without any intervening characters.

Example 1:
Input:
  s = "barfoothefoobarman",
  words = ["foo","bar"]
Output: [0,9]
Explanation: Substrings starting at index 0 and 9 are "barfoo" and "foobar" respectively.
The output order does not matter, returning [9,0] is fine too.

Example 2:
Input:
  s = "wordgoodgoodgoodbestword",
  words = ["word","good","best","word"]
Output: []

此题虽然没有被列入为穷码农的滑动窗口推荐题目,但考虑到它的热度相对较大,我便选择它来进行练习。并且,此题采用了原创解法,虽然效率较低,但起码对于一道hard题,能自己写出来也算是一种进步吧。其核心思想是利用双指针,比对字符串中每个潜在单词是否属于规定单词列表里的单词,若满足条件则用字典进行记录,并在每次循环时比对该字典里的所有单词是否与单词列表完全匹配。值得注意的是,如果字典最终无法完全匹配,那便清除字典,并重新记录。但此时左指针的起点将为原字典里第一个记录的单词的第二个字母,而非第二个记录的单词。

from collections import Counter

class Solution:
    def findSubstring(self, s: str, words: list) -> list:
        # solution: sliding window。原创做法,但效率较低。

        # special considerations:
        if len(s) == 0 or len(words) == 0:
            return []

        # create parameters
        ans = []
        wordsLen = len(words[0])
        wordform = Counter(words)
        left, right = 0, wordsLen-1
        curWordDic = {}
        mark = -1

        # start moving the right index
        while right < len(s):
            curWord = s[left:right + 1]

            # initiate index marker
            if len(curWordDic) == 0:
                mark = left

            if curWord in words:
                # determine if the word is repeated
                if curWordDic.get(curWord) is not None:
                    if curWordDic[curWord] < wordform[curWord]:
                        curWordDic[curWord] += 1
                    else:
                        curWordDic.clear()
                        left = mark + 1
                        right = mark + wordsLen
                        mark = -1
                        continue
                else:
                    curWordDic[curWord] = 1

                # determine if the window satisfies the requirement
                if curWordDic == wordform:
                    ans.append(mark)

                    # reassign the dictionary
                    curWordDic.clear()
                    left = mark + 1
                    right = mark + wordsLen
                    mark = -1
                    continue

                left += wordsLen
                right += wordsLen

            else:
                # clear the content
                curWordDic.clear()
                left = mark + 1
                right = mark + wordsLen
                mark = -1

        return ans

 

Minimum Window Substring

Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).

Example:
Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"

Note:
If there is no such window in S that covers all characters in T, return the empty string "".
If there is such window, you are guaranteed that there will always be only one unique minimum window in S.

此题的核心思路是创建双指针,均从列表第一个元素开始。右指针不断右移并把指向元素添加到窗口里面去,一旦窗口里元素的的种类和数量与指定字符串相同,便开始右移左指针,直到窗口的元素种类或数量不足为止。同时,在移动左指针时,不断比较此时窗口大小,更新最小滑动窗口。

但有一个改进的方法,便是一开始“过滤”掉所有在s出现但没有在t中出现的字符。之后再采用上诉方法求最小窗口。不过,虽然叫做“过滤”,但这并不意味着在s上直接删除字符,而是用一个字典记录所有出现在s和t中的字符,并记录这些字符在s里面的相应位置。

from collections import Counter


class Solution:
    def minWindow(self, s: str, t: str) -> str:
        # better solution: improved sliding window。通过只用字符串s中出现在字符串t中的字符(注意记录下标)来创建window,来
        # 减少循环比较次数。
        # 巧妙之处在于,虽然需过滤没有出现在t中的字符,但这并不意味着在s上直接删除字符,而是用一个字典记录所有出现在s和t中的字符,并记录
        # 这些字符在s里面的相应位置。

        # special consideration: s or t is empty
        if not t or not s:
            return ""

        # create a new "S"
        filtered_S = []
        for i, char in enumerate(s):
            if char in t:
                filtered_S.append((i, char))

        # create two pointers and other parts
        left, right = 0, 0
        formed = 0
        window = {}
        dict_t = Counter(t)
        ans = (float("inf"), None, None)

        # move right pointers
        while right < len(filtered_S):
            # add elements
            window[filtered_S[right][1]] = window.get(filtered_S[right][1], 0) + 1

            # decide whether the current window satisfies the requirement
            if window.get(filtered_S[right][1]) == dict_t.get(filtered_S[right][1]):
                formed += 1

            # decide whether to move the left pointer
            while formed == len(dict_t):
                # compare the length and record it
                if ans[0] > filtered_S[right][0] - filtered_S[left][0] + 1:
                    ans = (filtered_S[right][0] - filtered_S[left][0] + 1, filtered_S[left][0], filtered_S[right][0])

                # reduce the number of the specified element
                window[filtered_S[left][1]] -= 1

                # decide whether to reduce the form
                if window.get(filtered_S[left][1]) < dict_t.get(filtered_S[left][1]):
                    formed -= 1

                left += 1

            right += 1

        return "" if ans[0] == float("inf") else s[ans[1]: ans[2]+1]

 

Minimum Size Subarray Sum

Given an array of n positive integers and a positive integer s, find the minimal length of a contiguous subarray of which the sum ≥ s. If there isn't one, return 0 instead.

Example: 
Input: s = 7, nums = [2,3,1,2,4,3]
Output: 2
Explanation: the subarray [4,3] has the minimal length under the problem constraint.

Follow up:
If you have figured out the O(n) solution, try coding another solution of which the time complexity is O(n log n). 

此题的逻辑与"Minimum Window Substring"完全相同,唯一不同的是它在比较window是否和输入内容匹配时所需的参数数量较少,相对容易一点。

class Solution:
    def minSubArrayLen(self, s: int, nums: list) -> int:
        # solution: 滑动窗口。

        # special considerations:
        if len(nums) == 0:
            return 0

        minLen = (float("inf"), None, None)
        curSum = 0
        left, right = 0, 0
        window = []

        # start recording the numbers and move the right pointer
        while right < len(nums):
            # add the current right element
            window.append(nums[right])

            # add to the current sum
            curSum += nums[right]

            # compare if the sum reaches the defined number and move the left pointer if it does
            while curSum >= s:
                curLen = len(window)
                # find the shortest length
                if minLen[0] > curLen:
                    minLen = (curLen, left, right)

                # process window and sum because of moving the left pointer
                window.remove(nums[left])
                curSum -= nums[left]

                left += 1

            right += 1

        return 0 if minLen[0] == float("inf") else len(nums[minLen[1]: minLen[2]+1])

 

Sliding Window Maximum

Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Example:

Input: nums = [1,3,-1,-3,5,3,6,7], and k = 3
Output: [3,3,5,5,6,7] 

Explanation: 
Window position                Max
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

Note: 
You may assume k is always valid, 1 ≤ k ≤ input array's size for non-empty array.

Follow up:
Could you solve it in linear time?

解决此题的办法是通过“双向队列”和“单调队列思想”。所谓双向队列,我的理解是队列的头尾元素都可以进出,而非一边进一边出。在此基础上,可以优化暴力法的复杂度。而"单调队列"在此题中的体现是:每次准备从队尾新增一个数字时,都将之与队尾的元素进行比较,挨个pop掉所有小于此数的数字,直到在队列里遇到更大的数字或队伍为空为止,然后append该数字;这样一来队列将会呈现单调递减的形式,最左边(队头)的元素永远是最大的。值得一提的是,sliding window里面的数字并不一定和deque的数字相对应。因此,最后准备移除window最左边的元素时,并不一定指pop掉deque里最左边的数字,而是通过下标查找(nums[i-k+1]),看两者是否是同一个数字。有关单调队列,可以在leetcode解析:单调队列里找到更详细的解释。

import collections


class Solution:
    def maxSlidingWindow(self, nums: list, k: int) -> list:
        # solution 1: 滑动窗口(暴力法)。根据题意直接循环查找每个window的最大值,赋给answer列表,返回即可。复杂度O(n*k)

        # solution 2: 双向队列(deque)
        # special considerations:
        numLen = len(nums)
        if numLen == 0:
            return []

        # create the deque and other elements
        deque = collections.deque()
        ans = []

        # start traversing
        for i in range(0, numLen):
            # pay attention to "k-1": fill k-1 elements first
            if i < k-1:
                # fill the deque at first in a monotonic way
                while deque and deque[len(deque) - 1] < nums[i]:
                    # delete from the right side
                    deque.pop()
                deque.append(nums[i])
            else:
                # add new element and delete all elements that are smaller than it
                while deque and deque[len(deque)-1] < nums[i]:
                    # delete from the right side
                    deque.pop()
                deque.append(nums[i])

                # identify the biggest number (it is the left one)
                ans.append(deque[0])

                # remove the original left number in sliding window (not always the left one in deque)
                if deque[0] == nums[i-k+1]:
                    deque.popleft()

        return ans

 

Longest Repeating Character Replacement

Given a string s that consists of only uppercase English letters, you can perform at most k operations on that string.

In one operation, you can choose any character of the string and change it to any other uppercase English character.

Find the length of the longest sub-string containing all repeating letters you can get after performing the above operations.

Note:
Both the string's length and k will not exceed 104.

Example 1:

Input:
s = "ABAB", k = 2

Output:
4

Explanation:
Replace the two 'A's with two 'B's or vice versa.
 

Example 2:

Input:
s = "AABABBA", k = 1

Output:
4

Explanation:
Replace the one 'A' in the middle with 'B' and form "AABBBBA".
The substring "BBBB" has the longest repeating letters, which is 4.

此题可以先简化思想:当k=0时,此题便为寻找字符串中最长重复子字符串。由此可见,当右指针遇到重复字符串时,左指针不动,继续移动右指针;反之则同时移动左右指针,直到遇到重复字符为止。那么,当k=1时,此题便转变为:寻找一个子串,使之变换k个字符便能成为最长重复子串。言下之意,是寻找子串里出现次数最多的字符的个数,使之加上k,看是否大于window的长度。这便是解决此题的核心要点。

另外,此题无需单独设立一个window列表,仅需指针的位置即可。

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        # solution: sliding window
        # special considerations:
        lenS = len(s)
        if lenS < 3:
            return lenS

        #window = []
        ans = 0
        left, right = 0, 0
        windowDic = {}

        # start moving the right pointer
        while right <= lenS - 1:
            # add the char
            #window.append(s[right])
            #windowLen = len(window)

            # save the type and quantity
            windowDic[s[right]] = windowDic.get(s[right], 0) + 1

            # determine if it reaches the limitation
            curMaxLen = self.maxRepeatLen(windowDic, k)

            if curMaxLen < right-left+1:
                # move the left pointer
                #window.remove(s[left])
                windowDic[s[left]] -= 1
                left += 1

            else:
                curMaxLen = right-left+1

            if ans < curMaxLen:
                ans = curMaxLen

            right += 1

        return ans

    def maxRepeatLen(self, windowDic: dict, k: int):
        maxAmount = 0
        for amount in windowDic.values():
            if amount > maxAmount:
                maxAmount = amount

        return maxAmount + k

 

Fruit Into Baskets

In a row of trees, the i-th tree produces fruit with type tree[i].

You start at any tree of your choice, then repeatedly perform the following steps:

Add one piece of fruit from this tree to your baskets. If you cannot, stop.
Move to the next tree to the right of the current tree. If there is no tree to the right, stop.
Note that you do not have any choice after the initial choice of starting tree: you must perform step 1, then step 2, then back to step 1, then step 2, and so on until you stop.

You have two baskets, and each basket can carry any quantity of fruit, but you want each basket to only carry one type of fruit each.

What is the total amount of fruit you can collect with this procedure?

Example 1:
Input: [1,2,1]
Output: 3
Explanation: We can collect [1,2,1].

Example 2:
Input: [0,1,2,2]
Output: 3
Explanation: We can collect [1,2,2].
If we started at the first tree, we would only collect [0, 1].

Example 3:
Input: [1,2,3,2,2]
Output: 4
Explanation: We can collect [2,3,2,2].
If we started at the first tree, we would only collect [1, 2].

Example 4:
Input: [3,3,3,1,2,1,1,2,3,3,4]
Output: 5
Explanation: We can collect [1,2,1,1,2].
If we started at the first tree or the eighth tree, we would only collect 4 fruits.
 
Note:
1 <= tree.length <= 40000
0 <= tree[i] < tree.length

此题采用普通的队列便可顺利完成解答。不过我看此题热度很低,估计考察的企业比较少吧。

class Solution:
    def totalFruit(self, tree: list) -> int:
        # my solution: 队列
        basket = []
        type = 0
        count = 0
        max = 0
        for i in range(len(tree)):
            if len(basket) == 0:
                basket.append(tree[i])
                type += 1
                count += 1
                continue

            if tree[i] in basket:
                basket.append(tree[i])
                count += 1
                continue

            if tree[i] not in basket:
                if type == 1:
                    basket.append(tree[i])
                    type += 1
                    count += 1
                else:
                    if max < count:
                        max = count
                    # remove one type of fruits
                    preserveType = basket[len(basket)-1]
                    removeindex = 0
                    j = len(basket)-2
                    while j > -1:
                        if basket[j] == preserveType:
                            j -= 1
                            continue
                        else:
                            removeindex = j
                            break
                    # remove fruits
                    for k in range(removeindex, -1, -1):
                        basket.remove(basket[k])
                        count -= 1
                    #type -= 1
                    # add new type
                    basket.append(tree[i])
                    count += 1
        if max < count:
            max = count

        return max

总结

滑动窗口的规律相对明显:

  1. 题目总是让你去求最长/最短的连续字符串/数组(列表);
  2. 题目背景总是围绕着数组、字符串展开。

如果笔记存在一些问题,发现后我会尽快纠正。

*注:本文的所有题目均来源于leetcode

(题外话,前一段时间为了申请视频剪辑实习,连续花了一星期剪辑视频,因此没有持续练习Leetcode。但可惜最后还被刷下来了,连个回音都没有。之后,自己又接二连三投了几个互联网实习简历,但最后都没啥反应……算了,还是先去积累一些自己感兴趣的项目经验吧。目前,为了能在淘宝上抢口罩,我一直在整Python爬虫,Leetcode练习略有疏忽。不过我感觉挺有意思的,虽然直到现在还没抢到,但使程序以最快速度自动化操纵网页、抓取信息似乎能极大程度激发我的动力,整起来压根就停不下来。因此,后续的Leetcode刷题进度可能会慢下来,但考虑到动态规划是墨大AI这门课必须要掌握的内容,专项练习应该不会断掉。)

你可能感兴趣的:(leetcode训练)