★leetcode05_Longest Palindromic Substring

一.问题描述

Longest Palindromic Substring

Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.

回文字符串,eg:

“abba”或“abcba”

二.编写代码

回文字符串是以最中间字符为中心向两边对称的,因此考虑如下算法:对每个字符串,寻找以其为中心的最长回文子串。该算法时间复杂度为O(N^2)。算法实现中需要注意的是两种回文情况要分别考虑到。

代码如下:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        # 长度
        len_s = len(s)
        # 两个指针初始化
        # i=0
        # j=len_s-1

        # 初始化最长回文字符串机器长度
        max_str = s[0]
        max_str_len = 1

        # 奇数长度的回文串
        for i in range(0, len_s - 1):
            # 对每个i,寻找以它为中心的最长回文串
            len1 = 1
            j = i + 1
            i = i - 1
            while i >= 0 and j < len_s:
                if s[i] == s[j]:
                    len1 = len1 + 2
                    i = i - 1
                    j = j + 1
                else:
                    break
            if len1 > max_str_len:
                max_str_len = len1
                max_str = s[i + 1:j]

        # 偶数长度的回文串
        for i in range(0, len_s-1):
            # 对每个i,寻找以它为中心的最长回文串
            len1 = 1
            if s[i] == s[i + 1]:
                len1 = len1 + 1
                i = i - 1
                j = i + 3
                while i >= 0 and j < len_s:
                    if s[i] == s[j]:
                        len1 = len1 + 2
                        i = i - 1
                        j = j + 1
                    else:
                        break
                if len1 > max_str_len:
                    max_str_len = len1
                    max_str = s[i + 1:j]
            elif i>1 and s[i] == s[i - 1]:
                len1 = len1 + 1
                i = i - 2
                j = i + 3
                while i >= 0 and j < len_s:
                    if s[i] == s[j]:
                        len1 = len1 + 2
                        i = i - 1
                        j = j - 1
                    else:
                        break
                if len1 > max_str_len:
                    max_str_len = len1
                    max_str = s[i + 1:j]
        return max_str
                
        
注:该问题还可以用暴力解法,即寻找出该字符串的所有子串,判断其是否为回文串,并得出最大回文子串长度。对于一个长度为n的字符串,共有n^2个子串,这些子串的平均长度为n/2,因此该解法的时间复杂度是O(N^3)。

三.最长回文子串--Manacher算法

注:以下引用/总结自https://segmentfault.com/a/1190000003914228  【这篇讲得很好啦】
既然是对上述算法的改进,那么我们先来看看其存在的缺陷:
1)由于回文串的奇偶性造成了不同的对称轴位置,要队两种情况分别处理;
2)很多子串被重复多次访问。
eg:
char: a b a b a
  i : 0 1 2 3 4

当i==1和i==2时,左边的子串aba被重复访问了。

针对上述问题的分别改进:
1)解决长度奇偶性带来的对称轴位置问题
Manacher算法首先对字符串做一个预处理,即在所有空隙位置(包括首尾)插入同样的未在原串中出现的符号。该预处理能做到在将所有子串变成奇数长度同时不影响原字符串中子串的回文性。
aba  ———>  #a#b#a#
abba ———>  #a#b#b#a#

2)解决重复访问的问题
首先,定义一个回文半径RL为回文串中最左或最右的字符到对称轴的距离(该距离是将插入的符号算在内的),用RL[i]表示以第i个字符为对称轴的回文串的回文半径。
char:    # a # b # a #
 RL :    1 2 1 4 1 2 1
RL-1:    0 1 0 3 0 1 0
  i :    0 1 2 3 4 5 6

char:    # a # b # b # a #
 RL :    1 2 1 2 5 2 1 2 1
RL-1:    0 1 0 1 4 1 0 1 0
  i :    0 1 2 3 4 5 6 7 8
可以发现RL[i]-1对应为该回文子串的长度。于是问题变成了, 怎样高效地求的RL数组。 基本思路是利用 回文串的对称性,扩展回文串

我们再引入一个辅助变量MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight对应的回文串的对称轴所在的位置,记为pos,它们的位置关系如下。

