无重复字符的最长子串原题地址
方法一:滑动窗口
考虑用2个指针来维护子串,使得这条子串没有重复字符。
i和j表示下标,[i,j]表示子串,长度为j-i+1。我们可以用i遍历字符串的所有字符,对于每一个i,都尽可能地让j往右滑动,使得[i,j]为无重复字符的子串。此时,每一个i都有唯一对应的j,即j=r(i)。本题只需要求得j-i+1的最大值即可。
力扣的官方题解中给出了一个例子,可以很好地呈现这种思路。
j=r(i)这个函数是不减的。即对于任意,,,我们有恒成立。这个结论可以直观地想象到,显然[,]没有重复字符,那么[,]自然也没有重复字符,所以一定在的右边。
那么问题就转换为,如何找到每个i对应的j呢?我们只需要维护一个哈希表,把当前[i,j]的所有字符都存进去。当i向右移动时,就移除最左边的字符;当j向右移动时,就添加最右边的字符。这样,我们可以时刻知道最右边的字符是否在[i,j]的范围内出现过,若出现重复字符,则此时的[i,j]就是当前i对应的无重复字符的最长子串。
注意,j必须初始化为-1,这是因为下标为0的字符也要存储进哈希表中。
// 方法一:滑动窗口+哈希集合set
class Solution {
public:int lengthOfLongestSubstring(string s) {unordered_set<char> us;int ans = 0;// 滑动窗口// i表示左边界,j表示右边界,即[i,j]int j = -1; // 右指针移动时要插入,设置成-1会插入第一个字符int n = s.size();for (int i = 0; i < n; ++i){// 左指针向右移动一格,要去掉左边的一个元素if (i){us.erase(s[i - 1]);}// 右指针向右移动一格,如果该字符没有重复,就插入while (j + 1 < n && !us.count(s[j + 1])){++j;us.insert(s[j]);}// 窗口长度为j-i+1ans = max(ans, j - i + 1);}return ans;}
};
方法二:双指针,对方法一的优化
方法一是固定左指针,让右指针动,最坏情况下,2个指针都要遍历一遍字符串。
我们可以考虑把每个字符当前最后一次出现的下标也存下来,这样左指针就不用一步一步动了,可以直接跳跃到某个位置。
具体的操作如下:仍然用i和j表示区间端点的坐标,这次采用(i,j]来表示子串,子串长度为j-i。使用j遍历字符串,对于每一个j,先在哈希表中查找当前字符s[j]有没有出现过,如果没有出现过,则当前子串(i,j]是合法的;若出现过,则需要更新i到当前字符最后一次出现的位置(如abcb,当j指向第2个b,i指向a前面,此时的子串abcb非法,需要把i更新为指向第1个b,此时对应的子串cb合法)。最后记得把当前字符的位置存进去。
更新i的位置时,应取(i和当前字符最后一次出现的下标m)的较大值,而不是直接取m。这是因为,若m<i,则更新后i反而向左走了,当前子串可能会出现重复字符,变为非法子串。
// 方法二:优化方法一,滑动窗口+哈希集合map
class Solution {
public:int lengthOfLongestSubstring(string s) {// 存储元素及其最后一次出现的下标unordered_map<char, int> um;int ans = 0;int n = s.size();int i = -1;for (int j = 0; j < n; ++j){// 如果该字符出现过,且在区间范围内,需要更新左端点auto it = um.find(s[j]);if (it != um.end()){//i = it->second; // err, 反例:abbai = max(i, it->second);}// 此时为右端点当前最后一次出现um[s[j]] = j;// 无重复区间长度为j-ians = max(ans, j - i);}return ans;}
};