滑动窗口
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> c_set;
int len = 0;
// 指向字串首尾的
int beg = 0, end = 0;
int n = s.size();
for (; beg < n; ++beg){
if (beg != 0){
// 左指针向右移动一格
c_set.erase(s[beg-1]);
}
// 判断是否需要进行子串延长
while (end < n && !c_set.count(s[end])){
c_set.insert(s[end]);
++end;
}
len = max(len, int(end-beg));
}
return len;
}
};
但是上述代码的左指针每次向右移动一格,其实产生了更多的计算:
-
假设字符串为 abcbd,遍历子串(也就是c_set里的字符)依次是(带颜色子串):
-
abcbd
-
abcbd
-
abcbd ,此时的while循环中end=3,s[3]=b,b在子串中出现过,于是进入下一次for循环(beg=1),erase字串中第一个元素a,将左指针向右移动一位指向b;
-
但是,左指针向右移动一位指向 b 是多余的,因为刚才检查过 b 在子串中出现过,所以得到的字串(bc)必然是上一个字串的子集:
-
abcbd
-
abcbd
-
abcbd
-
所以此时将左指针直接移动到出现过的字符的下一个位置(比如上面出现过的字符为 b ,就应该将左指针移动到 b 的下一位 c)就可以省略掉子集的计算:
abcbd
abcbd
再作改进,需要记录下查找到的字符的位置,使用泛型算法find():
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<char> vec;
int len = 0;
auto rit = s.begin();
while(rit != s.end()){
// 子串存入vector,vector的size即为字串长度
while ( rit != s.end() && find(vec.begin(), vec.end(), *rit) == vec.end()){
vec.push_back(*rit);
++rit;
}
len = max(len, int(vec.size())); //当前子串长度
if (rit != s.end()){
//重复元素之前的元素都可以删除
vec.erase(vec.begin(), find(vec.begin(), vec.end(), *rit)+1);
}
}
return len;
}
};
但是查找的操作效率比较一般。
再进行优化(个人是最喜欢这个版本):
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.empty()) return 0;
int len = 0;
int l = 0, r = 0;//窗口的左右边界
while(r < s.size()){
//窗口中每新增一个元素s[r],都要查询之前的窗口区间中是否已经存在该元素
//遍历上一次的左边界到当前位置(之前的窗口区间),如果出现元素与当前位置元素重复,则需要更新左边界
for(int i = l; i < r; ++i){
if(s[i] == s[r]){
l = i+1;
break;
}
}
len = max(len, r-l+1);
++r;
}
return len;
}
};
更简洁版本:
数组大小128是ascii码的原因,char向int的转变;
i,j分别代表子串的左右边界(第一个和最后一个字符):没有重复字符时调整右边界(向右移一格),出现重复字符时调整左边界(移至重复字符的下一位),j - i + 1为字串长度。
假如此刻边界如下:
pwwkew
此时 i
→
\rightarrow
→ w, j
→
\rightarrow
→ k
- j 下一刻即将指向 e ,而且字串中不会出现字符与 e 重复,便移动右边界 j 指向 e :pwwkew
- j 再下一刻即将指向 w ,字串中的字符 w 会重复,应当调整左边界 i 指向字串中的字符 w 的下一个位置:pwwkew
数组中的元素是用来存储字符的位置的,并且会不断更新:
一开始:a[p] = 1, a[w] = 2;
当右边界移动到第二个字符 w 时, a[w] = 3,覆盖掉之前的值。
接下来:a[k] = 4, a[e] = 5 ;
最后的a[w] = 6也会覆盖之前的值。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int a[128] = {0};
int ans = 0;
int i = 0;
for (int j = 0; j < s.size(); j++) {
// a[s[j]]存储s[j]在s中的位置(从1开始):下标+1,(并且随着遍历遇到重复字符时会更新值)
// 不出现重复字符:i = max(i, a[s[j]]) = i,即不改变左边界
// 出现重复字符时:i= max(i, a[s[j]]) = a[s[j]],即将左边界指向重复字符的下一个位置
// 注意数组中维护的值是递增的,与下标有关
// 即根据左边界的值和将要处理的字符映射到数组中的值的大小关系,就可以知道要不要调整左边界
// 上述映射关系:字符作为数组下标,开局全初始化为0,如果该字符是第一次出现,那么映射的值必为0,也就是
// 该值肯定 <= i,反之如果该字符在正在处理的子串中出现过,那映射的值必然会 > i。
i = max(i, a[s[j]]);
a[s[j]] = j + 1;
ans = max(ans, j - i + 1);
}
return ans;
}
};
套滑动窗口的模板:
leetcode 第 76 题:最小覆盖子串(C++)_qq_32523711的博客-CSDN博客
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left= 0, right = 0;
int len = 0;
while(right < s.size()){
auto c = s[right];
++right;
++window[c];
while(window[c] > 1){
auto c = s[left];
++left;
--window[c];
}
len = max(len, right - left);
}
return len;
}
};