5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:
输入:s = "cbbd"
输出:"bb"

第一种方法

自己未看答案前的做法,也是最容易想到的一种做法。简单来说:就是列举出所有可能的子串,并判断是否是回文子串。但是没有通过力扣系统的时间要求,代码如下:

def longestPalindrome1(s):
    i = 0
    j = i + 1
    # 用来记录最长的回文子串
    palStr = ""
    while i < len(s):

        while j <= len(s):
            # 验证是否是回文子串
            if s[i:j] == s[i:j][::-1] and len(palStr) < len(s[i:j]):
                palStr = s[i:j]
            j += 1

        i += 1
        j = i + 1
    return palStr

第二种方法

自己想的一种方法,经多次完善终于通过了力扣系统。但是时间复杂度还是比较高,只是比第一种方法好一点,也能通过力扣系统的时间要求。

主要思想:一个字符串如果是回文字符串,那么它和它的逆序字符串对应位置的相同角标必然字符相等。但是针对一些比较特别的字符串比如:"abcdbbfcba"就会得不到正确答案,这时只有当判断到有一连串相等字符的时候,就判断这串字符是否是回文子串(就主要是这步增加了时间复杂度)。

代码如下,也有详细的注释:

def longestPalindrome2(s):
    # 用来记录最长的回文子串
    palStr = ""
    # 将s逆序的字符串
    reversedStr = s[::-1]
    # 字符串长度
    length = len(s)

    def getPalindromeStr(s1, reversedStr1):
        # 如果传进来的直接就是一个回文字符串,就直接返回
        if s1 == reversedStr1:
            palStr = s1
            return  palStr
        else:
            # 用来记录最终回文子串的开始角标和结束角标
            start, end = 0, 0
            # 用来记录每次遍历的时候子串相等的第一个角标和最后一个角标
            left, right = 0, 0
            # 用来记录上一个字符是否相等,默认为False
            preEqual = False
            for j in range(length - i):
                if s1[j] == reversedStr1[j]:
                    if preEqual:
                        # 如果上一个字符相等,并且这个字符也相等,就将子串相等的右角标移到现在这个字符
                        right = j
                        # 如果这次相等的子串长度大于原来记录的子串长度,并且子串是回文子串
                        # 就更新最终回文子串的开始和结束角标
                        if (right - left) > (end - start) and s1[left:right + 1] == s1[left:right + 1][::-1]:
                            start = left
                            end = right
                    else:
                        # 如果上一个字符不相等,就将子串相等的左角标移到现在这个位置
                        # 并且将preEqual设置为True
                        left = j
                        preEqual = True
                else:
                    # 如果字符不相等,preEqual就为False
                    preEqual = False

            # 返回最终记录的回文子串
            return s1[start:end + 1]

    for i in range(length):
        """
        第一次遍历: 传 cbbd和dbbc进去,如果是回文子串,直接返回
        第二次遍历:传 cbb和bbc bbd和dbb 两组数据进去
        第三次遍历:传 cb和bc   bd和db   两组数据进去
        ....
        
        如果是回文子串,传进去的正序子串和逆序子串,对应的角标至少相等。
        如果出现连续的角标都相等,那么它可能就是回文子串。
        再判断它是否是回文子串。
        
        cbbd |     cbb[d] |  [c]bbd
        dbbc |  [d]bbc    |     dbb[c]
             |            |
             |     cb[bd] | [cb]bd
             | [db]bc     |     db[bc]
        """
        # 将字符串在逆序字符串上方分别每次向左和向右移动一个字符,
        pal1 = getPalindromeStr(s[:length-i], reversedStr[I:])
        pal2 = getPalindromeStr(s[i:], reversedStr[:length-i])
        p = pal1 if len(pal1) > len(pal2) else pal2
        if len(p) > len(palStr):
            palStr = p

    return palStr

第三种方法

看了力扣官方答案过后的写法,主要是利用动态规划的思想。

主要思想:对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。具有转态转移的性质,每一步计算都尽量用到了以前的计算结果,这也是典型的空间换时间的算法思想。

就可以得到以下结论:

  • 状态:dp[i][j]表示子串s[i..j]是否为回文子串;
  • 得到状态转移方程:dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
    即是说一个字符串如果是回文,那么它首尾的字符一定相等,并且去掉首尾的子串也是一个回文。
  • 边界条件:(j - 1) - (i + 1) + 1 < 2 , 整理得:j - i < 3 <==> j - i + 1 < 4
    边界就是i+1j-1不能构成区间,也就是长度严格小于2的情况下,dp[i + 1][j - 1]没有意义。j - i + 1 < 4的语义就是当s[i..j]的长度为2或者3的时候,就不用检查子串是否是回文,这时不需要状态转移。
  • 初始化:dp[i][i] = true
    dp[i][i]表示对角线上的子串,其实也就是每个单个字符,它一定是回文。其实对角线上的值是不会被其他的状态值所参考。
  • 输出:在得到一个状态的值为true的时候,记录起始位置和长度,填表完成以后再截取
