搞定leetcode面试经典150题之滑动窗口

系列博客目录



理论知识

滑动窗口(Sliding Window)是一种常用的算法技巧,特别适用于在数组或字符串中查找符合条件的子序列或子数组。滑动窗口的基本思想是使用一个窗口(即一段连续的区间)来扫描整个数据结构,并通过动态调整窗口的大小和位置来逐步得出结果。

##滑动窗口算法总结

1. 基本概念

  • 窗口大小固定:在滑动窗口中,窗口的大小固定,每次窗口滑动一步。
  • 窗口大小可变:有时,窗口大小是动态调整的,通过扩展或收缩窗口来满足特定条件。

2. 滑动窗口的类型

  • 固定窗口大小:滑动窗口的大小在整个过程中保持不变。
  • 动态窗口大小:窗口大小根据某些条件进行扩展或收缩,直到满足题目的要求。

3. 适用场景

  • 子数组或子字符串问题:求解给定条件下,子数组或子字符串的最值、最长子序列、最小子序列等问题。
  • 在线算法:当需要一次遍历整个数据结构,并且有某些局部性质需要被保持或更新时。

4. 常见的滑动窗口问题类型

1) 最小/最大长度子数组

  • 题目:给定一个整数数组,求和大于或小于等于某个目标的最小子数组长度。
  • 解法:可以通过调整窗口的左右边界来扩展或收缩窗口,直到满足条件。

2) 最长子串/子数组

  • 题目:给定一个字符串或数组,要求找出满足某些条件的最长子串/子数组。
  • 解法:通过右指针扩展窗口,左指针收缩,保持窗口内满足条件的最大区间。

3) 包含所有字符的最小窗口

  • 题目:给定一个字符串 S 和一个目标字符串 T,找出最小的窗口子串,包含目标字符串 T 的所有字符。
  • 解法:使用两个指针来形成窗口,动态调整窗口的大小,并通过哈希表来记录窗口内字符的出现情况。

4) 重复字符或子数组

  • 题目:给定一个字符串或数组,求不包含重复字符的最长子串或子数组。
  • 解法:使用滑动窗口(动态窗口)和哈希表记录每个字符的最后出现位置,通过右指针扩展窗口,左指针根据重复字符的出现位置调整。

5. 常见的滑动窗口解法模板

动态窗口大小

def slidingWindow(nums):
    left = 0
    result = float('inf')  # 或其他合适的初始值
    for right in range(len(nums)):
        # 窗口扩展:操作 right,加入新的元素
        while condition:  # 满足题意条件时
            # 更新窗口内的结果
            result = min(result, current_window_value)
            # 窗口收缩:操作 left,移除不再符合条件的元素
            left += 1
    return result

固定窗口大小

def slidingWindow(nums):
    window_sum = 0
    for i in range(len(nums)):
        window_sum += nums[i]  # 加入当前元素到窗口
        if i >= window_size - 1:
            # 当前窗口的大小达到要求,进行处理
            # 这里可以做计算,更新结果
            window_sum -= nums[i - window_size + 1]  # 移出窗口最左边的元素
    return result

6. 经典题目与解法

  1. 最小覆盖子串(Minimum Window Substring)

    • 给定字符串 S 和目标字符串 T,找出最小的窗口,包含 T 中的所有字符。
    • 解法:滑动窗口+哈希表记录字符频率。
  2. 最大连续子数组和(Maximum Subarray Sum)

    • 给定一个整数数组,找到一个具有最大和的连续子数组。
    • 解法:动态调整窗口大小(滑动窗口)并计算窗口内的和。
  3. 无重复字符的最长子串(Longest Substring Without Repeating Characters)

    • 给定一个字符串,找出最长的不包含重复字符的子字符串。
    • 解法:滑动窗口+哈希表。
  4. 子数组和为 k(Subarray Sum Equals K)

    • 给定一个整数数组,找出其和为 k 的子数组的数量。
    • 解法:使用滑动窗口和前缀和。

7. 时间复杂度

滑动窗口的最大优势是它可以将复杂的暴力算法优化到 O(n) 时间复杂度。通常情况下,窗口内的元素总共会被遍历一次(每个元素最多被访问两次,左指针和右指针各一次)。因此,滑动窗口算法的时间复杂度通常是 O(n),其中 n 是数组或字符串的长度。

例题

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

问题描述:
给定一个字符串 s,找出其中不含有重复字符的最长子串的长度。

示例:

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

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

  3. 输入: s = "pwwkew"
    输出: 3
    解释: 无重复字符的最长子串是 "wke",所以其长度为 3
    注意: "pwke" 是子序列而非子串,因此不计入。

提示:

  • 0 <= s.length <= 5 * 10^4
  • s 由英文字母、数字、符号和空格组成

题解
思路就是最长的无重复字符串前部分是一个相对短的无重复字符串,我们通过一个可以变大的滑动窗口,首先通过滑动窗口包含小的无重复字符串,然后在针对字符串遍历的过程中,不断扩充滑动窗口,直到滑动窗口的长度等于最大的无重复字符串。具体操作就是遍历到一个新的字符,就查看这个字符是否有重复字符在窗口中,然后动态调整窗口大小。如果包含(用hashmap求得),则把左边的指针变为上一个重复字符的位置+1。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int Longest = 0;
        int left = 0;
        int right = 0;
        char[] sArray = s.toCharArray();
        Map<Character,Integer> map = new HashMap<>();
        while(right<s.length()){
            if(map.containsKey(sArray[right])&&map.get(sArray[right])>=left){
                left = map.get(sArray[right]);//忘了加1
            }else{
                map.put(sArray[right],right);//不应该在else中,因为不管是不是包含重复,都要更新每个字符的最大位置所在。
            }
            right++;
            Longest= Math.max(Longest,right-left);
        }
        return  Longest;
    }
}

