题目

力扣题目

给定一个字符串 s,找出其中不含重复字符的最长子串的长度。子串必须是原字符串中连续的一段。

示例 1:

Text
输入:s = "abcabcbb"
输出:3
解释:最长无重复子串可以是 "abc"、"bca" 或 "cab"。

示例 2:

Text
输入:s = "bbbbb"
输出:1

示例 3:

Text
输入:s = "pwwkew"
输出:3
解释:答案是 "wke";"pwke" 不是连续子串。

提示:

  • 0 <= s.length <= 10^5
  • s 由英文字母、数字、符号和空格组成

考察内容

  • 滑动窗口的扩张与收缩
  • 使用哈希集合维护窗口内字符
  • 使用哈希表记录字符最近出现的位置
  • 连续子串与子序列的区别
  • 窗口不变量与边界更新

解题思路

滑动窗口 + 哈希集合

使用闭区间 [left, right] 表示当前窗口,并用哈希集合保存窗口内出现的字符。始终维护不变量:窗口内没有重复字符。

当右指针准备加入 s[right] 时:

  1. 如果字符不在集合中,直接加入并更新最大窗口长度;
  2. 如果字符已经存在,就不断删除左端字符并右移 left;
  3. 直到重复字符被移出窗口,再加入 s[right]。

必须使用 while 收缩,因为重复字符不一定恰好位于窗口左端。例如窗口为 "abca" 时,需要连续移除 a 之前的所有字符以及旧的 a。

以 s = "abcabcbb" 为例:

right加入字符收缩后的窗口当前最大长度
0aa1
1bab2
2cabc3
3abca3
4bcab3
5cabc3
6bcb3
7bb3

每个字符最多进入和离开集合一次,因此:

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k),k 为字符集大小,最坏为 O(n)O(n)

最近位置跳转

进一步使用哈希表记录每个字符最近出现的下标。若 s[right] 曾经出现在当前窗口内,可以令:

left=max⁡(left,last[s[right]]+1)left = \max(left, last[s[right]] + 1)

这样左边界可以一次跳过旧的重复字符,不必逐个删除。这里必须取 max,否则字符上一次出现的位置若已经在窗口左侧,会让 left 错误地向后退。

代码演示

C++
// 写法一:哈希集合维护窗口中的字符
int lengthOfLongestSubstring(const std::string& s) {
    std::unordered_set<char> window;

    int left = 0;
    int result = 0;

    for (int right = 0; right < static_cast<int>(s.size()); ++right) {
        while (window.count(s[right])) {
            window.erase(s[left]);
            ++left;
        }

        window.insert(s[right]);
        result = std::max(result, right - left + 1);
    }

    return result;
}

// 写法二:记录最近下标,让 left 直接跳转
int lengthOfLongestSubstringJump(const std::string& s) {
    std::unordered_map<char, int> last;

    int left = 0;
    int result = 0;

    for (int right = 0; right < static_cast<int>(s.size()); ++right) {
        auto it = last.find(s[right]);
        if (it != last.end()) {
            left = std::max(left, it->second + 1);
        }

        last[s[right]] = right;
        result = std::max(result, right - left + 1);
    }

    return result;
}

易错点

  • 把子串理解成子序列:子串必须连续,不能跳过中间字符。
  • 用 if 代替 while 收缩窗口:移除一个左端字符后,重复字符可能仍在窗口内。
  • 加入重复字符后才收缩:集合写法应先排除旧字符,再插入当前字符,才能维护窗口无重复的不变量。
  • 窗口长度少算一位:闭区间 [left, right] 的长度是 right - left + 1。
  • 跳转写法让 left 后退:必须使用 max(left, last[c] + 1)。
  • 空字符串处理错误:循环不会执行,初始答案 0 正好符合要求。

总结

本题是滑动窗口的经典模型:右指针负责扩张,发现重复字符时左指针负责收缩,窗口始终保持无重复。集合写法更直观;最近位置写法可以让左边界一步跳转。

相关题目

    1. 最小覆盖子串
    1. 至多包含两个不同字符的最长子串
    1. 长度最小的子数组
    1. 替换后的最长重复字符
    1. 水果成篮