LeetCode 第5题:最长回文子串(Python3解法)

1:问题描述

来源:LeetCode

难度:中等


问题详情:
给你一个字符串 s,找到 s 中最长的回文子串。

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。


2:问题分析

首先要明确什么叫回文子串。回文子串的定义是一个字符串中正序和反序一样的子串。比如“sabccbad”中就存在回文子串"abccba"

2.1 时间复杂度和空间复杂度

在真正开始介绍各种算法前,先以表格形式展示各自的时间复杂度和空间复杂度, n n n 表示字符串 s s s 的长度。

算法时间复杂度空间复杂度
暴力for循环O( n 3 n^3 n3)O( n n n)
动态规划O( n 2 n^2 n2) O ( n 2 ) O(n^2) O(n2)
中心扩展 O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)

2.2 暴力for循环

首先容易想到的就是是双层for循环遍历所有字符子串,然后判断该子串是否为回文子串。

代码较为简单,如下所示:

def longestPalindrome(s: str) -> str:
    """
    一个最直接的思路就是双层for循环遍历所有子串可能,然后再对比是否反向和正向是一样的
    缺点:面对超长字符串,容易超时

    :param s:
    :return:
    """
    length = len(s)

    max_len = 0
    result = ''
    for i in range(length):
        for j in range(i, length + 1):
            sub_str = s[i:j]
            rever_str = sub_str[::-1]
            # 因为这个判断的次数是n次,而两层for循环时间复杂度为O(n²),因此时间复杂度为O(n³)
            if sub_str == rever_str:
                if len(sub_str) > max_len:
                    result = sub_str
                    max_len = len(sub_str)

    return result

虽然该思路较为简单易懂,但是时间复杂度也是相当的高,两层for循环的时间复杂度O( n 2 n^2 n2),同时在for循环内部对比是否为回文子串的时间复杂度为O( n n n),因此整个算法的时间复杂度为O( n 3 n^3 n3)。

对于空间复杂度,因为需要存储字符子串,而子串的最大长度与原字符串相等,所以空间复杂度为O( n n n).

2.3 动态规划法

2.3.1 思路

看答案不难想到🐷,对于回文子串的处理可以使用动态规划的方法。动态规划法就是将大问题转换为小问题。

还以上边的“sabccbad”为例,大回文子串“abccba”去掉两边的“a”之后的”bccb“仍是一个回文子串。

因此可以得出这样的公式:

p ( i , j ) = p ( i + 1 , j − 1 ) ∧ s [ i ] = = s [ j ] p\left( i,j\right) =p\left( i+1,j-1\right) \wedge s\left[ i\right] ==s\left[ j\right] p(i,j)=p(i+1,j1)s[i]==s[j]

p ( i , j ) p\left( i,j\right) p(i,j)表示 s [ i : j + 1 ] s[i:j+1] s[i:j+1]子串是否为回文子串,这个公式表示:在满足两边字符相等的前提下,某一子串是否为回文子串取决于去掉两边后的子串是否为回文子串 ,如果条件都满足,则为True。如此,就将长子串是否回文的问题转换到了短子串是否回文的问题,这就是动态规划的思想。

2.3.2 边界问题

根据上面的公式,可以发现这个解法需要 s [ i ] s[i] s[i] s [ j ] s[j] s[j] 和一个子串 p ( i + 1 , j − 1 ) p(i+1, j-1) p(i+1,j1),而子串 p ( i + 1 , j − 1 ) p(i+1, j-1) p(i+1,j1)的长度至少为1,再加上 s [ i ] s[i] s[i] s [ j ] s[j] s[j] ,我们至少需要当前字符串长度至少为3,才能继续转换问题。因此该解法有两种边界问题:

  1. 长度为1的字符子串. p ( i , i ) = T r u e p(i,i)=True p(i,i)=True,单字符显然是回文的,比如”a“
  2. 长度为2的字符子串, p ( i , i + 1 ) = ( s [ i ] = = s [ j ] ) p(i,i+1)=(s[i]==s[j]) p(i,i+1)=(s[i]==s[j]),对于长度为2的字符子串,如果两个字符都相等,显然其也是回文的,比如"cc"

2.3.3 代码

代码如下:

