3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1: 输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2: 输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3: 输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
滑动窗口程序模板:
缩小窗口与增大窗口视情况可以调换顺序
vector<int> window;
int left = 0, right = 0;
while (right < s.size()) {
// 增大窗口
window.insert(s[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.erase(s[left]);
left++;
}
}
本题运用滑动窗口的解题思路:
维护一个滑动窗口window[i, j],左开右开
- 初始状态为:i = 0, j = -1
- 窗口为空时:(j+1) - i == 0
- 每次循环迭代,先判断当前字符s[j]是否与窗口内的字符重复,并记录窗口长度
- 重复,则缩小窗口window[i+1, j]
- 否,则扩大窗口window[i, j+1]
- 本题扩大窗口前,先缩小窗口直至窗口内无重复
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
unordered_map<char,char> lookup;
int maxStr = 0;
/* 窗口[i, j] */
int i= 0, j = 0;
while (j < s.size()){
/* 判断当前字符是否与窗口内的字符重复 */
while (lookup.find(s[j]) != lookup.end()){
lookup.erase(s[i]);
++i;
}
/* 直到当前字符不与窗口内的字符重复才退出 */
/* 此时满足最长不重复字符串,记录长度,窗口[i, j]大小是(j + 1 - i) */
maxStr = max(maxStr, j - i + 1);
/* 将当前字符记录,只用到了key,而没有用到value */
lookup[s[j]] = j;
++j;
}
return maxStr;
}
};
复杂度分析
map的插入删除、查找为O(1)
2层while循环,且在最坏情况下,第二层while是O(N)
因此,时间复杂度为O(N·N·1),即O(N2)
因为使用了哈希表,所以空间复杂度为O(N)
优化
在上述方法中,缩小窗口时的是一个一个元素擦除的,在最坏情况下,要重复N次。因此,可以:
- 记录字符s[j]上次出现的位置(每次迭代记录字符出现位置)
- 当字符s[j]上次出现的位置大于等于窗口左边界i时,比较当前子串长度与目前为止所有子串最大长度,取最大值作为结果,同时将开始位置设为字符s[j]上次出现位置的下一位
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
int i = 0, j = 0, max = 0, tmp = 0;
map<int, int> record;
/* 滑动窗口法,需要2个指针, [i, j]窗口左开右开 */
while (j < s.size()){
/* 如果该字符被记录过 */
if (record.find(s[j]) != record.end()){
if (record[s[j]] >= i){
/* 若大于等于左边界,说明目前的窗口内出现了该字符 */
/* 更新窗口[i, j]------->[record[s[j]] + 1, j] */
i = record[s[j]] + 1;
}
}
/* 更新最长字符串长度 */
max = max > (j - i + 1) ? max : (j - i + 1) ;
record[s[j]] = j;
++j;
}
return max;
}
};
复杂度分析
map的插入删除、查找为O(1)
经过优化变成了1层while循环,O(N)
因此,时间复杂度为O(N·1),即O(N)
因为使用了哈希表,所以空间复杂度为O(N)
再优化
上述方法都使用了哈希表,针对本题使用的对象都是字符串,可以使用数组来充当哈希表。
- idx[s[j]]记录了字符s[j]上一次出现的位置,将idx[s[j]]与窗口的左边界比较
- 若小于左边界,说明目前的窗口内并不包含该字符
- 若大于等于左边界,说明目前的窗口内出现了该字符
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int res = 0, i = 0, j = 0;
vector<int> idx(128, -1);
while(j < s.size())
{
if(idx[s[j]] >= i)
{
i = idx[s[j]] + 1;
}
/* 注意 */
res = max(res, j - i + 1);
idx[s[j++]] = j;
}
return res;
}
};
复杂度分析
map的插入删除、查找为O(1)
经过优化变成了1层while循环,O(N)
因此,时间复杂度为O(N·1),即O(N)
因为使用了固定内存空间代替哈希表,所以空间复杂度为O(1)