滑动窗口相关题目

前言

滑动窗口

什么是滑动窗口?

其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!

如何移动?

我们只要把队列的左边的元素移出就行了,直到满足题目要求!

相关题目如下表所示

题目
53.最大子序和简单
3.无重复字符的最长子串中等
209.度最小的子数组中等
219存在重复元素中等
239/剑指 Offer 59 - I 滑动窗口最大值困难
567. 字符串的排列中等
718. 最长重复子数组中等
402.替换后的最长重复字符
30.串联所有单词的子串困难
76. 最小覆盖子串困难

1 leetcode53. 最大子序和

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

解题思路:

动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
时间复杂度:O(n)

代码

class Solution &#123;
    public int maxSubArray(int[] nums) &#123;
        int ans = nums[0];
        int sum = 0;
        for(int num: nums) &#123;
            if(sum > 0) &#123;
                sum += num;
            &#125; else &#123;
                sum = num;
            &#125;
            ans = Math.max(ans, sum);
        &#125;
        return ans;
    &#125;
&#125;

复杂度分析

  • 时间复杂度:O(n),进行了一次遍历。
  • 空间复杂度:O(1)。

2 leetcode3. 无重复字符的最长子串

题目描述

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

解题思路

本题使用滑动窗口的思路。

步骤:

(1)start不动,end向后移动

(2)当end遇到重复字符,start应该放在位置=MAX(上一个重复字符的位置的后一位 map.get(nums[i]) + 1 ,原有位置start)的最大值,同时记录最长的长度ans。

 这里解释下为啥子start需要取最大值:以 deedf 为例, 在经过第一轮判断e重复之后,start和end同时指向第二个e,end继续向后移,此时遇到了重复的d字符,但此时map中所包含的d的kv值仍是第一个d的为0,而此时start的位置为3。此时为了避免start指针回到了第一个位置,所以需要判断最大值使得指针不会回到最开始的d的位置,所以start=max(0+1,3)=3。

​ 简单来说,一旦start的位置变更之后,对于start之前的元素仍是在map集合中的,如果没有经过比较,一旦end遇到了重复元素的情况会优先从map中存储的前面的字符查找,这并不是期望的结果

(3)怎样判断是否遇到重复字符,且怎么知道上一个重复字符的位置?–用哈希字典的key来判断是否重复,用value来记录该字符的最新位置。

代码

class Solution(object):
    def lengthOfLongestSubstring(self, s):
        """
        :type s: str
        :rtype: int
        """
        hashIndex=&#123;&#125;#字典
        start,ans=0,0
        for end,v in enumerate(s):#end位置,v表示value
            if v in hashIndex.keys():#重复
                start=max(hashIndex.get(v)+1,start)
            hashIndex[v]=end
            ans=max(end-start+1,ans)
        return  ans

复杂度分析

  • 时间复杂度:O(n),进行了一次遍历。
  • 空间复杂度:O(n),借助hash存储过程。

3 leetcode 219. 存在重复元素 II

题目描述

给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 ij,使得 nums [i] = nums [j]**,并且 ij 的差的 **绝对值 至多为 k

维护一个哈希表record,即滑动窗口,里面始终最多包含 k 个元素,当出现重复值时则说明在 k 距离内存在重复元素。每次遍历一个元素则将其加入哈希表中,如果哈希表的大小大于 k,则移除最前面的数字
示例 1:

输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:

输入: nums = [1,0,1,1], k = 1
输出: true
示例 3:

输入: nums = [99,99], k = 2
输出: false

代码

