题目描述
给定一个字符串 s
,找出其中不含有重复字符的最长子串 的长度。题目链接
示例
- 输入:
s = "abcabcbb"
,输出:3
(最长无重复子串为 "abc") - 输入:
s = "bbbbb"
,输出:1
(最长无重复子串为 "b") - 输入:
s = "pwwkew"
,输出:3
(最长无重复子串为 "wke")
解法一:滑动窗口(长度维护法)
思路:利用哈希表记录字符最后出现的索引,通过维护「以当前字符结尾的无重复子串长度」动态调整窗口边界。
python
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
ans = pre = 0 # ans记录最长长度,pre记录当前子串长度
m = {} # 存储字符最后出现的索引
for k, v in enumerate(s):
pre = min(pre + 1, k - m.get(v, -1)) # 调整当前子串长度
ans = max(ans, pre) # 更新最长长度
m[v] = k # 记录当前字符的最新位置
return ans
关键点解析:
pre = min(pre + 1, k - m.get(v, -1))
:
-
- 若字符未重复,子串长度+1;若重复,则收缩窗口至重复字符的下一位
min
操作确保窗口左边界不回退
- 时间复杂度:O(n),每个字符仅遍历一次
- 空间复杂度:O(m),m为字符集大小(最坏存所有不同字符)
解法二:滑动窗口(边界维护法)
思路:直接维护窗口左边界,确保窗口内无重复字符,通过「当前索引-左边界」计算窗口长度。
python
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
m = {} # 存储字符最后出现的索引
pre, ans = -1, 0 # pre为窗口左边界(不包含),ans为最长长度
for i, v in enumerate(s):
pre = max(pre, m.get(v, -1)) # 左边界不回退
ans = max(ans, i - pre) # 计算当前窗口长度
m[v] = i # 记录当前字符的最新位置
return ans
关键点解析:
pre = max(pre, m.get(v, -1))
:
-
- 若字符重复,左边界移动至重复字符的下一位;否则保持原边界
max
操作确保左边界不回退
- 窗口长度直接由
i - pre
计算,逻辑更直观 - 时间/空间复杂度与解法一一致
两种解法对比
维度 | 解法一(长度维护) | 解法二(边界维护) |
---|---|---|
核心变量 | pre 表示当前子串长度 |
pre 表示窗口左边界(不包含) |
边界控制 | min(pre+1, k-m[v]) |
max(pre, m[v]) |
长度计算 | 隐式在 pre 中更新 |
显式通过 i-pre 计算 |
代码可读性 | 较简洁,但逻辑需要结合子串长度理解 | 更直观,直接对应滑动窗口模型 |
优化拓展
- 字符集限制:若已知字符集(如ASCII),可用长度256的数组替代字典,提升常数性能
- 边界处理:空字符串直接返回0(两解法均已自动处理)
- 极端测试用例:
-
- 全重复字符(如"aaaaa")→ 输出1
- 无重复字符(如"abcdefg")→ 输出长度n
- 回文字符串(如"abba")→ 输出2(子串"ab"或"ba")
总结
两解法均为滑动窗口的经典实现,通过哈希表记录字符位置实现O(n)时间复杂度。解法二通过直接维护窗口边界,代码逻辑更贴近滑动窗口的直观理解,是更推荐的实现方式。在面试中,此类解法能体现对线性时间算法和空间换时间思想的掌握。