LeetCode 3 无重复字符的最长子串 Python3的几种解法

原题:链接
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
示例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索引的字符是否存在于当前的串中即可,而无需重新判断ij+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

曾经沧海难为水,除却巫山不是云

发布了7 篇原创文章 · 获赞 12 · 访问量 2712

猜你喜欢

转载自blog.csdn.net/weixin_37538742/article/details/103963941