给定一个字符串
s,请你找出其中不含有重复字符的 最长 子串 的长度。示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke"是一个子序列,不是子串。提示:
0 <= s.length <= 105s由英文字母、数字、符号和空格组成
第一其实是很懵的,很久没写字符串相关的了,本来这一个板块算是我的弱点,而且涉及到的东西也比较多,像动态规划这种。因此先问了GPT,让它给我启发,有些思维模式启发的感觉还是不错。
对算法题的模板感太重了,反而失去了思考。
先别想"该用什么算法模板",先抓住题目中的一个关键词:子串。子串是连续的,所以你实际上是在字符串上维护一段连续区间。
假设你正在从左往右读字符串,并且目前手里有一个满足条件的子串:
abc
现在又读到一个 a:
abca
它不合法了。此时思考三个问题:
- 有没有必要把整个子串清空、从新
a重新开始? - 为了重新合法,应该移动子串的左端还是右端?
- 左端最少移动到哪里,才能排除重复的
a?
你会发现,不需要全部推倒重来:
abca
↓
bca
去掉旧的 a 以后,剩下的部分仍然有利用价值。
接下来尝试用纸模拟:
pwwkew
每读入一个字符,都记录四件事:
当前读到的字符
当前连续区间
区间是否合法
历史最大长度
关键在于始终维持一个"不重复区间"。新字符进来后:
- 没有重复:区间可以继续扩大。
- 出现重复:缩小区间,直到它重新合法。
- 重新合法后:更新最大长度。
你需要维护的状态大致只有:
- 当前区间的左边界。
- 正在向右探索的位置。
- 当前区间中有哪些字符,或者每个字符最近在哪里出现过。
- 目前见过的最大长度。
根据GPT的引导和回复写了第一版代码:
python
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
n = len(s)
res = 1
left, right = 0, 1
substr = {s[0]:0}
while left < n and right < n:
if s[right] not in substr:
substr[s[right]] = right
right += 1
res = max(res, len(substr))
else:
# 出现了重复的
index = substr[s[right]] # 取出重复的位置
left = index + 1
if right <= left: right = left + 1
substr.clear()
substr[s[left]] = left
return res
能过三个简单的测试用例,但提交有 661 / 1036 个通过的测试用例
边界问题,或者说是移动问题,也就是出现重复字符的时候如何更新让它重新变回一个合法区间,给个反例你就能想明白了
输入
s ="mjvhmi"
输出
4
预期结果
5
自己想了下又修改了一版,逻辑没有问题可以通过,但是差点忘记len(s) == 0的情况了
python
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
# 最后一版本!!忘记了边界,注意是可以提交空字串的!!
if not s:
return 0
n = len(s)
res = 1
left, right = 0, 1
substr = {s[0]:0}
while left < n and right < n:
if s[right] not in substr:
substr[s[right]] = right
res = max(res, len(substr))
else:
# 出现了重复的
index = substr[s[right]] # 取出重复的位置
while left <= index:
del substr[s[left]]
left += 1
left = index + 1
substr[s[right]] = right
right += 1
return res
最后GPT给了一个简化版本
python
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
res = 0
left = 0
last_index = {}
for right, char in enumerate(s):
if char in last_index and last_index[char] >= left:
# 移动左边界
left = last_index[char] + 1
last_index[char] = right # 更新
res = max(res, right - left + 1) # 相当于维护一个左边界
return res
这里不去记录每个子串出现的位置,只维护一个字典记录每个字符最后一次出现的位置,避免删除操作,比较麻烦
是啊!res可以不用这个字典的长度去求,有字串左右两侧的Index自然可以得到字串的长度!