无重复字符的最长子串
无重复字符的最长子串
题目是LeetCode的第三题,描述如下:
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
分析
准确的分析题目的问题,才是解决问题的关键。
- 问题:给定
一个字符串
,请你找出其中不含有重复字符
的最长子串
的长度
。 - 关键点:输入是字符串(String),输出是长度(Number),代码逻辑是找出不含重复字符的最长子串。因为输出的是长度,那么在算法过程中就只需要比较长度大小就可以。
- 注意:子串与子序列不同,子串是必须连续的,区别见实例3的解释。
思路
1. 蛮力法
我们先使用蛮力法的来思考这个问题,先找到所有的子串
,再从子串中找到满足条件的最长子串。以"abcb"
为例来分析,思路如下:
- 刚刚分解出三个条件,先从容易的
子串
开始,会分解为"a"、"b"、"c"、"b"、"ab"、"bc"、"cb"、"acb"、"bcb"、"abcb"
; - 去掉
重复的子串
和含重复字符
的子串,就剩下"a"、"b"、"c"、"ab"、"bc"、"cb"、"acb"
; - 最后找到最长的子串
"abc"
,返回长度3
;
在这个思路中,长度为n的字符串会分解成 n + (n - 1) + (n - 2) + ... + 1 = (n + 1) * n / 2
个子串。 在去掉含有重复字符的子串
时,循环的次数(子串的数量 * 子串的长度)大大的增加。总的来说,蛮力法不仅要使用更多的空间来存储中间的变量,时间复杂度也会很高,代码实现起来也很麻烦。
2. 滑动窗口算法
蛮力法的特点在于遍历所有满足要求的结果集合,从中找到得到符合的结果。那有没有办法不用遍历所有的子串也能找到题目要求的结果呢?
当然是有的,这个方法就是滑动窗口算法,此算法通过改变起止下标方式在列表上寻找符合条件子列表。把子列表看成是一个窗口,当起止下标同时变大同样大小,那这个窗口就像在滑动;当起止下标改变不一样的时候,窗口不仅在滑动,窗口的大小也在改变。
使用条件
使用这个算法需要同时满足两个条件:
- 线性数据结构;
- 结果来自于连续的子序列
本题的问题寻找字符串的最长子串,字符串中的字母可以通过下标的方式取得,因此字符串可以看成是数组或链表,满足第一个条件;子串是连续的字符,正好也满足第二个要求(如果问题是最长子序列,那动态规划就是比较好的方案了)。
只要满足上面两个条件,就可以使用滑动窗口的算法了。但是本题的话,还需要注意的就是窗口的大小。由于不知道最长子串具体的长度,那窗口的大小就是动态变化的。在编写代码的时候,尤其要注意窗口大小变化的条件,这也是最容易犯错的地方。
应用场景
在leetcode中出现的题目,也可以使用滑动窗口算法解决的题目有:3. 无重复字符的最长子串、219.存在重复元素II、209.长度最小的子数组、438. 找到字符串中所有字母异位词。当然还有很多别的题目也可以使用这个算法,就不一一列举了。
另外,学过网络的同学,应该知道TCP的流量控制中也有滑动窗口算法的使用,有兴趣的同学可以点击了解一下。
代码实现
滑动窗口实现(固定大小窗口)
由于我们并不知道满足条件的最长子串的长度是多少,所以无法确定窗口的大小。既然需要求解的是最大值,我们可以将窗口设置成输入字符串的长度,找到第一个满足所有字符只出现过一次的子串。
逻辑如下:
- 如果字符串的长度为1或者是0,直接返回它的长度;
- 用一个循环来控制窗口的大小,每一次循环完就减少窗口大小1;
- 在字符串上截取窗口大小的子串;
- 从子串的第一个字符,一直到最后一个字符,判断是否该字符出现的次数。如果出现次数大于1,则窗口滑动1;否则判断是否是最后一个字符,是的话就可以返回这个子串的长度,否的话判断下一个字符;
代码实现如下:
class Solution(object):
def lengthOfLongestSubstring(self, s):
subStr = ''
l = len(s)
ww = l
if l == 0 or 1 == 1:
return l
# 控制窗口大小变化
for i in range(0, ww):
# 窗口宽度为ww, 窗口滑动范围
for j in range(0, l - ww + 1):
subStr = s[j:j + ww]
# 判断当前字符在子串出现次数
for c in subStr:
if subStr.count(c) > 1:
break
elif subStr.index(c) == len(subStr) - 1:
return len(subStr)
ww = ww - 1
时间复杂度:上述代码实现用了3个for循环,加上subStr.count这个方法,时间复杂度是O(n4)
空间复杂度:存储变量的空间始终没变,所以空间复杂度是O(1)
这个代码有个地方是可以改进的。在子串中发现有一个字符出现多次,直接滑动窗口1。如果改成直接滑动到重复字符最后一次出现的地方,速度当然还是会快一些的。不过,这个操作还是没有改变时间复杂度。
滑动窗口实现(动态大小窗口)
在上面的思路中,窗口的大小在一次循环中是固定,我们可以考虑一下窗口大小动态变化的方案。最开始窗口大小是1,如果下一个字符与子串中的不重复就可以添加到子串里,重复的话窗口大小就变成重复字符下一个一直到子串末尾,然后再添加字符。每一次字符改变的时候,记录下最大的长度就好。具体过程如下:
下面的例子以字符"pwwkew"
来分析,设输入字符为s
,当前下标为i
,当前子串为subStr
、最大长度为maxLength
。
- 读取字符
s.index(i)
,判断s.index(i)
是否存在于subStr
中,如果存在转2,否则转3; - 将
s.index(i)
添加到subStr
末尾; subStr
截取自出现重复字符下标的下一位直到子串的末尾,执行2,比如说当前子串是"pw"
,读取到"w"
,则是将"pw"
截取w
的下一位;- 重复操作1直到读取完字符串
具体步骤见下图:
class Solution(object):
def lengthOfLongestSubstring(self, s):
subStr = ''
maxLength = 0
for c in s:
if c not in subStr:
subStr+=c
else:
# 当出现重复字符时才计数,减少计数次数
mx = max(maxLength, len(subStr))
subStr = subStr[subStr.index(c) + 1:] + c
# 当出现重复字符时才计数,减少计数次数
maxLength = max(maxLength, len(subStr))
return maxLength
时间复杂度:一层for循环,外加判断字符是否出现在字符串里,所以时间复杂度是O(n²)
空间复杂度:存储变量的空间始终没变,所以空间复杂度是O(1)