class Solution(object):
    def containsNearbyDuplicate(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: bool
        """
        record=&#123;&#125; 
        start=0
        for i,v in enumerate(nums):
            if v in record.keys():#存在重复值则说明在 k 距离内存在重复元素
                return True
            #不在 就加进字典中
            record[v]=i
            if len(record)>k:#如果哈希表的大小大于 k,则移除最前面的数字
                record.pop(nums[i-k])
        return False

复杂度分析

  • 时间复杂度:O(n),进行了一次遍历。
  • 空间复杂度:O(k),借助hash存储过程。

参考:

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/

4 leetcode209长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

解法1 :滑动窗口解法

解题思路

定义两个指针 start和 end分别表示子数组(滑动窗口窗口)的开始位置和结束位置,维护变量 sum存储子数组中的元素和(即从 nums[start]到 nums[end] 的元素和)。

初始状态下,start和 end都指向下标 0,sum的值为 0。

每一轮迭代,将 nums[end] 加到 sum,如果sum≥target,则更新子数组的最小长度(此时子数组的长度是end−start+1),然后将 nums[start] 从 sum 中减去并将start 右移,直到sum<s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将end 右移。

代码
class Solution(object):
    def minSubArrayLen(self, target, nums):
        """
        :type target: int
        :type nums: List[int]
        :rtype: int
        """
        n=len(nums)
        start,end=0,0
        sum=0
        ans=n+1
        while end<n:
            sum=sum+nums[end]
            while(sum>=target):# 大于target 去除滑动窗口前面的  直到之和小于target
                ans = min(end - start + 1, ans)
                sum=sum-nums[start]
                start = start + 1
            end=end+1
        return 0 if ans==n+1 else ans
复杂度分析

时间复杂度:O(n),其中 n是数组的长度。指针start 和 end最多各移动n 次。

两个while的循环为什么是O(n)?

因为: 两个指针都是单调的向右移动,所以至多2n

内循环有触发条件,也就是说在遍历外循环的过程中,不是每次遍历都会触发内循环执行。也就是说内循环触发的次数是一个常量,随着n的规模变大,触发次数并不一定变大。也就是说,在内循环触发时,最坏情况下复杂度为n,但受到触发次数限制。 假设触发次数为常量A,按照上面最坏情况计算,总的执行次数可表示为N+AN=(A+1)N,(A+1)为常量,因此时间复杂度是O(n)

空间复杂度:O(1)。

解法2 :前缀和+二分查找

解题思路

前缀和:为了使用二分查找,需要额外创建一个数组 sums 用于存储数组 nums 的前缀和,其中sums[i] 表示从nums[0] 到 nums[i] 的元素和。

例如nums = [2,3,1,2,4,3]中对应的sums=[0, 2, 5, 6, 8, 12]。

sums[0] = 0 意味着前 0 个元素的前缀和为 0

sums[1] = A[0] 前 1 个元素的前缀和为 A[0]

得到前缀和之后,对于每个开始下标 i,可通过二分查找得到大于或等于i的最小下标mid,使得sums[bound]−sums[i]≥s,并更新子数组的最小长度:此时子数组的长度是bound−i。

代码
class Solution(object):
    def minSubArrayLen(self, target, nums):
        """
        :type target: int
        :type nums: List[int]
        :rtype: int
        """
        n=len(nums)
        ans=n+1
        sums = [0] * (n+1)
        #得到前缀和
        for i in range(1,n+1):
            sums[i]=sums[i-1]+nums[i-1]
        print(sums)
        #在sums数组中使用二分查找  固定i 查找sums[bound]-sums[i]>=target,则子数组的长度是bound−i
        #sums=[0, 2, 5, 6, 8, 12,15]
        for i in range(n):
            left = i
            right =n
            while(left<=right):
                mid=left+(right-left)//2
                if((sums[mid]-sums[i])==target):
                    ans=min(mid-i,ans)
                    break
                elif((sums[mid]-sums[i])>target):#往左移动
                    ans=min(mid-i,ans)
                    right=mid-1
                else:
                    left=mid+1
        return 0 if ans==n+1 else ans
复杂度分析

时间复杂度:O(nlogn)。

空间复杂度:O(1)

5 leetcode718. 最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中 公共的、长度最长的子数组的长度。

示例:

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

6 leetcode239. 滑动窗口最大值

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值


[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

解题思路

文章目录
  1. 前言
  2. 1 leetcode53. 最大子序和
    1. 题目描述
    2. 解题思路:
    3. 代码
    4. 复杂度分析
  • 2 leetcode3. 无重复字符的最长子串
    1. 题目描述
    2. 解题思路
    3. 代码
    4. 复杂度分析
  • 3 leetcode 219. 存在重复元素 II
    1. 题目描述
    2. 代码
    3. 复杂度分析
  • 4 leetcode209长度最小的子数组
    1. 解法1 :滑动窗口解法
      1. 解题思路
      2. 代码
      3. 复杂度分析
    2. 解法2 :前缀和+二分查找
      1. 解题思路
      2. 代码
      3. 复杂度分析
  • 5 leetcode718. 最长重复子数组
  • 6 leetcode239. 滑动窗口最大值
    1. 题目描述
    2. 解题思路