给定一个字符串,找出不含有重复字符的最长子串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 无重复字符的最长子串是 "abc",其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 无重复字符的最长子串是 "b",其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 无重复字符的最长子串是 "wke",其长度为 3。请注意,答案必须是一个子串,"pwke"是一个子序列 而不是子串。
解决方案
方法一:暴力法
思路
逐个检查所有的子字符串,看它是否不含有重复的字符。
算法
从字符串的的起始位置开始,依次逐个的遍历每一个字符,对于每一个访问到的字符,作为一个子字符串的开头,开始一个新的循环,并将开头字符存放到哈希表(map)中,并将接下来访问到的新的字符依次放入到哈希表中,直到遇到一个重复的字符,此时统计该子字符串的长度。为了避免算法的复杂度变为O(n^3),算法中使用哈希的数据结构,来进一步降低字符查找的时间复杂度。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = s.length();
int max = 0;
for(int i=0;i<len;i++)
{
map<char,int> mymap;
int temp = 0;
for(int j=i;j<len;j++)
{
if(!mymap.count(s[j]))
{
mymap[s[j]] = j;
temp++;
}
else
{
if(temp > max)
max = temp;
mymap.clear();
break;
}
}
if(temp > max)
max =temp;
}
return max;
}
};
复杂度分析
-
时间复杂度:O(n^2)
-
空间复杂度:O(min(n,m)),我们需要 O(k) 的空间来检查子字符串中是否有重复字符,其中 k 表示哈希表的大小。而哈希表的大小取决于字符串 n的大小以及字符集/字母 m的大小。
方法二:滑动窗口
算法
暴力法非常简单。但它太慢了。那么我们该如何优化它呢?
在暴力法中,我们会反复的检查一个子字符串中是否含有重复的字符,但这是没有必要的。如果从索引 i 到 j-1 之间的字符串Sij已经被检查为没有重复的字符,我们只需要检查s[j]对应的字符是否已经存在于子字符串Sij中。
要检查一个字符是否已经在子字符串中,我们可以检查整个子字符串,这将产生一个复杂度为O(n^2)的算法,但我们可以做得更好。
通过使用 HashSet 作为滑动窗口,我们可以用 O(1) 的时间来完成对字符是否在当前的子字符串中的检查。
滑动窗口是数组/字符串问题中常用的抽象概念。 窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即[i, j)(左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [i,j)[i, j)[i,j) 向右滑动 111 个元素,则它将变为[i+1, j+1)(左闭,右开)。
回到我们的问题,我们是用HashSet将字符存储在当前窗口[i, j)(最初i=j)中。然后我们向右滑动索引j,如果它不在HashSet中,我们继续滑动j。直到s[j]已经存在于HashSet中。此时,我们找到的没有重复的最长子字符串将会以索引i开头。同样,我们对所有的i这么做,就可以找到答案。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = s.length();
set<int> HashSet;
int max = 0, i = 0, j = 0;
while(i<len && j < len)
{
if(HashSet.count(s[j]) == 0)
{
HashSet.insert(s[j]);
j++;
max = j-i > max ? j-i : max;
}
else
{
HashSet.erase(s[i]);
i++;
}
}
return max;
}
};
复杂度分析
-
时间复杂度:O(2n)=O(n),在最糟糕的情况下,每个字符将被 i 和 j 访问两次。
-
空间复杂度:O(min(m,n)),与之前的方法相同。滑动窗口法需要 O(k) 的空间,其中 k 表示 HashSet 的大小。而HashSet 的大小取决于字符串 n 的大小以及字符集/字母 m 的大小。
方法三:优化的滑动窗口
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。
也就是说,如果 s[j] 在 [i,j) 范围内有与 j′ 重复的字符,我们不需要逐渐增加 i 。 我们可以直接跳过 [i,j′] 范围内的所有元素,并将 i 变为 j′+1。