给定一个字符串 s ,请找出其中不含有重复字符的 最长子串 的长度。
LeetCode链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters
滑动能够在O(n)的时间复杂度情况下解决这个问题,即扫描一遍就能找出答案,以下就具体分析一下为什么滑动窗口能做到这一点,以abcdecb为例。
就本题而言,双层遍历是第一能想到的最简单的解决方法。双层遍历的根据在于穷举出原字符串的所有子串,然后取长度最长度子串作为结果。
如abcdecb,它的子串构成如下
- 以第一个字符a为首的子串有abcdecb,abcdecb,abcdecb,abcdecb,abcdecb,abcdecb。
- 以第二个字符b为首的子串有abcdecb,abcdecb,abcdecb,abcdecb,abcdecb,abcdecb。
- ……
- 以最后一个字符b为首的子串有abcdecb。
用双层遍历为原理基础,用哈希表作为优化手段,则解决流程大致如下:
- 外层遍历从a开始,一直扫描到第二个c,abcdec,发现c是重复的,停止以a为首的子串探测,最长子串是abcde,长度最长为5。
- 外层遍历从b开始,一直扫描到第二个c,bcdec,发现c是重复的,停止以b为首的子串探测,本次探测过程中没有长度超过5的。
- 。。。外层遍历从最后一个b开始,发现本次扫描结束,双层遍历结束。
代码如下
int lengthOfLongestSubstring(string s)
{
int maxLength = 0;
for (decltype(s.length()) i = 0; i < s.length(); ++i)
{
unordered_map<char, int> charIndex;
for (auto j = i; j < s.length();)
{
auto ite = charIndex.find(s[j]);
if (ite == charIndex.end())
{//没找到重复字符
charIndex[s[j]] = j; //记录本字符出现的位置
j++;
if (j - i > maxLength)
maxLength = j - i;
}
else
{//找到重复字符,内层遍历结束
break;
}
}
}
return maxLength;
}
这个解决方案必须要双层遍历,复杂度在O(n²)。
另一种改进的方法被称为滑动窗口法:滑动窗口法中有两个边界,左边界和右边界,从右边界开始不断探测字符,如果字符在当前扫描区间没出现过,则拓展右边界,如果字符在当前扫描区间出现,则将左边界设置为重复字符的位置+1。
如对于字符串abcdecb探测的时候,开始滑动窗口的左边界在a,右边界不断向右拓展,一直到第二个c,发现c在abcde出现过,窗口的左边界缩到第一个c的右边位置即d的位置,此时窗口的子串为de,然后右边界继续拓展,如此循环,直到右边界到达尾字符。
可以看出,在循环过程中,一直都是右边界在进行拓展,右指针扫描n次,所以时间复杂度在O(n)。
根据这个思路,我们可以写出如下代码
int lengthOfLongestSubstring(string s)
{
int maxLength = 0;
decltype(s.length()) startIndex = 0; //左边界
decltype(s.length()) endIndex = 0; //右边界
unordered_map<char, int> charIndex;
while (endIndex < s.length())
{
auto ite = charIndex.find(s[endIndex]);
if (ite == charIndex.end() || ite->second < startIndex)
{//该字符之前没出现过,或者虽然出现了,但是在本次扫描的区间之前
charIndex[s[endIndex]] = endIndex;
endIndex++;
maxLength = (endIndex - startIndex) > maxLength ? (endIndex - startIndex) : maxLength;
}
else
{//该字符之前出现过,而且在扫描区间内,更新扫描区间
startIndex = ite->second + 1;
}
}
return maxLength;
}
到此我们可以看出,滑动窗口和双层遍历最大的不同在于出现重复字符时的处理策略不同:
- 双层遍历:出现重复字符时,重新进行二次扫描,如扫描到第二个c时,abcdecde,判断c是重复的,放弃以a为首的所有子串,开始探测以b为首的子串,可以认为将左边界和右边界同时重置到了b。
- 滑动窗口:当c和之前的字符c重复时,直接将左边界重置到c->index+1,即d。
我们可以进行一些简单的证明,
假设AB——Y—C—T为当前的窗口,A为左边界,T为右边界,下一个字符Y在AT出现过
AB——Y——TY
此时在双层遍历中,会把左边界置为B,但可以看出,以B为首的子串长度要想超过AT,尾字符的位置必须>T,至少要来到Y,BY=AT,但此时Y已经和前面的Y重复了,故以B为首的不重复子串长度不可能超过AT,同理可以推理出一直到第一个Y后面的字符Z,以它为首的子串才可能超过AT,如AB——YZ——TYU。
所以滑动窗口法才会把左边界置为重复字符的下一个位置。
其中还有一个小地方需要注意,在重置左边界的时候,并没有修正哈希map,下次判断字符是否重复的时候,即使字符是重复的,但只要重复的位置不在本次扫描区间,就不影响本次的条件判断。