📝 【3. 无重复字符的最长子串】
📌 题目链接:leetcode.cn/problems/longest-substring-without-repeating-characters/
🔍 难度:中等 | 🏷️ 标签:字符串、双指针、滑动窗口、哈希表
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(∣Σ∣),其中 Σ 是字符集大小,通常为 ASCII 字符集,即 O(128) ≈ O(1)
🧠 题目分析
给定一个字符串
s,请你找出其中不含有重复字符的 最长子串 的长度。
✅ 示例说明:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
注意 "pwke" 是一个子序列,不是子串。
❓ 关键点解析:
- 子串 vs 子序列:子串要求连续,子序列不要求。本题是「子串」!
- 目标:找最长的 连续且无重复字符 的片段。
- 暴力解法:枚举所有子串 → 时间复杂度 O(n³),不可接受。
- 优化方向:利用「滑动窗口」思想,将时间降至 O(n)。
🔍 核心算法及代码讲解
🎯 核心思想:滑动窗口(Sliding Window)
滑动窗口是一种在数组或字符串上进行高效搜索的技术,特别适用于“寻找满足条件的最短/最长连续子数组”类问题。
✅ 为什么用滑动窗口?
- 我们要找的是连续子串,且不能有重复字符;
- 当我们向右移动左边界时,右边界不会回退(单调性);
- 所以可以用两个指针维护一个“合法区间”,动态扩展与收缩。
🧩 窗口如何工作?
| 操作 | 动作 |
|---|---|
右指针 rk 向右 | 尝试加入新字符 |
| 若出现重复 | 左指针 i 向右移动,直到不再重复 |
| 记录当前窗口长度 | 更新最大值 |
🛠️ 数据结构选择:unordered_set<char> occ
- 快速判断字符是否已存在;
- 支持
insert,erase,count操作,均 O(1) 平均时间; - 不需要存储索引,只需知道是否存在即可。
💡 优化技巧:避免重复遍历
- 一旦某个字符重复,我们不需要从头开始,而是直接跳过前面的无效部分;
- 这正是滑动窗口的核心优势:只向前走,不回头!
🧱 解题思路(分步详解)
🚶♂️ 步骤一:初始化变量
unordered_set<char> occ; // 记录当前窗口内出现的字符
int rk = -1, ans = 0; // 右指针初始在-1,ans记录答案
✅
rk = -1表示尚未开始,相当于在字符串左侧边界外。
🔄 步骤二:枚举左指针 i
i从0开始,代表当前子串的起始位置;- 每次循环前,先移除
s[i-1](因为左指针右移了); - 这样保证
occ中始终是[i, rk]区间的字符集合。
🧠 类比:想象你在扫地,每往前一步,就扔掉左边的地砖。
➡️ 步骤三:扩展右指针 rk
while (rk + 1 < n && !occ.count(s[rk + 1])) {
occ.insert(s[rk + 1]);
++rk;
}
- 只要下一个字符不在集合中,就继续加入并右移;
- 一旦发现重复,停止扩展,进入下一步。
📌 注意:
rk + 1 < n防止越界。
📏 步骤四:更新答案
ans = max(ans, rk - i + 1);
- 当前窗口
[i, rk]是以i开始的最长无重复子串; - 长度为
rk - i + 1; - 更新全局最大值。
🔁 步骤五:循环结束,返回结果
i遍历完所有可能的起始点;- 最终
ans即为所求。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n) ✅ 虽然有嵌套 while,但 rk 和 i 都只会向前移动,总共最多走 n 步,因此总操作次数 ≤ 2n |
| 空间复杂度 | O(∣Σ∣) ✅ 哈希集合最多存储所有不同字符,ASCII 字符集为 128,可视为常数空间 |
| 适用场景 | 所有“最长/最短连续子串”、“固定/变长窗口”问题,如: • 最长重复子串 • 字符串中包含特定字符的最小窗口 • 数组中和为 k 的子数组 |
💻 代码实现(保留原模板,完整注释)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.erase(s[i - 1]);
}
// 不断地移动右指针,直到遇到重复字符
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 将新的字符加入集合
occ.insert(s[rk + 1]);
// 右指针右移
++rk;
}
// 当前窗口 [i, rk] 是以 i 开始的最长无重复子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例1
string s1 = "abcabcbb";
cout << "Input: \"" << s1 << "\" -> Output: " << sol.lengthOfLongestSubstring(s1) << endl; // 输出: 3
// 测试用例2
string s2 = "bbbbb";
cout << "Input: \"" << s2 << "\" -> Output: " << sol.lengthOfLongestSubstring(s2) << endl; // 输出: 1
// 测试用例3
string s3 = "pwwkew";
cout << "Input: \"" << s3 << "\" -> Output: " << sol.lengthOfLongestSubstring(s3) << endl; // 输出: 3
// 测试用例4:空字符串
string s4 = "";
cout << "Input: \"" << s4 << "\" -> Output: " << sol.lengthOfLongestSubstring(s4) << endl; // 输出: 0
// 测试用例5:单字符
string s5 = "a";
cout << "Input: \"" << s5 << "\" -> Output: " << sol.lengthOfLongestSubstring(s5) << endl; // 输出: 1
return 0;
}
🚀 面试拓展 & 优化建议
🔍 面试题常考变形:
-
返回最长子串本身(而非长度)
- 修改:记录
start和maxLen,最后截取子串; - 示例:
string longestSubstring = s.substr(start, maxLen);
- 修改:记录
-
支持 Unicode 字符(如中文)
- 使用
unordered_set<wchar_t>或unordered_map<int, int>映射码点; - 注意内存开销增加。
- 使用
-
允许重复字符,但限制重复次数
- 使用
unordered_map<char, int>记录频次; - 当某字符频次 > k 时,左指针右移。
- 使用
-
扩展到子数组问题
- 如:“和为 k 的最长子数组”、“包含至少 k 个不同字符的最短子串”等。
✅ 滑动窗口通用模板(面试必背)
int slidingWindow(vector<int>& nums) {
unordered_map<int, int> freq; // 频次统计
int left = 0, right = 0;
int result = 0;
while (right < nums.size()) {
// 扩展右边界
freq[nums[right]]++;
right++;
// 缩小左边界,保持条件成立
while (invalidCondition(freq)) {
freq[nums[left]]--;
if (freq[nums[left]] == 0) freq.erase(nums[left]);
left++;
}
// 更新答案
result = max(result, right - left);
}
return result;
}
📌 该模板可用于解决大多数滑动窗口问题,核心是:维护窗口合法性 + 动态调整边界
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第9题 —— 438.找到字符串中所有字母异位词(中等)
🔹 题目:给定一个字符串
s和一个非空字符串p,找出s中所有p的字母异位词的起始索引。🔹 核心思路:使用滑动窗口 + 哈希表统计字符频次,比较窗口内字符是否与
p完全一致。🔹 考点:滑动窗口、字符频次统计、双指针、字符串匹配。
🔹 难度:中等,是“模式匹配”类问题的经典应用,常用于文本处理系统设计。
💡 提示:不要暴力枚举所有子串!要用滑动窗口优化至 O(n)!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!