def longestPalindrome2(s: str) -> str:
    """使用动态规划求解"""
    n = len(s)
    if n < 2:
        return s

    max_len = 1
    begin = 0
    # dp[i][j] 表示 s[i..j] 是否是回文串
    dp = [[False] * n for _ in range(n)]
    for i in range(n):
        dp[i][i] = True

    # 递推开始
    # 先枚举子串长度
    for L in range(2, n + 1):
        # 枚举左边界,左边界的上限设置可以宽松一些
        for i in range(n):
            # 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
            j = L + i - 1
            # 如果右边界越界,就可以退出当前循环
            if j >= n:
                break

            if s[i] != s[j]:
                dp[i][j] = False
            else:
                if L <= 3:
                    dp[i][j] = True
                else:
                    dp[i][j] = dp[i + 1][j - 1]

            # 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
            if dp[i][j] and L > max_len:
                max_len = L
                begin = i
    return s[begin:begin + max_len]

代码的流程为:

  • 使用 d p dp dp表示子串是否为回文,首先对所有长度为1的子串 d p [ i ] [ i ] = T r u e dp[i][i]=True dp[i][i]=True.
  • 其余的先默认为False
  • 然后从字符子串长度为2开始,依次递增至长度为 n n n
  • 如果两边字符相等,并且长度≤3,则直接赋值为True,因为长度为2时,很显然回文; 而长度为3时,因为两边字符相等,中间字符无论是什么,都是回文的
  • 如果两边字符相等,并且长度>3,则赋值为 d p [ i + 1 , j − 1 ] dp[i+1,j-1] dp[i+1,j1]
  • 记录最长回文子串的开始和长度,然后根据切片得到此子串

时间复杂度:因为两层for循环,都是 O ( n ) O(n) O(n)级别的,而for循环内部的语句都是 O ( 1 ) O(1) O(1),因此时间复杂度是 O ( n 2 ) O(n^2) O(n2).

空间复杂度:因为需要dp存储各子串是否回文,因此空间复杂度是 O ( n 2 ) O(n^2) O(n2).

2.4 中心扩展

2.4.1 思路

在动态规划法中已经体现了该解题方法的思路,如果一个回文子串左右两边加上同样的字符,那么新的子串同样是回文的

基于此思想,就有了中心扩展方法。

  1. 先从一个回文中心开始,回文中心是一个回文的字符子串。比如”a“开始。
  2. 两边的字符如果相等,假设为”c“,那么就扩展到了另一个回文子串”cac“
  3. 继续以上步骤,直至两边字符不再相等就停止,最后一个满足条件的子串就是以”a“为回文中心能够扩展的最长回文子串

上面步骤提到回文中心,结合2.2动态规划法中的边界问题,可以想到这里的回文中心也有两种:

  1. 长度为1的回文中心
  2. 长度为2的回文中心,很显然长度为2的字符子串无法以长度为1的回文中心扩展得到,因此需要考虑长度为2的情况

2.4.2 代码

代码如下所示:

def longestPalindrome3(s: str) -> str:
    def expand(s, i, j):
        """向回文中心两边扩展,如果两边的字符相等,就可以扩展,否则终止"""
        while i >= 0 and j < len(s) and s[i] == s[j]:
            i -= 1
            j += 1

        # 因为终止的时候是超界限或者不满足相等条件了,所以再通过+1,-1指向最后一个
        return i + 1, j - 1

    start = end = 0
    for i in range(len(s)):
        # 将回文中心分为长度为1和长度为2的两种形式;
        # ‘acddca’虽然是一个回文字符,但是通过某一个字符为中心,却无法扩展到整个字符串
        # 因此,需要以长度为2的'dd'为中心,向两边扩展。这就是下边两行的意思。
        start1, end1 = expand(s, i, i)
        start2, end2 = expand(s, i, i + 1)

        if end1 - start1 > end - start:
            start = start1
            end = end1
        if end2 - start2 > end - start:
            start = start2
            end = end2

    return s[start:end + 1]

对于时间复杂度:for循环的时间复杂度为 O ( n ) O(n) O(n),而expand函数(用于扩展子串的函数)时间复杂度为 O ( n ) O(n) O(n),因此总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

对于空间复杂度,则为 O ( 1 ) O(1) O(1)

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little student

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值