LeetCode 3 & 3090 题解:不定长滑动窗口——从“不重复“到“最多两次“,一个模板搞定频次约束问题

【算法】无重复字符的最长子串 & 每个字符最多出现两次的最长子字符串------不定长滑动窗口进阶

    • [3. 无重复字符的最长子串](#3. 无重复字符的最长子串)
      • [1. 题目链接](#1. 题目链接)
      • [2. 题目描述](#2. 题目描述)
      • [3. 题目示例](#3. 题目示例)
      • [4. 算法思路](#4. 算法思路)
        • 解法一:暴力枚举
        • [解法二:滑动窗口 + 哈希表(推荐)](#解法二:滑动窗口 + 哈希表(推荐))
      • [5. 核心代码](#5. 核心代码)
      • [6. 示例测试(总代码)](#6. 示例测试(总代码))
    • [3090. 每个字符最多出现两次的最长子字符串](#3090. 每个字符最多出现两次的最长子字符串)
      • [1. 题目链接](#1. 题目链接)
      • [2. 题目描述](#2. 题目描述)
      • [3. 题目示例](#3. 题目示例)
      • [4. 算法思路](#4. 算法思路)
      • [5. 核心代码](#5. 核心代码)
      • [6. 示例测试(总代码)](#6. 示例测试(总代码))
    • 总结

🎬 博主名称: 超级苦力怕

🔥 个人专栏: 《LeetCode 题解》

🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!


本篇文章讲解的是 LeetCode 第 3 题------无重复字符的最长子串 和 第 3090 题------每个字符最多出现两次的最长子字符串 。两道题同属 不定长滑动窗口 ,核心都是维护一个"满足字符频次约束"的窗口:第 3 题要求窗口内字符全部不重复(频次 ≤ 1),第 3090 题则放宽为每个字符最多出现两次(频次 ≤ 2)。

不定长滑动窗口是字符串问题的通用解法,时间复杂度 O(n),空间复杂度 O(字符集大小)。在实际业务中,它广泛应用于 日志时间窗口分析 (如"最近 5 分钟内最多允许 3 次失败登录")、网络流量控制 (TCP 拥塞窗口的动态调整)、基因序列匹配 (寻找满足特定碱基频次约束的最长片段)等场景。

本文将使用 Java 进行讲解,从暴力枚举逐步过渡到滑动窗口,帮助你掌握不定长滑窗的核心框架------右指针扩张 + 左指针收缩

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

1. 题目链接

直达链接:LeetCode 3

2. 题目描述

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

提示:

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

3. 题目示例

示例 1:

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

示例 2:

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

示例 3:

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

4. 算法思路

这道题是不定长滑动窗口的入门经典题。窗口内需要满足的约束是:所有字符互不相同(即每个字符的出现次数 ≤ 1)。

解法一:暴力枚举

算法思想:

  • 枚举所有可能的子串起点 i 和终点 j
  • 对每个子串,用哈希集合检查是否有重复字符
  • 若无重复,更新最大长度

复杂度分析:

  • 时间复杂度:O(n²),枚举所有子串 O(n²),每次判重 O(n),总计 O(n³),实际可通过提前 break 优化到 O(n²)
  • 空间复杂度:O(n),哈希集合存储子串字符
解法二:滑动窗口 + 哈希表(推荐)

核心框架:右指针扩张,左指针收缩。

维护一个可变长度的窗口 [left, right]

  1. 右指针 right 不断向右扩张 ,将 s[right] 加入窗口,更新频次表
  2. 当窗口内出现重复字符时 (即 s[right] 的频次 > 1),左指针 left 不断右移,同时减少对应字符的频次,直到重复消除
  3. 窗口重新合法后,用当前窗口长度 right - left + 1 更新答案

示例推演s = "abcabcbb"

复制代码
初始:left = 0, right = 0, cnt = {}, ans = 0

right=0 'a': cnt={a:1}, window="a",     合法, ans=1
right=1 'b': cnt={a:1,b:1}, window="ab",   合法, ans=2
right=2 'c': cnt={a:1,b:1,c:1}, window="abc", 合法, ans=3
right=3 'a': cnt={a:2,b:1,c:1} → 重复!
            left=0 移出'a': cnt={a:1,b:1,c:1}, left=1
            window="bca", 合法, ans=3
right=4 'b': cnt={a:1,b:2,c:1} → 重复!
            left=1 移出'b': cnt={a:1,b:1,c:1}, left=2
            window="cab", 合法, ans=3
right=5 'c': cnt={a:1,b:1,c:2} → 重复!
            left=2 移出'c': cnt={a:1,b:1,c:1}, left=3
            window="abc", 合法, ans=3
right=6 'b': cnt={a:1,b:2,c:1} → 重复!
            left=3 移出'a': cnt={a:0,b:2,c:1}, left=4 (a 被移除)
            仍重复!left=4 移出'b': cnt={b:1,c:1}, left=5
            window="cb", 合法, ans=3
right=7 'b': cnt={b:2,c:1} → 重复!
            left=5 移出'c': cnt={b:2}, left=6 (c 被移除)
            仍重复!left=6 移出'b': cnt={b:1}, left=7
            window="b", 合法, ans=3

最终答案:3

易错点: 收缩左指针时可能需要多次移动,要用 while 循环而非 if。例如 s = "abca" 中遇到第二个 'a' 时,移出最左边的 'a' 即可;但 s = "abcc" 中遇到第二个 'c' 时,需要连续移出 'a''b' 和第一个 'c'

复杂度分析:

  • 时间复杂度:O(n),每个字符最多被左右指针各访问一次
  • 空间复杂度:O(∣Σ∣),其中 ∣Σ∣ 为字符集大小

5. 核心代码

java 复制代码
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int[] cnt = new int[128];
        int left = 0, ans = 0;

        for (int right = 0; right < n; right++) {
            char c = s.charAt(right);
            cnt[c]++;

            while (cnt[c] > 1) {
                cnt[s.charAt(left)]--;
                left++;
            }

            ans = Math.max(ans, right - left + 1);
        }

        return ans;
    }
}

6. 示例测试(总代码)

java 复制代码
public class Main {
    public static void main(String[] args) {
        Solution sol = new Solution();

        // 示例1测试
        String s1 = "abcabcbb";
        System.out.println("示例1输出:" + sol.lengthOfLongestSubstring(s1)); // 预期输出3

        // 示例2测试
        String s2 = "bbbbb";
        System.out.println("示例2输出:" + sol.lengthOfLongestSubstring(s2)); // 预期输出1

        // 示例3测试
        String s3 = "pwwkew";
        System.out.println("示例3输出:" + sol.lengthOfLongestSubstring(s3)); // 预期输出3
    }
}

3090. 每个字符最多出现两次的最长子字符串

1. 题目链接

直达链接:LeetCode 3090

2. 题目描述

给你一个字符串 s,请返回满足每个字符最多出现两次的最长子字符串的长度。

提示:

  • 1 <= s.length <= 100
  • s 仅由小写英文字母组成

3. 题目示例

示例 1:

复制代码
输入:s = "bcbbbcba"
输出:4
解释:以下子字符串长度为 4,并且每个字符最多出现两次:
- "bcba"(b 出现 2 次,c 出现 1 次,a 出现 1 次)
- "cbbc"(c 出现 2 次,b 出现 2 次)
- "cbba"(c 出现 1 次,b 出现 2 次,a 出现 1 次)

示例 2:

复制代码
输入:s = "aaaa"
输出:2
解释:以下子字符串长度为 2,并且每个字符最多出现两次:
- "aa"
不存在更长的满足条件的子字符串。

4. 算法思路

这道题与第 3 题一脉相承,唯一的区别是约束条件放宽了------从"每个字符最多出现 1 次"变为"每个字符最多出现 2 次"。

滑动窗口框架完全一致,只需修改收缩条件:

  1. 右指针 right 不断向右扩张 ,将 s[right] 加入窗口,频次 +1
  2. s[right] 的频次 > 2 时,左指针不断右移,直到该字符频次降回 2
  3. 窗口合法后,用 right - left + 1 更新答案
解法一:暴力枚举

算法思想:

  • 枚举所有子串 [i, j],统计每个子串中各字符的频次
  • 若所有字符频次 ≤ 2,更新最大长度

复杂度分析:

  • 时间复杂度:O(n²),n 最大 100,完全可行
  • 空间复杂度:O(1),固定 26 个小写字母
解法二:滑动窗口(推荐)

由于 s 仅由小写字母组成,可以用 int[26] 数组代替哈希表,效率更高。

示例推演s = "bcbbbcba"

复制代码
初始:left = 0, ans = 0, cnt[26] = {}

right=0 'b': cnt[b]=1, window="b",       合法, ans=1
right=1 'c': cnt[b]=1,c=1, window="bc",     合法, ans=2
right=2 'b': cnt[b]=2,c=1, window="bcb",    合法, ans=3
right=3 'b': cnt[b]=3,c=1 → b 超限!
            移出 left=0 'b': cnt[b]=2,c=1, left=1
            window="cbb", 合法, ans=3
right=4 'b': cnt[b]=3,c=1 → b 超限!
            移出 left=1 'c': cnt[b]=3,c=0, left=2
            仍超限!移出 left=2 'b': cnt[b]=2,c=0, left=3
            window="bb", 合法, ans=3
right=5 'c': cnt[b]=2,c=1, window="bbc",    合法, ans=3
right=6 'b': cnt[b]=3,c=1 → b 超限!
            移出 left=3 'b': cnt[b]=2,c=1, left=4
            window="bcb", 合法, ans=3
right=7 'a': cnt[b]=2,c=1,a=1, window="bcba", 合法, ans=4

最终答案:4

复杂度分析:

  • 时间复杂度:O(n),每个字符最多被左右指针各访问一次
  • 空间复杂度:O(1),固定大小数组 int[26]

5. 核心代码

java 复制代码
class Solution {
    public int maximumLengthSubstring(String s) {
        int n = s.length();
        int[] cnt = new int[26];
        int left = 0, ans = 0;

        for (int right = 0; right < n; right++) {
            int idx = s.charAt(right) - 'a';
            cnt[idx]++;

            while (cnt[idx] > 2) {
                cnt[s.charAt(left) - 'a']--;
                left++;
            }

            ans = Math.max(ans, right - left + 1);
        }

        return ans;
    }
}

6. 示例测试(总代码)

java 复制代码
public class Main {
    public static void main(String[] args) {
        Solution sol = new Solution();

        // 示例1测试
        String s1 = "bcbbbcba";
        System.out.println("示例1输出:" + sol.maximumLengthSubstring(s1)); // 预期输出4

        // 示例2测试
        String s2 = "aaaa";
        System.out.println("示例2输出:" + sol.maximumLengthSubstring(s2)); // 预期输出2
    }
}

总结

第 3 题 vs 第 3090 题 对比

维度 3. 无重复字符的最长子串 3090. 每个字符最多出现两次的最长子字符串
难度 中等 简单
约束条件 每个字符频次 ≤ 1 每个字符频次 ≤ 2
字符集 英文字母、数字、符号、空格 仅小写英文字母
频次存储 int[128]HashMap int[26]
收缩条件 cnt[c] > 1 cnt[idx] > 2
核心技巧 不定长滑动窗口 + 频次约束 同第 3 题,仅阈值不同

不定长滑动窗口通用模板

两道题共享同一个算法框架,差异仅在于频次阈值的不同:

复制代码
右指针扩张 → 更新频次
    ↓
while (窗口不合法) {
    左指针收缩 → 更新频次
}
    ↓
更新答案

这个模板可以推广到任意"每个字符最多出现 k 次"的问题------只需将阈值从 1 或 2 改为 k 即可。

核心要点

  1. 滑动窗口的核心是单调性:当右指针固定时,左指针只会向右移动、不会回退,这保证了 O(n) 的时间复杂度
  2. 第 3 题中,while (cnt[c] > 1) 的循环条件保证了窗口内无重复字符;第 3090 题中改为 while (cnt[idx] > 2),约束放宽后窗口可以更长
  3. 第 3 题字符集包含符号和空格,使用 int[128] 覆盖 ASCII 全集;第 3090 题仅小写字母,使用 int[26] 更高效
  4. 收缩左指针时用 while 而非 if------因为可能需要连续移出多个字符才能恢复窗口合法性
  5. 两道题都只需一次遍历,时间复杂度 O(n),空间复杂度 O(1)(固定大小数组)
相关推荐
阿Y加油吧1 小时前
吃透 RAG 检索:纯向量短板、BM25 混合检索、RRF 融合与重排序
人工智能·leetcode
Overboom1 小时前
[BEV感知] --- IPM算法
数码相机·算法
qq_296553271 小时前
【LeetCode】最大子数组乘积:三种解法从暴力到最优
数据结构·算法·leetcode·职场和发展·动态规划·柔性数组
不知名的老吴1 小时前
关于C++中的placement new
数据结构·c++·算法
平行侠1 小时前
023Pollard-ρ 因子分解算法
数据结构·算法
谭欣辰1 小时前
C++倍增算法详解
数据结构·c++·算法
MATLAB代码顾问1 小时前
差分进化算法(DE)原理与Python实现
开发语言·python·算法
MicroTech20252 小时前
微算法科技(NASDAQ :MLGO)基于后量子密码学的动态BFT共识机制:QDBFT架构
科技·算法·密码学
Brilliantwxx2 小时前
【C++】认识 list(初步认识+模拟实现)
开发语言·数据结构·c++·笔记·算法·list