一、文章标题
LeetCode 003. 无重复字符的最长子串 - 滑动窗口与哈希表详解
二、文章内容
1. 题目概述
- 题目描述:给定一个字符串 s,请找出其中不含重复字符的最长子串的长度。子串需连续,且字符不可重复。
- 原题链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/
- 难度等级:Medium
- 相关标签:字符串、哈希表、滑动窗口、双指针
2. 文章目录
目录
- 题目概述
- 解题思路
- 算法详解
- 3.1 [解法一:暴力枚举 + 去重检查](#解法一:暴力枚举 + 去重检查)
- 3.2 [解法二:滑动窗口(HashMap 下标跳跃)](#解法二:滑动窗口(HashMap 下标跳跃))
- 解法对比
- 最优解推荐
3. 解题思路
- 问题分析:
- 输入:一个字符串 s(可包含字母、数字、符号等)。
- 输出:s 中不含重复字符的最长子串的长度(整数)。
- 约束:子串必须连续;字符一旦重复,需移动左边界重新保持无重复。
- 核心难点:
- 如何在扫描过程中高效检测并维护"无重复"的窗口。
- 当右侧遇到重复字符时,如何快速将左指针跳到正确位置,避免一个个挪动导致退化为 O(n^2)。
- 解题方向:
- 暴力:枚举所有起点,向右扩张直到出现重复,更新答案。
- 滑动窗口 + HashSet:维护一个无重复窗口,重复时左指针逐步右移并移除字符。
- 滑动窗口 + HashMap(最优):记录每个字符最近一次出现的位置,遇到重复时左指针一次性跳跃到重复字符的下一个位置。
4. 算法详解
解法一:暴力枚举 + 去重检查 {#解法一}
算法原理
- 基本思想:固定起点 i,使用一个集合记录已出现字符,从 i 向右扩张 j,遇到重复则停止,更新答案。
- 适用场景:数据量较小、用于直观理解问题结构与边界情况。
具体实现
- 步骤1:特殊情况处理,若 s 为空或长度为 0,返回 0。
- 步骤2:外层循环遍历起点 i,内层使用 HashSet 从 i 开始扩张 j,直到遇到重复。
- 步骤3:过程中维护最长长度 maxLen。
复杂度分析
- 时间复杂度:O(n^2),最坏情况下(如全不重复),每个起点都可能扫描到末尾。
- 空间复杂度:O(min(n, Σ)),Σ 为字符集大小,集合中最多存储当前子串的字符数。
Java代码实现
java
class Solution {
public int lengthOfLongestSubstring(String s) {
// 边界处理:空字符串或null,直接返回0
if (s == null || s.length() == 0) {
return 0;
}
int n = s.length();
int maxLen = 0;
// 外层固定起点 i
for (int i = 0; i < n; i++) {
java.util.HashSet<Character> seen = new java.util.HashSet<>();
// 从 i 向右扩张 j
for (int j = i; j < n; j++) {
char c = s.charAt(j);
if (seen.contains(c)) {
// 遇到重复字符,当前起点下的最长子串结束
break;
}
seen.add(c);
// 更新答案
int currLen = j - i + 1;
if (currLen > maxLen) {
maxLen = currLen;
}
}
}
return maxLen;
}
}
解法二:滑动窗口(HashMap 下标跳跃) {#解法二}
算法原理
- 基本思想:使用双指针 [left, right] 表示当前无重复窗口,HashMap 记录每个字符最近一次出现的下标。
- 当右指针读到重复字符 c 时,将 left 跳到 max(left, lastIndex[c] + 1),确保窗口内无重复。
- 适用场景:通用最优解,线性时间,思路清晰,易于面试手写。
具体实现
- 步骤1:特殊情况处理,s 为空或长度为 0 返回 0。
- 步骤2:维护 HashMap<Character, Integer> lastIndex,left 指向当前窗口左边界,遍历 right 从 0 到 n-1。
- 步骤3:若当前字符 c 在 map 中且上次位置 >= left,则更新 left 为上次位置 + 1;随后更新 map 中 c 的下标为 right,并维护答案。
复杂度分析
- 时间复杂度:O(n),每个字符至多被左右指针各访问/更新常数次。
- 空间复杂度:O(min(n, Σ)),Σ 为字符集大小,map 存储窗口中出现过的字符下标。
Java代码实现
java
import java.util.HashMap;
class Solution {
public int lengthOfLongestSubstring(String s) {
// 边界处理:若字符串为 null 或长度为 0,则没有有效子串,返回 0
if (s == null || s.length() == 0) {
return 0;
}
// lastIndex 用于记录每个字符最近一次出现的位置(下标)
HashMap<Character, Integer> lastIndex = new HashMap<>();
int left = 0; // 当前无重复窗口的左边界
int maxLen = 0; // 结果:最长无重复子串长度
// 使用 right 指针遍历字符串的每个字符
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果字符 c 出现过,并且它上一次出现位置在当前窗口内
if (lastIndex.containsKey(c)) {
int prev = lastIndex.get(c);
// 将 left 跳到重复字符的下一位,但不能左移(保持单调不回退)
if (prev + 1 > left) {
left = prev + 1;
}
}
// 更新当前字符的最新出现位置
lastIndex.put(c, right);
// 计算当前窗口长度并更新答案
int currLen = right - left + 1;
if (currLen > maxLen) {
maxLen = currLen;
}
}
return maxLen;
}
}
5. 解法对比 {#解法对比}
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
暴力枚举 + 去重检查 | O(n^2) | O(min(n, Σ)) | 思路直观、实现简单 | 性能较差,无法通过大数据量 | 入门理解、数据量小 |
滑动窗口(HashMap 下标跳跃) | O(n) | O(min(n, Σ)) | 线性时间、实现清晰、面试高频 | 需理解左指针"跳跃"细节 | 通用最优、面试推荐 |
6. 最优解推荐 {#最优解推荐}
- 最优解推荐:滑动窗口(HashMap 下标跳跃)。
- 原因:在保证 O(n) 时间复杂度的同时,代码可读性强,空间使用可控,适合在面试中快速、稳定地实现与讲解。
三、文章标签
算法,哈希表,滑动窗口,双指针,字符串,LeetCode,中等
四、文章简述
经典字符串题,利用滑动窗口 + 哈希表实现 O(n) 线性解,关键在于用下标映射实现左指针跳跃,避免逐步移动导致退化。适合准备面试与夯实基础的同学,代码注释详尽,便于快速掌握与复盘。