3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
cpp
#include <iostream>
#include <string>
#include <vector>
#include <string>
#include <unordered_set>
using namespace std;
// 求字串 中的 最长连续字串
int lengthOfLongestSubstring(string s) {
/*unordered_set(哈希集合):平均 O(1) 查找,更快
set(红黑树集合):O(log n) 查找,保持元素有序*/
unordered_set<char> strSet;
int n = s.size();
int maxSize = 0;
//右指针 j 标记子串的结束位置
int j = -1;
//每次循环代表以 i 作为新的左边界开始寻找无重复子串
//左指针 i 标记子串的起始位置
for (int i = 0; i < n; i++)
{
//移动左指针(收缩窗口
//当左指针向右移动时,将离开窗口的字符从集合中移除
if (i != 0) {
strSet.erase(s[i - 1]);
}
// 查找以当前i为边界 能找到的非重复最大字串
while (j + 1 < n && !strSet.count(s[j + 1])) {
strSet.insert(s[j + 1]);
j++;
}
maxSize = max(maxSize, j - i + 1);
}
return maxSize;
}
测试用例
cpp
int main() {
// 测试用例
struct TestCase {
string input;
int expected;
};
TestCase testCases[] = {
// 基础测试
{"abcabcbb", 3}, // "abc"
{"bbbbb", 1}, // "b"
{"pwwkew", 3}, // "wke"
// 边界情况
{"", 0}, // 空字符串
{"a", 1}, // 单字符
{"abcdef", 6}, // 无重复字符
{"abba", 2}, // 重复字符在两端
// 复杂情况
{"dvdf", 3}, // 重复字符在中间
{"anviaj", 5}, // "nviaj"
{"tmmzuxt", 5}, // "mzuxt"
{"abcdeafgh", 8}, // "bcdeafgh"
// 包含空格
{"a b c", 3}, // 包含空格
{" ", 1}, // 多个空格
// 混合字符
{"aA123@!", 7}, // 大小写字母、数字、符号混合
{"abc123abc", 6}, // "123abc"
};
int passed = 0;
int total = sizeof(testCases) / sizeof(testCases[0]);
for (int i = 0; i < total; i++) {
int result = lengthOfLongestSubstring(testCases[i].input);
if (result == testCases[i].expected) {
cout << "✓ Test case " << i + 1 << " passed: \""
<< testCases[i].input << "\" -> " << result << endl;
passed++;
}
else {
cout << "✗ Test case " << i + 1 << " failed: \""
<< testCases[i].input << "\"" << endl;
cout << " Expected: " << testCases[i].expected
<< ", Got: " << result << endl;
}
}
cout << "\n" << passed << "/" << total << " test cases passed." << endl;
return 0;
}
以字符串 "abcabcbb" 为例:
初始:i=0, rk=-1, occ={}第1轮:i=0 → rk移动到2 → "abc" → 长度=3
第2轮:i=1 → 移除'a' → rk移动到3 → "bca" → 长度=3
第3轮:i=2 → 移除'b' → rk移动到4 → "cab" → 长度=3
第4轮:i=3 → 移除'c' → rk移动到5 → "abc" → 长度=3
第5轮:i=4 → 移除'a' → rk无法移动(遇到'b'重复) → "bc" → 长度=2
...
最终结果:3
为什么用 unordered_set 而不是 set?
unordered_set(哈希集合):平均 O(1) 查找,更快
set(红黑树集合):O(log n) 查找,保持元素有序
在这个算法中:
我们只需要快速判断字符是否存在,不需要有序性
因此使用 unordered_set 性能更好
unordered_set
unordered_set occ;
// 插入元素occ.insert('a');
// 检查元素是否存在if (occ.count('a')) {
cout << "'a' 存在" << endl;
}
// 移除元素occ.erase('a');
// 清空集合occ.clear();
// 获取集合大小int size = occ.size();
// 遍历集合(顺序不确定)for (char c : occ) {
cout << c << " ";
}
为什么只移动一个?
cpp
for (int i = 0; i < n; ++i) {
if (i != 0) {
occ.erase(s[i - 1]); // 关键:i从0到1,移除s[0]; i从1到2,移除s[1]...
}
// ... 扩展右边界
}
查找的是以当前i为边界 能找到的非重复最大字串
外层 for 循环的 i 就是左指针
每次循环 i++,左指针就自动向右移动一位
occ.erase(s[i - 1]) 正是移除刚刚离开窗口的左边界字符
假设字符串 s = "abcde",当前窗口是 "bcd":

为什么不多移动几个?
因为左指针每次只移动一位是最优的:
如果窗口中有重复字符,一定是右边界遇到了重复
此时需要收缩左边界直到重复字符被移除
每次移一位就能保证找到第一个无重复的窗口起点
字符串: "abcabcbb" 过程:
i=0: 窗口扩张到 "abc" (rk=2)
occ = {'a','b','c'},
ans=3
i=1: 移除 s[0]='a', occ={'b','c'}右边界可扩展:
s[3]='a'不在occ中 → 加入
occ={'b','c','a'},
窗口="bca" (rk=3),
ans=3
i=2: 移除 s[1]='b', occ={'c','a'}右边界可扩展:
s[4]='b'不在occ中 → 加入
occ={'c','a','b'},
窗口="cab" (rk=4),
ans=3
i=3: 移除 s[2]='c', occ={'a','b'}右边界可扩展:
s[5]='c'不在occ中 → 加入
occ={'a','b','c'},
窗口="abc" (rk=5),
ans=3
i=4: 移除 s[3]='a', occ={'b','c'}右边界 s[6]='b'
已在occ中 → 停止扩展
窗口="bc" (rk=5),
ans=3
为什么先移除再扩展?
假设字符串 s = "abca",
当前状态:
第1轮结束:i=0, rk=2, 窗口="abc", occ={'a','b','c'}
现在进入第2轮(i=1): 情况A:先移除再扩展(正确)
- 移除 s[0]='a' → occ={'b','c'}
- 尝试扩展:s[3]='a'不在occ中 → 可以加入
- 结果:窗口="bca", rk=3, 长度=3
情况B:先扩展再移除(错误)
- 尝试扩展:s[3]='a'在occ中(因为occ={'a','b','c'})→ 无法扩展
- 移除 s[0]='a' → occ={'b','c'}
- 结果:窗口="bc", rk=2, 长度=2 ← 错失了更长的子串!