无重复字符的最长子串

难度: 中等

标签: 哈希表 字符串 滑动窗口

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

Submission

运行时间: 76 ms

内存: 17 MB

from collections import defaultdict

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        memo = defaultdict(int)
        ret = 0
        l = r = 0
        while r < len(s):
            c = s[r]
            memo[c] += 1
            r += 1
            while memo[c] > 1:
                d = s[l]
                l += 1
                if d in memo:
                    memo[d] -= 1

            ret = max(ret, r - l)
        return ret
0 0

Explain

这个题解使用了滑动窗口的思路。用两个指针 l 和 r 表示滑动窗口的左右边界,用一个哈希表 memo 记录窗口内每个字符出现的次数。每次移动右指针 r,将新字符加入窗口,并更新哈希表。如果发现哈希表中新加入字符的次数大于1,说明有重复字符,则移动左指针 l 缩小窗口,并更新哈希表,直到窗口内没有重复字符为止。在移动过程中,用一个变量 ret 来记录最长无重复字符子串的长度。

时间复杂度: O(n)

空间复杂度: O(min(m, n))

from collections import defaultdict

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        memo = defaultdict(int)  # 记录每个字符出现的次数
        ret = 0  # 记录最长无重复字符子串的长度
        l = r = 0  # 滑动窗口的左右指针
        while r < len(s):
            c = s[r]  # 右指针指向的新字符
            memo[c] += 1  # 将新字符加入哈希表
            r += 1  # 右指针右移
            while memo[c] > 1:  # 如果新加入的字符导致重复
                d = s[l]  # 左指针指向的字符
                l += 1  # 左指针右移
                if d in memo:
                    memo[d] -= 1  # 更新哈希表,缩小窗口
            
            ret = max(ret, r - l)  # 更新最长无重复字符子串的长度
        return ret

Explore

为什么在更新最长无重复字符子串的长度时,使用`max(ret, r - l)`而不是其他方式?

在该算法中,`r - l` 计算的是当前无重复字符的子串长度。由于在每次循环中子串的长度可能增加(当右指针向右移动并添加新字符时)或减少(当左指针向右移动以排除重复字符时),因此我们需要不断地更新记录的最大长度。使用 `max(ret, r - l)` 确保了每次都将最大长度记录下来,而不是仅仅记录最后一个窗口的大小。这是因为最长的无重复子串可能出现在任何位置,并不一定是最后一个窗口。

哈希表`memo`的作用是什么?为什么要记录每个字符出现的次数?

哈希表 `memo` 用于实时记录滑动窗口内每个字符的出现次数。这样做的目的是快速检测窗口中是否有重复字符。当右指针 `r` 向右移动并添加一个新的字符到窗口时,我们需要检查该字符是否已经存在于窗口中。如果有,则需要通过移动左指针 `l` 来排除重复,直到窗口中该字符的数量减少到1或0。记录每个字符的出现次数使得这个过程变得快速和简单,避免了每次都需要遍历整个窗口来查找重复字符。

当发现新加入的字符导致重复时,为什么只需要移动左指针`l`而不需要同时调整右指针`r`?

当右指针 `r` 添加一个新的字符后出现重复时,说明要解决这个重复问题,需要从窗口的左边开始排除字符,因为滑动窗口是从左往右扩展的。移动左指针 `l` 是为了缩小窗口,直到窗口中不再有重复字符。右指针 `r` 保持不动是因为我们不需要回退其进展——我们已经知道在 `r` 之前的位置直到 `l` 中没有重复,所以只需继续从 `r` 向右扩展即可。这样可以有效地通过单次遍历解决问题,保持时间复杂度控制在线性级别。

在内层while循环中,为什么要检查`if d in memo`再进行`memo[d] -= 1`操作?

这个检查是为了确保代码的健壮性和正确性。虽然在当前的上下文中,`d`(左指针 `l` 指向的字符)总是存在于哈希表 `memo` 中,因为它是之前加入到窗口的。但在更复杂的修改或不同的实现中,可能会出现 `d` 不在 `memo` 中的情形,特别是如果代码被修改以处理更复杂的场景或在不同的算法结构中重用。这种检查可以避免在尝试减少一个不存在的键的值时抛出错误,从而使函数更加安全和健壮。

Related Problems

返回题目列表