状态规划表.png

注意:需要先升序填列保证左下方的值先被算出来,再升序填行,这样就可以参考左下方的值了。

具体代码如下,附有详细注释:

def longestPalindrome3(s):
    # 记录字符串长度
    length = len(s)
    # 如果字符串长度为1,那它本身就是回文
    if length < 1:
        return s

    # 用来记录最大回文子串长度,默认为1
    maxlength = 1
    # 用来记录最大回文子串的开始位置,默认为0
    begin = 0

    # dp[i][j]表示子串s[i..j]是否为回文字符串
    # 初始化二维表格
    dp = [[False] * length for _ in range(length)]
    for i in range(length):
        # 对角线值初始化为true
        dp[i][i] = True

    # 因为要先填左下角,也就是先升序填列
    # 所以先从j = 1,开始填
    j = 1
    while j < length:

        # 因为对角线已经赋值了,只需要填对角线上方的值即可
        # 所以i从0到j-1就好了
        i = 0
        while i < j:
            # 一般将容易的条件写在前面
            # 当字符串首尾不相等时,那么它就不是回文
            if s[i] != s[j]:
                dp[i][j] = False
            else:
                # 当首尾字符相等的时候,如果去掉首尾后,没有字符了,或者只剩下一个字符,
                # 也就是此时整个字符串长度为2或者3时,那它一定是回文
                if j - i < 3:
                    dp[i][j] = True
                else:
                    # 如果整个字符串长度大于3时,就需要参考中间的子串是否是回文
                    # 也就是填表时需要参考左下角的值,这也就是一个状态转移的过程
                    dp[i][j] = dp[i + 1][j - 1]

            # 只要dp[i][j] == true成立,表示子串s[i..j]是为回文字符串
            # 如果子串s[i..j]长度大于记录的最长的回文子串长度,就重新记录回文的起始位置和长度
            if dp[i][j] and j - i + 1 > maxlength:
                maxlength = j - i + 1
                begin = i

            i += 1

        j += 1

    # 最后返回最长的回文子串
    return s[begin:begin+maxlength]
复杂度分析
  • 时间复杂度:O(n^2),其中 n 是字符串的长度。动态规划的状态总数为 O(n^2),对于每个状态,我们需要转移的时间为 O(1)。
  • 空间复杂度:O(n^2),即存储动态规划状态需要的空间。

第四种方法

看了力扣官方答案过后的写法,采用了中心扩展算法。

此方法的本质是:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。

中心扩散法

代码如下:

def expandAroundCenter(s, left, right):
    """
    根据传进来的挨着的两个角标为中心,在字符串s中尽可能向左右扩展回文子串
    """
    # 边界为:left和right不能越界
    # 扩展条件为:s[left] == s[right] cbbd
    while left >= 0 and right < len(s) and s[left] == s[right]:
        left -= 1
        right += 1
    # 因为当退出循环的时候,s[left] != s[right]
    # 也就是说此时除去首尾字符,中间的子串才是回文字符串,所以返回(left + 1, right - 1)
    return left + 1, right - 1


def longestPalindrome4(s):
    # 记录字符串长度
    length = len(s)

    # 用来记录最长回文子串的开始和结束位置
    start, end = 0, 0
    # 枚举回文中心的位置,最后一个位置没有必要枚举了,因为它不可能再向右扩展了
    for i in range(length-1):
        # 以i为中心的,最长奇数回文子串的开始和结束位置
        # 在expandAroundCenter函数中left和right位置都传i,表示以i为中心
        odd_start, odd_end = expandAroundCenter(s, i, i)

        # 以i和i+1为中心的,最长偶数回文子串的开始和结束位置
        even_start, even_end = expandAroundCenter(s, i, i+1)

        # 记录最长的回文子串的开始和结束位置
        if odd_end - odd_start > end - start:
            start, end = odd_start, odd_end
        if even_end - even_start > end - start:
            start, end = even_start, even_end

    # 返回最长的回文子串
    return s[start: end + 1]
复杂度分析
  • 时间复杂度:O(n^2),枚举回文中心位置的个数是2(n-1),奇数和偶数回文子串情况分别是n-1个,每个回文中心最多会向外扩展O(n)次 。
  • 空间复杂度:O(1),只用到常数个临时变量。

力扣官方答案传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容