leetcode刷题记录:算法(三)滑动窗口算法

滑动窗口是为了减少循环次数。
滑动窗口有几个应用场景:

  1. 给定一个整数数组,计算长度为 ‘k’ 的连续子数组的最大总和。
    这中应用情况下,窗口大小是固定的,窗口每移动一步,窗口中的数值和上一步中的有重合,这个重合的部分不再参与计算,滑动窗口就是利用这个来将计算量减少。

  2. 给定一个字符串S和一个目标字符串T,找到S中包含所有T中字母的最小子字符串。
    这种情况下,窗口大小是变化的。先设置窗口大小为0,然后移动右侧边界,逐步扩张窗口,直至能满足要求。

  3. 算是前面两种的集合了。给定一个字符串S,找到S中重复的最长子串。这种情况下的滑动窗口,是不需要进行缩减的,因为要求的是最长,窗口长度只增不减。

别的暂时还没遇到,遇到再补。
继续刷题,还是饲养员up推荐的三道题。

3.无重复字符的最长子串
这个题就是前面说的第二种情况,思路还是很清晰的。
写的有点复杂,搞了一个flag将情况分为移动左窗口和移动右窗口,还加了一个判断特定情况的条件(是否是空字符串)。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if len(s)==0:
            return 0
        s_left = 0
        s_right = 0
        max_len = 1
        flag = 0  ##0时移动右窗口,1时移动左窗口
        window = s[s_left]
        while (s_right < len(s)-1):
            if flag == 0:
                s_right += 1
                if s[s_right] not in window:
                    window = s[s_left:s_right+1]
                    if len(window) > max_len:
                        max_len = len(window)
                elif s[s_right] in window:
                    flag = 1
                    window = s[s_left:s_right+1]
            else:
                s_left += 1
                if s[s_left - 1] == s[s_right]:
                    flag = 0
                    window = s[s_left:s_right+1]
        return max_len

官方给的答案如下,比我的答案短很多,但是运行起来时间和内存占用比我的更拉胯。
其中,官方答案利用了一个哈希集合,我一直用的列表切片。
可以学习到的几点:

  1. 如何让代码兼容特殊情况(比如说空集或只有一个数据),使得代码应用更广泛,我自己写的里面对空字符串和单字符的情况做了另外处理。
  2. 取最大值不要用if了,很蠢,python给提供了max函数竟然不用(叹气)
  3. 官方给的例子用了嵌套循环,减少了代码量,我则是通过flag来对右窗口进行多次移动的,即多次重复操作的时候,可以考虑用个循环代替flag。
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 哈希集合,记录每个字符是否出现过
        occ = set()
        n = len(s)
        # 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        rk, ans = -1, 0
        for i in range(n):
            if i != 0:
                # 左指针向右移动一格,移除一个字符
                occ.remove(s[i - 1])
            while rk + 1 < n and s[rk + 1] not in occ:
                # 不断地移动右指针
                occ.add(s[rk + 1])
                rk += 1
            # 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = max(ans, rk - i + 1)
        return ans

209.长度最小的子数组
好家伙,我在上一个代码的基础上修修补补,最后击败了全国百分之11的对手,垃圾制造者就是我了。

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        # 这里采用list了,不用set了,set的特性是没有重复
        occ = list()
        ans = list()
        n = len(nums)
        if n == 0:
            return 0
        # 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        rk = -1
        for i in range(n-1):
            if i != 0:
                # 左指针向右移动一格,移除一个字符
                occ.pop(0)
            while rk + 1 < n and sum(occ) < s:
                # 不断地移动右指针
                occ.append(nums[rk + 1])
                rk += 1
            # 第 i 到 rk 个字符是一个极长的无重复字符子串
            if sum(occ) >= s:
                ans.append(rk - i + 1)
        return 0 if len(ans)==0 else min(ans)

改了一遍,稍微好了一点,不过用for i in range(n)确实可以排除空集特殊情况:

class Solution:
    def minSubArrayLen(self, s: int, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        s_left = 0
        s_right = -1
        sum_sub = 0
        min_len = list()
        flag = 0    ##  0时移动右窗口,1时移动左窗口
        while (s_right < len(nums)):
            if flag == 0 and s_right <len(nums)-1:
                ## 移动右窗口
                s_right += 1
                ## 更新和
                sum_sub += nums[s_right]
                ## 如果此时和不满足要求,那么flag不变,继续移动右窗口
                ## 如果此时和满足要求,那么记录长度,并开始移动左窗口
                if sum_sub >= s:
                    min_len.append(s_right-s_left+1)
                    flag = 1
            else:
                ## 移动左窗口
                s_left += 1
                ## 更新和
                sum_sub -= nums[s_left-1]
                ## 如果此时和不满足要求,那么停止移动左窗口,开始移动右窗口
                if sum_sub < s:
                    flag = 0
                ## 如果此时和满足要求,那么记录长度,并继续移动左窗口
                else:
                    min_len.append(s_right-s_left+1)
                if s_left == s_right:
                    break
        return 0 if len(min_len)==0 else min(min_len)

第三道题,也是我第一眼看上去没有思路的题:

  1. 替换后的最长重复字符
    给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。

这篇文是写滑动窗口的,那么自然是从滑动窗口入手了。
从题目中可以提炼出:

  1. 最短的窗口为k+1。这很好理解:如果原来的字符串中,无重复的字符,那么替换k次之后,子串的长度为k+1(随便找一个字符,让后把这个字符相邻的k个字符替换成相同的)
  2. 假设最后得到的子串长度为length,那么这个窗口中,有k个是替换的,重复字符为nums=length-k,而在满足这一条件之前,length-nums<=k,所以在这条件满足之前,都可以扩大窗口。
  3. 我们不需要减小窗口的大小。因为求的是最长的子串。

将思路整理成伪代码:

  • 如果向右移动一次后,窗口的长度length-max(窗口内重复字符数量)<=k:
  • 窗口右侧移动一步
  • 反之:
  • 窗口向右滑动一步

灵光一闪,思路可以简化为:

  • 在一步循环中,无论是什么情况,窗口右侧都会移动一次,左窗口则视情况要不要移动。

写了好久终于通了,不过效果还是挺差的,在时间上只击败了57%的人。

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
    	##maxCount为了记录26个字母在当前滑动窗口中的数目
        maxCount = [0] * 26 
        left, right = 0, -1
        n = len(s) 
        ## 终于学会了这招,这样的话如果是空集,就不会进入循环造成错误
        ## right从-1,每步+1,一直到 n-1,所以一共n步
        for i in range(n):
        	## 右窗移动一步
            right += 1
            ## 将新加入的字符数目记录下来,ord()返回的是ASIC码
            maxCount[ord(s[right])-ord('A')]+=1
            ## 这条就是上面分析出来的条件,如果满足条件,左窗口向右移动一次
            if (right-left) - max(maxCount) >= k:
                left += 1
                ## 左窗口向右移动一次,那么就抛弃了一个字符,我们需要把这个字符的数目-1
                maxCount[ord(s[left-1])-ord('A')] -= 1
        ## 最后返回的是窗口长度
        return right-left+1

好啦,滑动窗口的题就先到这里了,我会继续分享自己的学习历程,希望看到的大佬多多指点,

©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页