我们从左往右地访问字符串来求RL,假设当前访问到的位置为i,即要求RL[i],在对应上图,i必然是在po右边的(obviously)。但我们更关注的是,i是在MaxRight的左边还是右边。我们分情况来讨论。

1)当iMaxRight的左边

情况1)可以用下图来刻画:

我们知道,图中两个红色块之间(包括红色块)的串是回文的;并且以i为对称轴的回文串,是与红色块间的回文串有所重叠的。我们找到i关于pos的对称位置j,这个j对应的RL[j]我们是已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分的情况。

  1. j为对称轴的回文串比较短,短到像下图这样。

这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i为中心的回文串,于是可以令RL[i]=RL[j]。但是以i为对称轴的回文串可能实际上更长,因此我们试着以i为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。

  1. j为对称轴的回文串很长,这么长:

这时,我们只能确定,两条蓝线之间的部分(即不超过MaxRight的部分)是回文的,于是从这个长度开始,尝试以i为中心向左右两边扩展,,直到左右两边字符不同,或者到达边界。

不论以上哪种情况,之后都要尝试更新MaxRightpos,因为有可能得到更大的MaxRight。

具体操作如下:

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以i为中心扩展回文串,直到左右两边字符不同,或者到达边界。
step 3: 更新MaxRight和pos
2)当iMaxRight的右边

遇到这种情况,说明以i为对称轴的回文串还没有任何一个部分被访问过,于是只能从i的左右两边开始尝试扩展了,当左右两边字符不同,或者到达字符串边界时停止。然后更新MaxRightpos

本人实现代码如下:(注:代码还可优化,没必要分这么多情况,比如最左边界只用取最小值即可)


class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        # 预处理,插入特殊字符
        s = '#' + '#'.join(s) + '#'
        # 长度
        len_s = len(s)
        # 回文半径初始化
        RL = [0] * len_s
        # 当前最靠右的回文子串的相关信息,left和right关于pos对称
        left = 0
        right = 0
        pos = 0
        # 初始化最大长度回文子串
        max_str = s[1]
        max_str_len = 1
        # 依次求RL[i]
        for i in range(len_s):
            # step1:求RL[i]
            # 第一种情况:i在right的右边,即其相应的回文子串并没有被访问过
            if i >= right:
                # 定义start为继续扩展回文子串的地方(关于i,从start开始向左)
                RL[i]=1
                start = i - 1
            else:
                # i关于pos的对称位置j
                j = 2 * pos - i if pos != 0 else 0
                # 第二种情况:j及其回文串均在left到right内
                if (j - RL[j]) >= left:
                    RL[i] = RL[j]
                    start = i - RL[i]
                # 第三种情况:j在left到right内,但其回文子串已经超出该范围
                else:
                    RL[i] = right - i + 1
                    start = 2 * i - right - 1
            while start >= 0 and (2 * i - start) < len_s and s[start] == s[2 * i - start]:
                RL[i] += 1
                start=start-1
            # step2:更新最靠右回文子串的相关信息
            if (i + RL[i] - 1) > right:
                right = i + RL[i] - 1
                pos = i
                left = 2 * pos - right
            # 更新最长回文子串信息
            if (RL[i] - 1) > max_str_len:
                max_str_len = RL[i] - 1
                max_str = s[i - RL[i] + 1:i + RL[i]]
        return max_str.replace('#', '')
算法复杂度分析:
空间复杂度:插入分隔符形成新串,占用了线性的空间大小;RL数组也占用线性大小的空间,因此空间复杂度是线性的。
时间复杂度:尽管代码里面有两层循环,通过amortized analysis我们可以得出,Manacher的时间复杂度是线性的。由于内层的循环只对尚未匹配的部分进行,因此对于每一个字符而言,只会进行一次,因此时间复杂度是 O(n)
关于时间复杂度:最简单的理解是对于字符串中的n个字符,每个字符最多被访问两次,则时间复杂度必然为O(N)。
更严谨的关于时间复杂度的分析证明.......请自行百度

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值