【唐叔学算法】第五天:滑动窗口算法-轻松解决数组与字符串的子区间问题

37 阅读5分钟

大家好,我是唐叔,今天我们要一起学习一个在处理数组和字符串问题时非常高效的算法——滑动窗口算法。滑动窗口算法不仅能够帮助我们高效地解决一系列问题,还能让我们在处理复杂逻辑时更加得心应手。希望通过今天的分享,能够让各位读者朋友对滑动窗口算法有一个全面而深刻的理解。

滑动窗口算法概述

定义

滑动窗口是一种常用的算法技巧,主要用于处理数组或字符串的子区间问题。它通过维护一个窗口(即一个子区间),并在数组或字符串上滑动,来高效地找到满足条件的子区间。

应用场景

滑动窗口广泛应用于以下场景:

  • 「寻找最小子串」:例如,给定两个字符串 st,找到 s 中包含 t 所有字符的最小子串。
  • 「计算子数组的和」:例如,找到一个数组中和大于等于某个目标值的最短子数组。
  • 「统计子区间数量」:例如,统计一个数组中满足某种条件的子区间的数量。
  • 「连续子字符串问题」:例如,判断字符串中是否存在和为特定值的连续子字符串。

实现步骤

使用滑动窗口解决问题的一般步骤如下:

  1. 「初始化窗口」:定义两个指针 leftright,分别表示窗口的左边界和右边界。
  2. 「扩展窗口」:移动右指针 right,扩大窗口,直到窗口内的元素满足某种条件。
  3. 「收缩窗口」:移动左指针 left,缩小窗口,直到窗口内的元素不再满足某种条件。
  4. 「记录结果」:在每次窗口调整后,记录当前窗口的信息,如最长/最短子区间、满足条件的子区间数量等。
  5. 「重复步骤2-4」,直到右指针 right 达到数组或字符串的末尾。

注意事项

  • 「边界条件」:处理边界条件时要特别小心,确保不会空窗口或窗口越界。
  • 「窗口大小」:根据具体问题的需求,窗口大小可能是固定的,也可能是动态变化的。
  • 「窗口的滑动方向」:可以是左移或右移,也可以是双向滑动。
  • 「条件判断」:确保窗口的扩展和收缩逻辑正确,即条件判断的逻辑正确,避免遗漏或冗余。

LeetCode实战

入门题:3. 无重复字符的最长子串

题目链接:3. 无重复字符的最长子串

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

解题思路

这个问题非常适合用滑动窗口来解决。我们维护一个窗口 [left, right),并通过一个哈希表来记录窗口内字符的出现次数。当窗口内出现重复字符时,移动左指针 left,直到窗口内没有重复字符。

Java代码实现

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> charIndexMap = new HashMap<>(); // 记录字符及其最近出现的位置
        int left = 0, maxLength = 0;

        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            if (charIndexMap.containsKey(c)) {
                left = Math.max(charIndexMap.get(c) + 1, left);
            }
            charIndexMap.put(c, right);
            maxLength = Math.max(maxLength, right - left + 1);
        }

        return maxLength;
    }
}

中等题:76. 最小覆盖子串

题目链接:76. 最小覆盖子串

题目描述:给定两个字符串 st,返回 s 中包含 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

解题思路

这个问题同样可以用滑动窗口来解决。我们维护一个窗口 [left, right),并通过两个哈希表来记录 t 中字符的出现次数和窗口内字符的出现次数。当窗口内包含 t 中所有字符时,尝试收缩窗口,更新最小覆盖子串。

Java代码实现

import java.util.HashMap;
import java.util.Map;

public class Solution {
    public String minWindow(String s, String t) {
        Map<Character, Integer> need = new HashMap<>(); // 记录 t 中字符及其出现次数
        Map<Character, Integer> window = new HashMap<>(); // 记录窗口内字符及其出现次数

        for (char c : t.toCharArray()) {
            need.put(c, need.getOrDefault(c, 0) + 1);
        }

        int left = 0, right = 0;
        int valid = 0// 记录窗口内满足条件的字符数
        int start = 0, len = Integer.MAX_VALUE;

        while (right < s.length()) {
            char c = s.charAt(right);
            right++;

            if (need.containsKey(c)) {
                window.put(c, window.getOrDefault(c, 0) + 1);
                if (window.get(c).equals(need.get(c))) {
                    valid++;
                }
            }

            // 当窗口内包含 t 中所有字符时,尝试收缩窗口
            while (valid == need.size()) {
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }

                char d = s.charAt(left);
                left++;

                if (need.containsKey(d)) {
                    if (window.get(d).equals(need.get(d))) {
                        valid--;
                    }
                    window.put(d, window.get(d) - 1);
                }
            }
        }

        return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
    }
}

更多LeetCode题目推荐

结语

通过本文的学习,相信大家对滑动窗口算法有了更深入的理解。滑动窗口算法虽然强大,但在实际应用中也需要谨慎处理,特别是要注意边界条件的设定和窗口大小的控制。希望各位读者朋友能够在实践中灵活运用滑动窗口算法,解决更多的编程问题。如果有任何疑问或建议,欢迎在评论区留言交流!让我们一起享受编程的乐趣,不断探索和学习!


如果喜欢这篇文章,别忘了点赞和分享哦!我是唐叔,我们下次再见!😊