题目
给定一个字符串 s ,请你找出其中不含有重复字符的 最长
子串的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
提示:
- 0 <= s.length <= 5 * 104
- s 由英文字母、数字、符号和空格组成
练气萌新
我们继续我们的冒险,让我们用一个有趣的故事来探讨无重复字串问题的解决思路。
在一片广阔的海洋中,有四座神秘的岛屿,分别被称为岛屿A、岛屿B、岛屿C和岛屿D。这些岛屿之间有一条不稳定的的超时空列车在运行,列车每天会按照特定的顺序在岛屿之间穿梭,但是这个顺序每天是不稳定的。这个列车只有在明天日出的时候,它才知道自己会以一个什么样的状态经过这些岛屿。
比如今天是: A-B-C-D
列车会经过A岛屿,B岛屿,C岛屿,D岛屿。
明天可能就变成了:A-C-A-B-C-D
列车会经过A岛屿,C岛屿,然后突然进入一个时空裂缝,回到了A岛屿,又经过B岛屿,C岛屿,D岛屿。
勇敢的冒险家小明,他决定乘坐这辆超时空列车,探索这些岛屿的奥秘。可是这个神秘岛屿有个神秘的地方,每当你重复登上这个岛屿的时候,你会被岛屿当成贡品吃掉,成为岛屿的养分。所以,为了领略岛屿的更多风光,小明需要找出列车可以行驶的所有可能的路线中经过最多岛屿的一段行程,同时避免重复经过相同的岛屿序列,因为倒霉的小明还没吃过爱情的苦,他的人生还很长,他还不想被吃掉。
小明开始思考这个问题,以这个时刻表“A-C-A-B-C-D”为例子,
小明的思路是这样的:他假设自己从每一个岛屿都开始出发,然后观察在之后的旅程中,哪些岛屿会因为重复出现而被"吃掉"。通过这种方式,他可以找出从每个起点出发能够到达的最远的岛屿,也就是最长的无重复岛屿序列。
为了实现这个想法,小明制作了一张表格:
通过这个表格可以看到,最长经历过的岛屿数量是4,他解决了这个问题!
打开小明的大脑,看看他怎么思考的,里面呈现出来的景象如下:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0 # 记录最长无重复岛屿序列的长度,题目说了最小为0,我们从0开始
for i in range(len(s)): # 从每个岛屿作为起点开始探索
visited = set() # 记录已访问过的岛屿
curr_length = 0 # 记录’绿色可行路线‘的长度
for j in range(i, len(s)): # 从起点岛屿开始,向后探索
if s[j] not in visited: # 如果当前岛屿没有被访问过
visited.add(s[j]) # 将当前岛屿标记为已访问
curr_length += 1 # 当前无重复岛屿序列长度加1
else: # 如果当前岛屿已经被访问过,说明遇到了重复岛屿
break # 停止探索,记录当前的无重复岛屿序列长度
max_length = max(max_length, curr_length) # 更新最长无重复岛屿序列长度
return max_length # 返回最长无重复岛屿序列的长度
max_length = 0
: 小明使用一个变量 max_length 来记录目前为止找到的最长无重复岛屿序列的长度。
for i in range(len(s)):
: 小明从每个岛屿作为起点开始探索。这就像小明假设自己从每一个岛屿都开始出发,看看能走多远。
visited = set()
: 小明使用一个集合 visited 来记录在当前探索中已经访问过的岛屿,这样可以快速判断一个岛屿是否已经出现过。(回顾我们的第一课:知识点3:如何快速如何快速判断var2是否出现在数组中)
curr_length = 0
: 小明使用一个变量 curr_length 来记录当前无重复岛屿序列的长度。
for j in range(i, len(s)):
: 从起点岛屿开始,小明向后探索,就像小明沿着航线前进,直到遇到重复的岛屿。
if s[j] not in visited:
: 如果当前岛屿没有被访问过,说明它是一个新的岛屿,小明准备将其加入到当前的无重复岛屿序列中。
visited.add(s[j])
: 小明将当前岛屿标记为已访问,以免后续重复访问。
curr_length += 1
: 当前无重复岛屿序列的长度加1,因为小明找到了一个新的岛屿。
else:
: 如果当前岛屿已经被访问过,说明小明的航线中出现了重复的岛屿。
break
: 小明停止探索,记录当前的无重复岛屿序列长度,因为继续前进会让小明被岛屿吃掉。
max_length = max(max_length, curr_length)
: 小明更新目前为止找到的最长无重复岛屿序列的长度,就像小明在表格中记录每条航线的最长无重复岛屿序列长度一样。
return max_length
: 在探索完所有可能的起点后,小明返回最长无重复岛屿序列的长度,这就是小明想要找到的答案。
不过小明的这个思路work,但是不够优雅。我们有没有什么好的点子可以帮助到他呢。
如之前所说,暴力破解的思路一般是因为遗漏了信息,或者说没有充分利用信息。因此,我们来回看一下这个表格,看看哪些信息,是小明疏忽了的。或者说,哪些信息我们没有利用到,针对最长可行路线这个问题,找找表格里,哪里是做了重复的计算,导致了浪费。
我们注意这段序列,在遍历过A-B-C-D这个路线后,其实再去找B-C-D, C-D,D的可行路线,已经失去意义,因为这些都是A-B-C-D这个路线的子路线(子串),所以我们遗漏了信息:**最长非重复字符串的字串,也是非重复的。**我们可以调整我们的循环条件,优化掉这部分冗余的计算。
回看小明的大脑,在之前小明的暴力破解的思路里,小明在外层循环用i
来表示的假设的上车地点,内层循环用j
来表示的可行路线的终点。这里我们注意到当内层循环j
是今天路线表的终点站的时候,就可以结束循环了。因为,后面找到的路线,都在A-B-C-D这个路线的子路线里。所以尾部提前添加处理。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0 # 记录最长无重复岛屿序列的长度
for i in range(len(s)): # 从每个岛屿作为起点开始探索
visited = set() # 记录已访问过的岛屿
curr_length = 0 # 记录当前无重复岛屿序列的长度
for j in range(i, len(s)): # 从起点岛屿开始,向后探索
if s[j] not in visited: # 如果当前岛屿没有被访问过
visited.add(s[j]) # 将当前岛屿标记为已访问
curr_length += 1 # 当前无重复岛屿序列长度加1
else: # 如果当前岛屿已经被访问过,说明遇到了重复岛屿
break # 停止探索,记录当前的无重复岛屿序列长度
max_length = max(max_length, curr_length) # 更新最长无重复岛屿序列长度
if j == len(s) - 1: # 如果内层循环j已经到达字符串的末尾
break # 提前结束外层循环,因为后面的子串都是当前子串的子集
return max_length # 返回最长无重复岛屿序列的长度
好像没有什么实质的变化,我们再放大一下,后面我们进行了处理,前面是否也遗漏了信息呢。
在第一次循环到第二次循环,C这个序列出现了重合,但是我们好像看不出问题。
继续往后看,第二次循环到第三次循环
我们可以注意到,对于A-C-A-B-C-D这个时刻表,在第2次循环C-A-B和第3次循环的A-B-C-D中。
A-B这个序列出现了重合,也就是说我们在前面同样遗漏了这个信息,**最长非重复字符串的子串,也是非重复的。**我们每次循环中,没有充分利用上一次循环告诉我们的信息。我们可以利用前一次循环得到的信息,避免重复计算。
例如:
在第1次循环中,我们找到了非重复子串A-C。这告诉我们,下一次循环可以直接从C开始,因为我们已经知道A-C是一个非重复子串。
在第2次循环中,我们从C开始,找到了非重复子串C-A-B。这告诉我们,下一次循环可以直接从A-B开始,
在第3次循环中,我们从A-B开始,找到了非重复子串A-B-C-D。此时,内循环的索引j
已经抵达了字符串的末尾。这意味着,我们已经找到了以A-B开头的最长非重复子串,不需要再继续循环下去了。因此,我们可以提前结束内循环,进入外循环。
好的,现在整理一下我们的思路,我们发现我们每次循环需要维护的是一个window(窗口) 第一次循环是A-C,第二次是C-A-B,第三次是A-B-C-D
所以我们需要两个变量,一个变量left
表示起点的下标,一个变量right
表示终点的下标,初始都从下标0开始,所以left=right=0
,就像第一次循环我们是从A开始的一样。循环条件就是right
抵达字符串末尾,然后right
接触到重复的字符,就利用当前最长字串的信息,进入“下一次内循环”,
我们发现,每次循环需要维护一个窗口(window),这个窗口表示当前的无重复字符子串。第一次循环是A-C,第二次是C-A-B,第三次是A-B-C-D。
为了表示这个窗口,我们需要两个变量:
- left:表示窗口的起点下标
- right:表示窗口的终点下标
初始时,我们从下标0开始,所以left=right=0
,就像第一次循环我们是从A开始的一样。
我们的主循环条件是right还没有抵达字符串的末尾。在每次主循环中:
- 取s[right]的值
- 然后检查s[right] 是否会和窗口内的字符冲突:
- 如果出现了重复字符,我们就需要移动left,缩小窗口,直到窗口内没有重复字符为止。这个过程可以看作是之前的那次"内循环"。
- 如果没有重复字符,我们将s[right]加入到当前窗口,继续移动right,扩大窗口。
- 在移动right之前,我们需要更新当前最长无重复子串的长度。
代码实现如下:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left=right=0
max_length = 0 # 记录最长无重复岛屿序列的长度
curr_length = 0 # 记录当前无重复岛屿序列的长度
visited=set() # 记录已访问过的岛屿
while right < len(s): # 修改循环条件,当右边到底就结束循环
while s[right] in visited: # 出现重复后,一直缩短左边的窗口,直到找到这次循环可以利用到的最长非重复字串
visited.remove(s[left])
left+=1 # 下一次循环从当前的left+1开始
else:
visited.add(s[right])
curr_length = right-left+1 # 记录当前长度,当前长度是当前的right-left+1
right += 1 # 下一次循环从当前的right+1开始
max_length=max(max_length,curr_length) # 查一下现在的长度和记录的最长长度
return max_length # 返回最长无重复岛屿序列的长度
看上去不错,我们帮助冒险家小明变聪明了!一起期待下一次冒险吧!~
have fun~
注:如果这个结果可能因为测试用例的不同结果有差异;
筑基初期
我们使用了set()
, 来辅助做是否重复的判断,在我们的这个问题场景中,可以用其他数据结构,让它更快吗?点赞收藏过100,解锁章节】
结丹初期
这个题目我们可以引入“分而治之”的思路,进一步优化算法否?【点赞收藏过100,解锁章节】
代码纯享版本
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left=right=0
max_length = 0 # 记录最长无重复岛屿序列的长度
curr_length = 0 # 记录当前无重复岛屿序列的长度
visited=set() # 记录已访问过的岛屿
while right < len(s): # 修改循环条件,当右边到底就结束循环
while s[right] in visited: # 出现重复后,一直缩短左边的窗口,直到找到这次循环可以利用到的最长非重复字串
visited.remove(s[left])
left+=1 # 下一次循环从当前的left+1开始
else:
visited.add(s[right])
curr_length = right-left+1 # 记录当前长度,当前长度是当前的right-left+1
right += 1 # 下一次循环从当前的right+1开始
max_length=max(max_length,curr_length) # 查一下现在的长度和记录的最长长度
return max_length # 返回最长无重复岛屿序列的长度