原题:链接
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
示例1:
示例2:
示例3:
老规矩,不直接怼代码。先捋一捋这道题的思路。
其实暴力求解的方法比较容易想得到。要确定无重复字符的最长子串,我们可以先固定一个起始点,例如首先取第一个字符,之后从第二个字符开始不断累加并更新最大长度,直到遇到重复的字符之后则改变起始点,即从第二个字符开始继续上述的操作,直到起始点到达最后一个字符,则意味着我们考虑了该字符串中无重复字符子串的所有可能性,因此最后更新的最大长度一定是本题的解。实现也并不复杂,需要一个额外的函数IsUnique来确定传入的字符串是否包含重复的字符。IsUnique的功能可以用set来实现:
def IsUnique(s, start, end):
# 判断重复的set
Set = set()
for i in range(start, end):
if s[i] in Set:
# 如果有重复
return False
Set.add(s[i])
return True
主函数可以用两个for loop解决:
def lengthOfLongestSubstring(s):
n = len(s)
ans = 0
for i in range(n):
for j in range(i+1, n+1):
if IsUnique(s, i, j):
# 更新最大长度
ans = max(ans, j-i)
return ans
这个算法缺陷很明显,就是SLAD(slow like a doggy)… \* ^ ▽ ^ \*
大致可以推算其为立方阶的时间复杂度(2层for和一个O(n)量级的IsUnique),即O(n^3)。在实际应用中显然是不允许的,为此需要优化。
很明显IsUnique函数是低效的,因为我们只要确定了索引i
到索引j
中没有重复的字符,则只需要判断j+1
索引的字符是否存在于当前的串中即可,而无需重新判断i
到j+1
是否Unique… 为此我们可以做一些调整,能将上个算法的时间复杂度优化到O(n^2)。
具体而言就是每次进行外循环之前,初始化一个集合,并遍历内层循环找到当前i
为起始点的最长子串长度。
def lengthOfLongestSubstring(s):
n = len(s)
if n == 0:
return 0
ans = 1
for i in range(n):
Set = set()
Set.add(s[i])
for j in range(i+1, n):
if s[j] in Set:
break
Set.add(s[j])
ans = max(ans, j-i+1)
return ans
虽然做了进一步的优化,但总觉得还是很慢。是否存在一种线性时间复杂度的算法呢?有的,就是滑动窗口算法(Sliding Window)。
最近也在初学网络相关知识,记得TCP管理ACK号和处理收发包也用到了滑动窗口的算法,关于这以后再补充学习吧…滑动窗口顾名思义,就是用一个可滑动的窗口去包含我们想要考察的索引范围,并动态地改变窗口的大小,即窗头和窗尾。滑动窗口解决此题基于以下的思想:
首先窗头left
指针指向字符串的开头,窗尾指针right
不断滑动(+1)并更新所获得的满足要求的子串的最大长度。滑过的字符须通过一个Hash Table保存,直到right
指向了一个重复的字符,意味着我们移动窗头left
。举个栗子,如字符串adbcbd
,当right
指到第二个b
的时候,发现此为重复的字符,那么可以将left
逐渐移动到第一个b
那里去就行。
为此可以写出如下代码:
def lengthOfLongestSubstring(s):
n = len(s)
dic = {}
left, right, ans = 0, 0, 0
while left < n and right < n:
if s[right] in dic:
# 清除掉之前保存在dic的键值对
# 直到left移动到dic[s[right]]处
del dic[s[left]]
left += 1
else:
# 加入dic中
dic[s[right]] = right
ans = max(ans, right-left+1)
right += 1
return ans
此时,这个算法的时间复杂度为O(n),最坏的情况即全为相同的字符,每个字符都会被left和right访问一次。
其实,还是有优化的空间的。正如上面所举的栗子,left
是可以直接移动到第一个b
那里去的。
def lengthOfLongestSubstring(s):
n = len(s)
dic = {}
# left = -1 方便判断
# 前开后闭 即考虑left+1到right的字符长度
left, right, ans = -1, 0, 0
while right < n:
if s[right] in dic:
# 直接移动过去
left = max(left, dic[s[right]])
dic[s[right]] = right
ans = max(ans, right-left)
right += 1
return ans
整个程序随着right的结束而结束,因此这也是一个时间复杂度为O(n)的算法。
另外考虑到字符的ASCII码属性,一个字符的大小为1个字节,可以表示256个不同的字符。
因此我们也可以仅利用数组而非哈希表来保存遍历的值,其索引值就为其对应的ASCII码。
def lengthOfLongestSubstring(s):
n = len(s)
# 初始化数组
m = [-1]*256
# left = -1 方便判断
left, ans = -1, 0
for right in range(n):
left = max(left, m[ord(s[right])])
m[ord(s[right])] = right
ans = max(ans, right-left)
return ans
事实上只需要开辟128个空间就能满足本题需要,并将其改成习惯的前闭后开的取索引方式。
def lengthOfLongestSubstring(s):
n = len(s)
# 初始化数组
m = [0]*128
# 这里改成前闭后开的形式
# left=0
left, ans = 0, 0
for right in range(n):
i = ord(s[right])
left = max(left, m[i])
m[i] = right+1
ans = max(ans, right-left+1)
return ans
曾经沧海难为水,除却巫山不是云