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 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]

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

你可能感兴趣的:(5. 最长回文子串(Python))