无重复字符的最长子串

无重复字符的最长子串

无重复字符的最长子串题目是LeetCode的第三题,描述如下:

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

示例 2:

输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。

示例 3:

输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

分析

准确的分析题目的问题,才是解决问题的关键。

  1. 问题:给定一个字符串,请你找出其中不含有重复字符最长子串长度
  2. 关键点:输入是字符串(String),输出是长度(Number),代码逻辑是找出不含重复字符的最长子串。因为输出的是长度,那么在算法过程中就只需要比较长度大小就可以。
  3. 注意:子串与子序列不同,子串是必须连续的,区别见实例3的解释。

思路

1. 蛮力法

我们先使用蛮力法的来思考这个问题,先找到所有的子串,再从子串中找到满足条件的最长子串。以"abcb"为例来分析,思路如下:

  1. 刚刚分解出三个条件,先从容易的子串开始,会分解为"a"、"b"、"c"、"b"、"ab"、"bc"、"cb"、"acb"、"bcb"、"abcb"
  2. 去掉重复的子串含重复字符的子串,就剩下"a"、"b"、"c"、"ab"、"bc"、"cb"、"acb"
  3. 最后找到最长的子串"abc",返回长度3

在这个思路中,长度为n的字符串会分解成 n + (n - 1) + (n - 2) + ... + 1 = (n + 1) * n / 2 个子串。 在去掉含有重复字符的子串时,循环的次数(子串的数量 * 子串的长度)大大的增加。总的来说,蛮力法不仅要使用更多的空间来存储中间的变量,时间复杂度也会很高,代码实现起来也很麻烦。

2. 滑动窗口算法

蛮力法的特点在于遍历所有满足要求的结果集合,从中找到得到符合的结果。那有没有办法不用遍历所有的子串也能找到题目要求的结果呢?

当然是有的,这个方法就是滑动窗口算法,此算法通过改变起止下标方式在列表上寻找符合条件子列表。把子列表看成是一个窗口,当起止下标同时变大同样大小,那这个窗口就像在滑动;当起止下标改变不一样的时候,窗口不仅在滑动,窗口的大小也在改变。

使用条件

使用这个算法需要同时满足两个条件:

  1. 线性数据结构
  2. 结果来自于连续的子序列

本题的问题寻找字符串的最长子串,字符串中的字母可以通过下标的方式取得,因此字符串可以看成是数组或链表,满足第一个条件;子串是连续的字符,正好也满足第二个要求(如果问题是最长子序列,那动态规划就是比较好的方案了)。

只要满足上面两个条件,就可以使用滑动窗口的算法了。但是本题的话,还需要注意的就是窗口的大小。由于不知道最长子串具体的长度,那窗口的大小就是动态变化的。在编写代码的时候,尤其要注意窗口大小变化的条件,这也是最容易犯错的地方。

应用场景

在leetcode中出现的题目,也可以使用滑动窗口算法解决的题目有:3. 无重复字符的最长子串、219.存在重复元素II、209.长度最小的子数组、438. 找到字符串中所有字母异位词。当然还有很多别的题目也可以使用这个算法,就不一一列举了。

另外,学过网络的同学,应该知道TCP的流量控制中也有滑动窗口算法的使用,有兴趣的同学可以点击了解一下

代码实现

滑动窗口实现(固定大小窗口)

由于我们并不知道满足条件的最长子串的长度是多少,所以无法确定窗口的大小。既然需要求解的是最大值,我们可以将窗口设置成输入字符串的长度,找到第一个满足所有字符只出现过一次的子串。

逻辑如下:

  1. 如果字符串的长度为1或者是0,直接返回它的长度;
  2. 用一个循环来控制窗口的大小,每一次循环完就减少窗口大小1;
  3. 在字符串上截取窗口大小的子串;
  4. 从子串的第一个字符,一直到最后一个字符,判断是否该字符出现的次数。如果出现次数大于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

  1. 读取字符s.index(i),判断s.index(i)是否存在于subStr中,如果存在转2,否则转3;
  2. s.index(i)添加到subStr末尾;
  3. subStr截取自出现重复字符下标的下一位直到子串的末尾,执行2,比如说当前子串是"pw",读取到"w",则是将"pw"截取w的下一位;
  4. 重复操作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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值