5. 最长回文子串(Python)

题目

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:

输入: "cbbd"
输出: "bb"

解答

方案1:暴力求解

遍历每一个子串,构建回文串判定函数(is_palindromic_string),用于判定每个子串是否为回文串,随时更新当前最大回文子串(max_substr)及其长度(max_len)。

class Solution:
    def longestPalindrome(self, s):
        is_palindromic_string = lambda s: s == s[::-1]  # 回文串判定函数
        max_len, max_substr = 0, ''

        for i in range(len(s)):
            for j in range(i+1, len(s)+1):
                cur_substr = s[i:j]                     # 取当前子串
                print(cur_substr)
                if is_palindromic_string(cur_substr):
                    cur_len = len(cur_substr)           # 当前子串长度
                    if cur_len > max_len:
                        max_len = cur_len
                        max_substr = cur_substr         # 当前最长子串
        return max_substr

算法的时间复杂度为O(N^3),果然,时间超过限制。

方案2:动态规划

这里,我们采用以空间换时间的动态规划策略,网上很多案例晦涩难懂,我们这里稍作改造。设我们的输入字符串为s:
1.构造二维矩阵dp,维度为len(s)*len(s),dp[left][right]表示子串s[left:right+1]是否为回文串,left和right均为下标索引,范围在0~len(s)之间,且left<right,因此dp矩阵中的每一个变量均为布尔量;
2.构造状态转移方程,考虑三种情况,
(1)left = right,当前子串只有一个字符,一定为回文串;
(2)right = left + 1,当前子串只有两个字符,判断这两个字符是否相等;
(3)right > left + 1,当前子串有多个字符,判断根据首位字符是否相等以及除去首位字符剩余子串是否为回文串判定当前子串。
状态转移方程为:
dp\left [ left \right ]\left [right \right ]= \left\{\begin{matrix}True, \qquad \qquad \qquad\qquad\qquad \qquad \qquad \qquad \qquad \qquad \qquad left=right; \\ str\left [ left \right ]==str\left [ right \right ], \qquad \quad \qquad \qquad \qquad \qquad \qquad right-left=1; \\ str\left [ left \right ]==str\left [ right \right ] \& dp \left[ left+1 \right ]\left [right-1 \right ],\qquad right-left>1; \end{matrix}\right.
或者把前两种情况归类:
dp\left [ left \right ]\left [right \right ]= \left\{\begin{matrix}str\left [ left \right ]==str\left [ right \right ], \quad \quad \qquad \qquad \qquad \qquad \qquad right-left<=1; \\ str\left [ left \right ]==str\left [ right \right ] \& dp \left[ left+1 \right ]\left [right-1 \right ],\qquad right-left>1; \end{matrix}\right.

class Solution:
    def longestPalindrome(self, s):

        dp = [[False]*len(s) for _ in range(len(s))]
        max_start, max_len = 0, 0                       # 最长回文子串开始位置及长度
        for right in range(len(s)):                     # 右指针先走
            for left in range(right+1):                 # 左指针跟着右指针
                if right - left < 2:                    # 前两种情况
                    dp[left][right] = (s[left] == s[right])
                else:                                   # 最后一种情况
                    dp[left][right] = (s[left] == s[right]) and dp[left+1][right-1]
                # cur_substr = s[left:right+1]          # 当前考察的子串
                cur_len = right + 1 - left              # 当前子串长度为 right + 1 - left
                if dp[left][right] and max_len < cur_len:

                    max_start = left
                    max_len = cur_len

        return s[max_start:max_start + max_len]

时间和空间复杂度O(N^2),通过了验证,但是这个程序还是比较慢的:
执行用时 : 8980 ms, 击败了7.80% 的用户
内存消耗 : 20.7 MB, 中击败了17.62% 的用户

方案3:中心扩展

回文串有两个特点:
1.当回文串的左侧和右侧同时增加相同的字符时,构成的字符串也是回文串;
2.只有一个字符的字符串本身也是回文串;

根据这个特点,我们从头到尾遍历字符串,并且每遍历到一个位置时,我们都对这个位置的字符进行左右扩展,如果左右两头的元素相同,又可以构成新的回文子串,循环执行下去,直到左右两端元素不相同了为止,在此过程中,记录下每次扩展得到的最长子串起始位置和长度。需要注意的是,根据回文子串元素个数,我们需要考虑两种情况,一种是奇数个,一种是偶数个,这里我们用左右扩展的起始下标进行区分。

class Solution:
    def longestPalindrome(self, s):
        def extend(start_idx, end_idx, max_start, max_len):
            """
            扩展函数,用于得到向左向右同步扩展后的最长回文子串
            :param start_idx: 向左扩展的起始位置
            :param end_idx: 向右扩展的起始位置
            :param max_start: 当前最长回文子串的左指针
            :param max_len: 当前最大长度
            :return: 当前最长回文子串的起始位置和长度
            """

            while 0 <= start_idx <= end_idx < len(s):   # 子串起止下标合法时
                if s[start_idx] == s[end_idx]:          # 如果新增的两端字符相等
                    cur_str = s[start_idx:end_idx]      # 当前子串是回文串
                    cur_len = end_idx + 1 - start_idx   # 当前子串长度
                    if max_len < cur_len:
                        max_len = cur_len
                        max_start = start_idx
                    start_idx -= 1                      # 左指针向左移动一位
                    end_idx += 1                        # 右指针向右移动一位
                else:
                    break
            return max_start, max_len

        max_start, max_len = 0, 0                       # 初始化最长回文子串开始位置及长度

        for i in range(len(s)):                         # 进行一次遍历
            left = right = i                            # 判断长度为奇数的回文子串开始和结束位置
            # 从i位置向两边扩展,搜寻可以得到的最大回文子串
            max_start, max_len = extend(left, right, max_start, max_len)

            left, right = i, i+1                        # 判断长度为偶数的回文子串开始和结束位置
            # 从i位置向左扩展,从i+1位置向右扩展,搜寻可以得到的最大回文子串
            max_start, max_len = extend(left, right, max_start, max_len)

        return s[max_start:max_start + max_len]

网上广为流传的还有一个办法,遍历字符串中每一个字符,主要考虑两种情况:

  1. 向左和向右各扩展一个字符,查看构成的新子串是否为回文串;
  2. 只向右扩展一个字符,查看构成的新子串是否为会问串。
    这种方法要更加快些。
class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        is_palindromic_string = lambda s: s == s[::-1]

        str_length = len(s)
        cur_length = 0
        start_index = 0
        for i in range(str_length):

            cur_start_index = i - cur_length - 1                                # 向左扩展一个字符
            cur_substr = s[cur_start_index: cur_start_index+cur_length+2]       # 向右扩展一个字符,获得子串

            if cur_start_index >= 0 and is_palindromic_string(cur_substr):      # 判断扩展后的子串是否回文
                start_index = cur_start_index                                   # 更新子串起始下标
                cur_length += 2                                                 # 更新当前最长子串长度

            else:
                cur_start_index = i - cur_length                                # 向左不扩展
                cur_substr = s[cur_start_index: cur_start_index+cur_length+1]   # 向右扩展一个字符,获得子串
                if cur_start_index >= 0 and is_palindromic_string(cur_substr):  # 判断扩展后的子串是否回文
                    start_index = cur_start_index                               # 更新子串起始下标
                    cur_length += 1                                             # 更新当前最长子串长度

        return s[start_index: start_index + cur_length]

其实还有疑问,为什么当两边扩展后就无需单边扩展,以及为何起始下标要这样设置,懂的大佬请在评论中留言。。
执行用时 : 92 ms, 击败了96.52% 的用户
内存消耗 : 13 MB, 击败了94.22% 的用户

方案4:Manacher's Algorithm(马拉车算法)

我们可以看到上面需要考虑子串是长度为奇数还是偶数两种情况,如何可以统一起来?马拉车算法应运而生。我们在输入字符用“#”隔开,且首位也添加“#”号,如“cbbcd”变成“#c#b#b#c#d#”,这样就可以确保字符串的长度为奇数了。

class Solution:
    def longestPalindrome(self, s):
        def get_length(string, index):
            # 循环求出index为中心的最长回文字串
            start, length = index // 2, 0
            r_ = len(string)
            for i in range(1, index + 1):
                if index + i < r_ and string[index - i] == string[index + i]:
                    start = (index - i) // 2
                    length += 1
                else:
                    break
            return start, length

        s_list = [c for c in s]
        new_s = '#' + '#'.join(s_list) + '#'        # 形成新串

        max_start = max_length = 0
        length = len(new_s)
        for index in range(0, length):
            start, r_length = get_length(new_s, index)
            if max_length < r_length:
                max_start, max_length = start, r_length
        return s[max_start: max_start+max_length]

如有疑问,欢迎评论区留言~

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