修改上面错误代码后。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int Longest = 0;
        int left = 0;
        int right = 0;
        char[] sArray = s.toCharArray();
        Map<Character,Integer> map = new HashMap<>();
        while(right<s.length()){
            if(map.containsKey(sArray[right])&&map.get(sArray[right])>=left){
                left = map.get(sArray[right])+1;
            }
            map.put(sArray[right],right);
            right++;
            Longest= Math.max(Longest,right-left);
        }
        return  Longest;
    }
}

209. 长度最小的子数组 中等

问题描述:
给定一个含有 n 个正整数的数组 nums 和一个正整数 target,找出该数组中满足其总和大于等于 target 的长度最小的子数组,并返回其长度。如果不存在符合条件的子数组,返回 0

示例:

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

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

  3. 输入: target = 11, nums = [1, 1, 1, 1, 1, 1, 1, 1]
    输出: 0

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^4

进阶:

  • 如果你已经实现了 O(n) 时间复杂度的解法,请尝试设计一个 O(n log n) 时间复杂度的解法。

题解
思路就是应用滑动窗口,遍历数组,每次添加新的一个数字,当找到了满足大于target的子数组后,我们可以从滑动窗口的前面部分移除适当的数字,来使其仍满足大于target,但是减少了长度。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int right = 0;
        int sum = 0;
        int shortest = Integer.MAX_VALUE;
        while(right < nums.length){
            sum+= nums[right++];
            if(sum>=target){
                while(true){
                    sum-=nums[left++];
                    if(sum<target){
                        shortest = Math.min(shortest,right-left+1);
                        break;
                    }
                }
            }
        }
        return  shortest == Integer.MAX_VALUE? 0:shortest;//注意返回值,来处理数组中所有的数值加起来都达不到target的情况。
    }
}

滑动窗口和双指针的关系

滑动窗口(Sliding Window)和双指针(Two Pointers)有着密切的关系,它们在许多算法问题中是相互交替使用的技巧。二者的核心思想都是通过两个指针来扫描数据结构,动态调整指针位置来满足特定条件。下面是它们的关系和区别:

1. 双指针(Two Pointers)

  • 定义:双指针是指在一个数据结构(通常是数组或字符串)中使用两个指针来进行遍历操作。这两个指针可以是从两端向中间移动(对撞指针),也可以是从同一端一起向前推进(如快慢指针)。
  • 典型应用:两数之和(Two Sum),判断回文串,合并有序数组等。

2. 滑动窗口(Sliding Window)

  • 定义:滑动窗口是一种特殊的双指针技巧,通常用于查找子数组或子串。它通过动态调整窗口的大小(即指针的位置)来满足特定条件。滑动窗口可以是固定大小,也可以是动态调整的。
  • 典型应用:无重复字符的最长子串,长度最小的子数组等。

3. 滑动窗口和双指针的关系

  • 本质上都是双指针技巧:滑动窗口是双指针技术的一种具体应用。当我们在滑动窗口问题中使用“左指针”和“右指针”来表示窗口的边界时,实际上就是在应用双指针技巧。

  • 滑动窗口是双指针的一种特殊情况:双指针可以用于其他算法(例如:合并有序数组),而滑动窗口则通常用于查找符合某种条件的子数组或子串。滑动窗口中的两个指针常常用来维护一个满足条件的区间或窗口。

4. 两者的相似性与区别

  • 相似性

    • 都是通过维护两个指针来扫描整个数组(或字符串),并通过调整指针的位置来优化算法。
    • 都可以通过一遍遍历来解决问题(通常是 O(n) 时间复杂度)。
  • 区别

    • 滑动窗口:指针移动时,窗口的大小会变化,可以扩展也可以收缩,通常用于动态变化的区间。
    • 双指针:不仅限于窗口大小变化,有时指针之间的距离不变(如快慢指针问题),而是通过指针间的相对位置来解决问题。

5. 示例对比

双指针:

  • 问题:两数之和(Two Sum II)
  • 解法:使用两个指针,分别指向数组的左右两端,计算当前和并根据和与目标值的大小调整指针。
def twoSum(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return [left, right]
        elif total < target:
            left += 1
        else:
            right -= 1
    return []

滑动窗口:

  • 问题:无重复字符的最长子串
  • 解法:使用两个指针维护一个窗口,左指针指向窗口的开始,右指针扩展窗口,保持窗口内没有重复字符。
def lengthOfLongestSubstring(s):
    left = 0
    max_len = 0
    seen = {}
    for right in range(len(s)):
        if s[right] in seen and seen[s[right]] >= left:
            left = seen[s[right]] + 1
        seen[s[right]] = right
        max_len = max(max_len, right - left + 1)
    return max_len

6. 总结

  • 滑动窗口可以看作是一种特殊的双指针技术,主要用于动态维护子数组或子串的区间。
  • 双指针技术更为通用,可以用于很多不同类型的问题,而滑动窗口更专注于处理那些涉及到区间长度、最值、满足某些条件的子数组的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值