最长不含重复字符的子字符串

最长不含重复字符的子字符串

题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含’a’~'z’的字符。例如,在字符串"arabcacfr"中,最长的不含重复字符的子字符串就是"acfr",长度为4。

采用动态规划的思想解决这个问题,因为i位置的最长子字符串明显仅与i-1的最长子字符串长度有关,因此递推公式可以表示为:
f ( i ) = g ( f ( i − 1 ) ) f(i) = g(f(i - 1)) f(i)=g(f(i1))
但f(i - 1)不能表示为在i-1位置时的最长子字符串长度,因为这样的话不知道这个最长子字符串的起始位置,不方便加入第i个字符后的比较。

因此比较合适的表示方式是将f(i - 1)表示为以i-1个字符为结尾的最长子字符串长度,这样在加入第i个字符进行比较时,可以很方便的跟i-1位置的状态进行比较,因为可以很方便的得到f(i - 1)所代表的具体字符串。这样的表示方法带来的好处是最长的子字符串一定在某个位置出现过(不一定是f(i)),并且可以得到最长子字符串的具体表示。

具体的递推逻辑为:

  • 如果strs[i]没有在之前的字符串中出现过, f(i) = f(i - 1) + 1
  • 如果出现过,那么需要确定其最后出现的位置,分为两种情况:
    • 如果最后出现的位置在f(i - 1)的范围内, 那么其最长子串长度就是两个重复字符之间的长度;
    • 如果最后出现的位置在f(i - 1)的范围外, 那么其最长子串的长度仍然是f(i) = f(i - 1) + 1。

根据这个逻辑,很容易写出代码:

def find_longest_substring_without_duplication(strs):
    #  递推公式为:f(i) = g(f(i - 1)) + 1, 如果知道了f(i), 那么既可以得到最长子串的长度, 也可以知道最长子串的具体表达
    #  如果strs[i]没有在之前的字符串中出现过, f(i) = f(i - 1) + 1
    #  如果出现过, 那么需要确定其最后出现的位置, 如果最后出现的位置在f(i - 1)的范围内, 那么其最长子串长度就是两个重复字符之间的长度;
    #  如果最后出现的位置在f(i - 1)的范围外, 那么其最长子串的长度仍然是f(i) = f(i - 1) + 1
    if len(strs) == 1:
        return strs

    longest_substring_length = [1 for i in range(len(strs))]
    for i in range(1, len(strs)):
        idx = strs[:i].rfind(strs[i])
        if idx == -1 or idx < i - longest_substring_length[i - 1]:
            longest_substring_length[i] = longest_substring_length[i - 1] + 1
        else:
            longest_substring_length[i] = i - idx
    return longest_substring_length

代码这样写的话,遍历时间复杂度为O(n),rfind查找的时间复杂度为O(n),总时间复杂度为O(n^2),空间复杂度为O(n)。优点是能方便的获得最长子字符串的长度及具体表示。

从时间复杂度和空间复杂度来看都有优化的空间:

  1. 时间复杂度能优化的点在查找,查找的目的是为了找到该字符最后一次出现的位置,可以用hash来替代,这样时间复杂度就降为了O(n),且空间复杂度并不会随之增加;
  2. 空间复杂度能优化的点在f(i)的状态只与f(i - 1)有关,因此没必要保存前面的状态,用两个变量来记录max_length(上面代码中的max(longest_substring_length))和cur_length(f(i)),再加一个max_idx来表示最后一次改变max_length的位置,方便得出最长子字符串的具体表示。

改进后的代码如下所示:

def find_longest_substring_without_duplication(strs):
    #  递推公式为:f(i) = g(f(i - 1)) + 1, 如果知道了f(i), 那么既可以得到最长子串的长度, 也可以知道最长子串的具体表达
    #  如果strs[i]没有在之前的字符串中出现过, f(i) = f(i - 1) + 1
    #  如果出现过, 那么需要确定其最后出现的位置, 如果最后出现的位置在f(i - 1)的范围内, 那么其最长子串长度就是两个重复字符之间的长度;
    #  如果最后出现的位置在f(i - 1)的范围外, 那么其最长子串的长度仍然是f(i) = f(i - 1) + 1
    if len(strs) == 1:
        return strs

    position = [-1 for i in range(26)]  # -1 代表没出现过
    max_length = 0
    cur_length = 0
    max_idx = -1
    for i in range(len(strs)):
        idx = position[ord(strs[i]) - ord('a')]
        if idx == -1 or idx < i - cur_length:
            cur_length = cur_length + 1
        else:
            if cur_length >= max_length:
                max_length = cur_length
                max_idx = i
            cur_length = i - idx

        if cur_length >= max_length:
            max_length = cur_length
            max_idx = i

        position[ord(strs[i]) - ord('a')] = i
    return max_length, max_idx

通过这样的优化将时间复杂度优化到O(n),空间复杂度优化到O(1)。

无论是位置还是状态,优化的点其实都是只需要最后出现的值,因此用变量来替代就可以了,以后编程得想到了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值