LeetCode 3:无重复字符的最长子串

题目描述

LeetCode 3:无重复字符的最长子串

难度:中等

给定一个字符串 s,找出其中不包含重复字符的最长连续子串,并返回它的长度。

示例 1

输入:s = "abcabcbb"
输出:3
解释:"abc"、"bca" 和 "cab" 都是长度为 3 的无重复字符子串。

示例 2

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

示例 3

输入:s = "pwwkew"
输出:3
解释:最长无重复字符子串之一是 "wke"。子串要求字符在原字符串中连续。

数据范围

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

题解

思路:滑动窗口与哈希表

使用 [start, end] 表示当前窗口,并保证窗口内的字符互不重复。

  • end 逐个扫描字符串,加入新的字符。
  • start 记录窗口的左边界,只向右移动。
  • mp 记录每个字符最近一次出现的下标加一。
  • ans 记录遍历过程中发现的最大窗口长度。

遇到字符 alpha := s[end] 时,如果它曾经出现过,取出 v := mp[alpha]。当 v > start 时,说明上次出现的位置仍在当前窗口内,将 start 移动到 v,就能排除旧字符。

随后用 end - start + 1 更新答案,并将 mp[alpha] 更新为 end + 1。

为什么保存下标加一

假设字符上次出现在下标 i。再次遇到它时,窗口左边界需要移动到 i + 1。直接保存 i + 1,就可以用 start = v 完成更新。

例如 "abcabcbb" 扫描到下标 3 的 a 时,mp['a'] 为 1。将 start 从 0 移动到 1 后,窗口变为 "bca",其中的字符仍然互不重复。

为什么要判断 v > start

哈希表保存了扫描过的所有字符的位置,其中可能包含已经离开窗口的字符。只有旧位置仍在窗口内时,才需要移动左边界。

以 "abba" 为例:

  1. 扫描到第二个 b 时,将 start 移动到下标 2,窗口为 "b"。
  2. 扫描到最后一个 a 时,mp['a'] 为 1,小于当前 start = 2。
  3. 保持左边界为 2,得到合法窗口 "ba",最终答案为 2。

如果直接赋值 start = v,左边界就会退回下标 1,形成包含重复字符的窗口 "bba"。因此左边界必须保持单调递增。

正确性说明

每轮处理前,已有窗口中的字符互不重复。加入新字符后,唯一可能新增的重复来自该字符的历史位置。

如果旧位置在窗口内,将左边界移动到旧位置后一位,即可去除重复;如果旧位置在窗口外,当前窗口可以直接扩展。这样的更新保留了以 end 结尾的最长合法窗口。

遍历所有右边界并取最大窗口长度,就能得到整个字符串的最长无重复字符子串长度。

复杂度分析

  • 时间复杂度:平均 O(n)。右边界遍历字符串一次,每轮只进行常数次哈希表查询、更新与比较。
  • 空间复杂度:O(min(n, |Σ|)),其中 |Σ| 表示字符集大小。这份代码使用 byte 作为键,最多保存 256 个不同的值,因此在固定字节字符集下也可以记为 O(1)。

Go 代码

func lengthOfLongestSubstring(s string) int {
	ans := 0
	n := len(s)
	mp := make(map[byte]int)

	for start, end := 0, 0; end < n; end++ {
		alpha := s[end]
		if v, ok := mp[alpha]; ok {
			if v > start {
				start = v
			}
		}
		if end-start+1 > ans {
			ans = end - start + 1
		}
		mp[alpha] = end + 1
	}

	return ans
}

这份实现按字节处理字符串,适用于本题的字符范围。处理中文、表情等 Unicode 内容时,可以转换为 []rune,并使用 map[rune]int 按 Unicode 码点维护窗口。

小结

滑动窗口通过移动左边界排除重复字符,哈希表提供最近一次出现的位置。理解 mp 保存下标加一,以及 start 始终向右移动这两个细节,就能准确维护窗口并在线性时间内求出答案。

← back