【算法】滑动窗口总结

232 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

一、前言

滑动窗口算法的思路: 维护一个窗口,不断滑动,更新答案。

滑动窗口-2022-08-0800-32-07.png

// 大致逻辑如下:
int left = 0, right = 0;
​
while (right < s.size()) {
    // 1. 增大窗口(右边)
    window.add(s[right]);
    ++right;
    
    // 2. 缩小窗口(左边)
    while (window needs shrink) {
        window.remove(s[left]);
        ++left;
    }
}

需要思考 4 个问题:

  1. 移动 right 扩大窗口时,需要更新什么?
  2. 什么时候开始移动 left 缩小窗口?
  3. 移动 left 缩小窗口时,需要更新什么?
  4. 结果是在扩大窗口时更新,还是在缩小窗口时更新?

二、题目

(1)最长无重复子串(中)

LeetCode 3

题干分析

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

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

思路解法

思路:滑动窗口

  • 定义 leftright 滑动窗口的左右两边。
  • 定义 counts[256] 数组: 记录字符出现的次数。

    • 如果 counts[c] > 1,说明当前窗口中存在重复字符,就需要移动 left 缩小窗口了。

滑动窗口-2022-08-0800-32-08.png

// Time: O(n), Space: O(m), m 是字符集大小, Faster: 92.19%
public int lengthOfLongestSubstring(String s) {
​
    int [] counts = new int[256]; // 记录字符出现的次数
    int left = 0, right = 0;      // 定义滑动窗口左右指针
    int ans = 0;                  // 记录结果
​
    while (right < s.length()) {
        char c = s.charAt(right);
        ++right;     // 右指针右移动 +1
        ++counts[c]; // 更新窗口内数据
        // 判断左侧窗口是否要收缩
        while (counts[c] > 1) {
            char d = s.charAt(left);
            ++left;
            // 更新窗口内数据
            --counts[d];
        }
        // 更新最大值
        ans = Math.max(ans, right - left);
    }
    return ans;
}

优化:滑动窗口,避免不必要判断

  • 当遇到重复字符时: 左侧窗口 left 不再一个个移动,而是直接移动到重复字符的下一位。
  • index[256] 数组: 记录字符当前最大小标。

滑动窗口-2022-08-0801-33-23.png

// Time: O(n), Space: O(m),m 是字符集大小, Faster: 92.19%
public int lengthOfLongestSubstring1N(String s) {
    int[] index = new int[256];
    Arrays.fill(index, -1);      // 初始,下标均为 -1
    int maxLen = 0;
    for (int left = 0, right = 0; right < s.length(); ++right) {
        left = Math.max(index[s.charAt(right)] + 1, left);  // 左侧边下标
        maxLen = Math.max(maxLen, right - left + 1);        // 比较最大值
        index[s.charAt(right)] = right;                     // 更新下标
    }
    return maxLen;
}

(2)找到字符串中所有字母异位词(中)

LeetCode 438

题干分析

这个题目说的是,给你字符串 s 和 p,你要在 s 中找到所有 p 的变位词,并返回它们的开始下标。变位词指的是使用相同字母以不同顺序构成的单词。在这个题目中,字符串 s 和 p 都只由小写字母组成,并且长度不会超过 100。

# 比如说,给你的字符串 s 和 p 是:
s: bcbababac
p: abc
​
# 在字符串 s 中,abc 的变位词有 cba 和 bac,它们的组成字符都是 a/b/c,只是排列顺序不一样。# 我们要返回这两个变位词的开始下标。第一个变位词的开始下标是 1,第二个变位词的开始下标是 6,因此返回:
[1,6]

思路解法

首先想到滑动窗口(固定大小) :即滑动窗口大小已固定

  • 初始化滑动窗口: 初始化滑动窗口后,需判断此窗口是否满足题意
  • 移动滑动窗口: 以固定窗口大小,不断移动

滑动窗口-2022-08-0809-17-00.png

// Time: O(n * n), Space: O(n), Faster: 86.62%
public List<Integer> findAnagrams(String s, String p) {
    if (s == null || p == null || s.length() < p.length()) return Collections.emptyList();
    int sLen = s.length(), pLen = p.length();
    char[] pc = new char[26];
    char[] sc = new char[26];
    // 1. 初始化滑动窗口
    for (int i = 0; i < pLen; ++i) {
        pc[p.charAt(i) - 'a']++;
        sc[s.charAt(i) - 'a']++;
    }
    List<Integer> result = new ArrayList<>();
    // 1.2. 判断初始化滑动窗口 是否 满足题意
    if (Arrays.equals(sc, pc)) result.add(0);
    // 2. 滑动窗口不断移动
    for (int i = pLen; i < sLen; ++i) {
        sc[s.charAt(i) - 'a']++;        // 右侧边扩
        sc[s.charAt(i - pLen) - 'a']--; // 左侧边进
        if (Arrays.equals(sc, pc)) result.add(i - pLen + 1); // 判断是否满足题意
    }
    return result;
}

这里有个优化方案: 先统计字符数量。

// Time: O(n), Space: O(k), Faster: 94.93%
public List<Integer> findAnagramsOn(String s, String p) {
    List<Integer> result = new ArrayList<>();
    if (s == null || p == null || s.length() < p.length()) return result;
    int sLen = s.length(), pLen = p.length();
    char[] pc = new char[26];
    for (int i = 0; i < pLen; ++i) {
        pc[p.charAt(i) - 'a']++;
    }
​
    int left = 0, right = 0;
    while (right < sLen) {
        if (pc[s.charAt(right) - 'a'] > 0) {
            pc[s.charAt(right) - 'a']--;
            ++right;
        } else {
            pc[s.charAt(left) - 'a']++;
            ++left;
        }
        if (right - left == pLen) result.add(left);
    }
    return result;
}