题目
Given a string, find the length of the longest substring without
repeating characters.Example 1:
Input: “abcabcbb” Output: 3 Explanation: The answer is “abc”, with
the length of 3. Example 2:Input: “bbbbb” Output: 1 Explanation: The answer is “b”, with the
length of 1. Example 3:Input: “pwwkew” Output: 3 Explanation: The answer is “wke”, with the
length of 3.
Note that the answer must be a substring, “pwke” is a subsequence and not a substring.
分析
这也是leetcode很经典的题目了,考察的点就是滑动窗口
解法一:(暴力)
暴力二重循环遍历这个字符串,时间复杂度O(n²),肯定是不用考虑的。先不说这种算法能不能在OJ过,面试问到肯定过不了。这里就提一下,不写代码了。
解法二:(滑动窗口)
这也是这篇文章的主题,滑动窗口这东西在计算机领域用的还是很广泛的,常见的就是tcp协议当中的滑动窗口。滑动窗口说白了就是用两个变量记录一下窗口的边界值,在这里就是字符串的下标值,那具体怎么用呢?下面就来分析一下这道题如何用滑动窗口解决。
对任意一个字符串,我们可以用维护滑动窗口的两个边界来保证边界之内的字符串都是没有重复的,那么边界值的差再加1就是字符串的长度。来看例子abcabcbb,这个窗口的最大值就是abc,当然了bca, cab, abc…都是,对每个位置,我们记录一下滑动窗口的最大值,那么遍历完字符串,就可以得到最长无重复子串的长度。这里的问题关键就是如何来动态维护窗口的边界值
简单分析一下,每当我们遇到一个新的字符,我们需要检验它是否出现过,那前面有若干个字符,我们如何来检验呢,肯定是需要保存一下已经遍历过的字符串,可以用set也可以用map,但是map更好,因为它还可以同时记录下之前出现的下标,帮助我们快速移动窗口。
还是那个例子: abcabcbb,当遍历完前三个字符的时候,我们应该记录下了abc的出现的下标,分别就是0,1,2。这个值的作用能帮助我们找到最近一次重复的位置,从而帮助我们确定窗口的左边界。回到刚才的过程,现在abc都是不重复的,所以窗口的左边界就是a,下标为0,右边界为c,下标为2,最大长度为3,接着遇到了a,发现是之前出现过的了,这时候窗口就需要收缩了,因为包含了重复元素,那怎么收缩呢,为了保证不遗漏元素,肯定是需要收缩左窗口,只需要把左窗口移动到第一个a的下一个位置,就又能保证窗口是不重复的,为什么能保证,刚才我们说过map记录了这个字符最近一次出现的位置,那么它再次出现的时候,只要左窗口移动到上次出现的下一个位置,就一定能保证窗口里不带重复的元素。这就是滑动窗口解决这个问题的方法。下面我们来写代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ans = 0;
if(s.empty()) return ans;
unordered_map<char, int>hash;
for (int i = 0, j = 0; i < s.size() && j < s.size(); ++j) {
//如果没有出现过 就记录最近一次它出现的位置
if (!hash.count(s[j])) {
hash[s[j]] = j;
}
//如果出现过,就把左边界移到它上次出现的位置的下一个位置,同时更新最新出现的位置
else {
//注意这个max
i = max(i, hash[s[j]] + 1);
hash[s[j]] = j;
}
ans = max(ans, j - i + 1);
}
return ans;
}
};
代码很容易理解,不过需要注意的一个点就是注释标注max的地方,为什么需要加一个max呢?因为我们的滑动窗口是只能单方向伸展的,或者说边界只能往下标更大的地方延展,原因就是边界更新是根据记录的上次出现的位置,而这个位置肯定是下标增大的趋势变化,如果这里不加上max比较,就可能出现左边界跑到更小的地方去了。
举个例子:abba,很显然最长不重复子串长度为2,当遍历到最后一个字符a的时候,此时i的位置在下标2,j的位置在下标3,由于a出现了重复,i要更新到上一次出现的a的下一个位置,这个位置是第一个b的下标1,如果不加max比较,此时i就从2变成了1,然后j-i+1就变成了3,显然是错误的,原因就是b比a先重复,导致左边界先跑到后面,从而后面a重复的时候,左边界跑到了前面去了。所以加上max比较就能保证这里不出错。
解法三:
其实这里所谓解法三,只是对解法二进一步优化。由于题目给的s是字符串,所以只需要考虑出现的字符种类(最多256),可以不需要用map而是开一个256大小的int数组,下标对应字符的ASCII值,对应数组的值表示它是否出现过,初始化数组为-1,表示都没有出现过,同样的思路,可以写出如下代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ans = 0;
if(s.empty()) return ans;
//unordered_map<int, int>hash;
vector<int> hash(256, -1);
for (int i = 0, j = 0; i < s.size() && j < s.size(); ++j) {
if (hash[static_cast<int>(s[j])] == -1) {
hash[static_cast<int>(s[j])] = j;
}
else {
i = max(i, hash[static_cast<int>(s[j])] + 1);
hash[static_cast<int>(s[j])] = j;
}
ans = max(ans, j - i + 1);
}
return ans;
}
};