3. 无重复字符的最长子串
class Solution {
int DupFromEnd(string &S, int Begin, int End) {
char Back = S[End - 1];
for (int i = End - 2; i >= Begin; --i) {
if (S[i] == Back)
return i;
}
return -1;
}
public:
int lengthOfLongestSubstring(string s) {
int Longest = 0;
int Left = 0, Right = 0;
while (Right < s.length()) {
// Fetch
int DupIdx = DupFromEnd(s, Left, Right + 1);
// Move
if (DupIdx == -1)
++Right;
else
Left = DupIdx + 1;
Longest = std::max(Longest, Right - Left);
}
return Longest;
}
};
我的题解时间复杂度好像是要高一点,从遍历的次数能看出来,最坏情况是没有一个重复字符,达到了 \(\mathrm O\left(n^2\right)\),但是平均好像还是接近 \(\mathrm O(n)\)?挺有意思的。但是其实还是官解这个方法更好一些:
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 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
看到「重复」其实就该想到 unordered_set 了。其实这种解法算是很有意思的,因为你会发现用了 unordered_set 和双指针之后,重复字符的位置 DupIdx 其实是不重要的——让左指针一直往右跑就好了,跑到 occ.count(s[rk + 1]) 为假(也即任意位置上的重复字符被去除)之后,留下的自然就是没有重复的极大子串。
二刷的时候用官解的思路重新做一遍。
二刷
「滑动窗口」不叫「跳跃窗口」是有原因的。我第一次解的时候写了一个用 unordered_map 的错误写法:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int Longest = 0;
int Begin = 0;
unordered_map<char, int> CharIdx;
for (int i = 0; i < s.size(); ++i) {
if (auto I = CharIdx.find(s[i]); I != CharIdx.end())
Begin = I->second + 1;
CharIdx[s[i]] = i;
Longest = max(Longest, i - Begin + 1);
}
return Longest;
}
};
事实上这个解法的问题就在于窗口不是滑动而是直接根据 it->second 进行跳跃,所以丢失了很多信息。真正的「滑动窗口」应该是逐个元素收窄的,如下。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int Longest = 0;
int l = 0;
unordered_set<char> Charset;
for (int i = 0; i < s.size(); ++i) {
// If charset overlaps, erase from the left ONE BY ONE.
while (Charset.count(s[i]))
Charset.erase(s[l++]);
Charset.insert(s[i]);
Longest = max(Longest, i - l + 1);
}
return Longest;
}
};
评论区给出了一种滑动窗口模板,非常好,很受用。
//外层循环扩展右边界,内层循环扩展左边界
for (int l = 0, r = 0 ; r < n ; r++) {
//当前考虑的元素
while (l <= r && check()) {//区间[left,right]不符合题意
//扩展左边界
}
//区间[left,right]符合题意,统计相关信息
}
三刷
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int Result = 0;
int Left = 0;
unordered_set<char> Set;
for (int i = 0; i < s.length(); ++i) {
while (Set.contains(s[i]))
Set.erase(s[Left++]);
Set.insert(s[i]);
Result = max(Result, i - Left + 1);
}
return Result;
}
};
我操,我要给自己一巴掌,怎么做题之前没看分类。以后记住这题是滑动窗口。然后说实话这个区间左端点还是写成小写字母l吧,写成Left我他妈还以为是左元素迭代器,直接一个Set.erase(Left++);起手,这谁能绷住。