在 Python 中查找字符串中最长的回文

Posted

技术标签:

【中文标题】在 Python 中查找字符串中最长的回文【英文标题】:Finding the longest palindrome within a string in Python 【发布时间】:2017-11-12 21:39:34 【问题描述】:

我正在尝试解决 LeetCode 上的 Longest Palindromic Substring 问题。问题陈述是:

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

例子:

Input: "babad"

Output: "bab"

注意:“aba”也是一个有效的答案。 示例:

Input: "cbbd"

Output: "bb"

我想出了以下解决方案(包括一些测试用例):

import pytest

class Solution:
    def longestPalindrome(self, s):
        candidate = ""
        longest = ""
        contains_palindrome = False
        for i, char in enumerate(s):
            if i == 0:
                candidate = char
            elif i == 1:
                if s[1] == s[0]:
                    candidate = self.get_palindrome(s, start=0, end=1)
            elif i >= 2:
                if char == s[i-1]:
                    candidate = self.get_palindrome(s, start=i-1, end=i)
                elif char == s[i-2]:
                    candidate = self.get_palindrome(s, start=i-2, end=i)
            if len(candidate) > len(longest):
                longest = candidate
        return longest

    @staticmethod
    def get_palindrome(s, start, end):
        palindrome = s[start:end+1]
        while end < len(s) - 1:
            if s[end+1] == s[start] and Solution.all_same(palindrome):
                end += 1
                palindrome += s[end]
            else:
                break
        while (start > 0) and (end < len(s) - 1):
            start -= 1
            end += 1
            if s[start] == s[end]:
                palindrome = s[start] + palindrome + s[end]
            else:
                break
        return palindrome

    @staticmethod
    def all_same(items):
        return all(item == items[0] for item in items)


def test_1():
    assert Solution().longestPalindrome("babad") == "bab"

def test_2():
    assert Solution().longestPalindrome("cbbd") == "bb"

def test_3():
    assert Solution().longestPalindrome("abba") == "abba"

def test_4():
    assert Solution().longestPalindrome("a") == "a"

def test_5():
    assert Solution().longestPalindrome("ccc") == "ccc"

def test_6():
    assert Solution().longestPalindrome("aaaa") == "aaaa"

def test_7():
    assert Solution().longestPalindrome("aaabaaaa") == "aaabaaa"


if __name__ == "__main__":
    pytest.main([__file__])

问题是我收到“超出时间限制”错误:

我的理解是,这个算法的时间复杂度是 O(n^2),因为它会检查每个字符的回文数,最长可达 n 个字符。在 LeetCode 的解决方案中,还有 O(n^2) 算法(在 Java 中)。

我猜对Python来说时间限制有点太严格了,比Java慢。还是我遗漏了什么,我的解决方案的时间复杂度实际上是否大于 O(n^2)?

【问题讨论】:

我没有深入研究您的代码,但事实上的标准方法是使用动态编程。 我猜时间限制“知道”big-O 的工作原理,也就是说,它是渐近的东西。所以它会以某种方式估计你的解决方案需要多长时间 n,然后看看你在 100n 或类似的东西上做。并使用足够大的 n,以使设置成本无关紧要。 【参考方案1】:

您失败的测试字符串似乎仅由 a 组成。这是最坏的情况,实际上是 O(n³) 而不是 O(n²),因为在 all_same 中还有另一个隐藏循环。 (一开始我还以为字符串上的切片操作符[:]会做一个拷贝,但事实并非如此。)

您需要调用all_same,因为您在主函数中区分了“aa”和“aba”这两种情况。但是您不需要在循环中执行此操作,因为您将只在get_palindrome 的第一个while 循环中添加相同的字母。因此,一个快速的解决方法是测试所有字符是否都相同一次:

    if Solution.all_same(palindrome):
        while end < len(s) - 1:
            if s[end+1] == s[start]:
                end += 1
                palindrome += s[end]
            else:
                break

现在all_same os 可以在两个或三个字母的字符串上运行,并且会很快。

更好的解决方案根本不需要all_same。当您可以只传递“b”并让该函数完成其余工作时,为什么还要将“aba”传递给get_palindrome

        elif i >= 2:
            if char == s[i-1]:
                candidate = self.get_palindrome(s, start=i-1, end=i)
            else:
                candidate = self.get_palindrome(s, start=i, end=i)

总体而言,由于所有breaks 和不必要的大小写区别,代码看起来相当凌乱。为什么要将索引和palindrome 作为单独的实体保留在get_palindrome 中,您必须保持同步?

在我看来,这是一个更整洁的版本:

class Solution:
    def longestPalindrome(self, s):
        longest = ""

        for i, _ in enumerate(s):
            candidate = self.get_palindrome(s, start = i, end = i)

            if len(candidate) > len(longest):
                longest = candidate

        return longest

    @staticmethod
    def get_palindrome(s, start, end):
        while end + 1 < len(s) and s[end+1] == s[start]:
            end += 1

        while start > 0 and end + 1 < len(s) and s[start - 1] == s[end + 1]:
            start -= 1
            end += 1

        return s[start:end + 1]

即便如此,仍有改进的空间:对于字符串“aaaa”,代码仍然会考虑“aaaa”、“aaa”、“aa”和“a”。 get_palindrome 中的第一个 while 将一路走下去,但没有机会找到更好的命中。我们可以通过在主函数中找到相同字母的延伸来改进这一点:

class Solution:
    def longestPalindrome(self, s):
        longest = ""
        i = 0
        l = len(s)

        while i < l:
            end = i

            while end + 1 < l and s[end + 1] == s[i]:
                end += 1

            candidate = self.get_palindrome(s, i, end)

            if len(candidate) > len(longest):
                longest = candidate

            i = end + 1

        return longest

    @staticmethod
    def get_palindrome(s, start, end):
        while start > 0 and end + 1 < len(s) and s[start - 1] == s[end + 1]:
            start -= 1
            end += 1

        return s[start:end + 1]

这对于像“abababab”这样的字符串仍然不理想,但在你的情况下应该足够快。

【讨论】:

我提交了第一个解决方案并被接受。事实上,我错过了all_same 方法也是 O(n),所以我运行的是 O(n^3) 方法而不是 O(n^2) 方法。 (您的解决方案确实也更整洁;从单个字母开始扩展回文是有意义的)。【参考方案2】:

我尝试了“动态规划”的想法,即找到“0_th order”回文的中心,然后随着深度 j 的增加和不匹配的发生进行修剪

修剪是在列表组合内完成的,应该相对较快,但仍然 O(n^2)

class Solution:
    def longestPalindrome(self, s):

        s = '>' + s + '<'  # add guard values

        # make lists of '0_th order' palindrome 'centers', even and odd

        evn = [i for i, a in enumerate(zip(s, s[1:])) if a[0] == a[1]]

        odd = [i + 1 for i, a in enumerate(zip(s, s[2:])) if a[0] == a[1]]

        # prune lists of centers when elements +/- j from centers don't match

        evn_last, odd_last = [[1], 0], [[1], 1]

        j = 1
        while evn:
            evn_last = (evn, j)
            evn = [e for e in evn if s[e - j] == s[e + j + 1]]
            j += 1

        j = 1
        while odd:
            odd_last = (odd, j)
            odd = [e for e in odd if s[e - j] == s[e + j]]
            j += 1

        # determine longest, construct palindrome

        if 2 * evn_last[1] > 2 * odd_last[1] - 1:

            cntr = evn_last[0][0]
            pal = s[cntr] + s[cntr + 1]
            for i in range(1, evn_last[1]):
                pal = s[cntr - i] + pal + s[cntr + i + 1]
        else:
            cntr = odd_last[0][0]
            pal = s[cntr]
            for i in range(1, odd_last[1]):
                pal = s[cntr - i] + pal + s[cntr + i]
        return pal

抱歉,如果我错误地粘贴到类包装器中 - OOP 不是我的事 确实通过了你的测试

可能已经弄清楚调用实例,明显的重命名

S = Solution()

%timeit S.fred_longestPalindrome("aba"*300)
17.8 ms ± 230 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit S.Kurt_longestPalindrome("aba"*300)
52.8 ms ± 108 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

【讨论】:

【参考方案3】:

这是我发现获得最长回文及其长度的方法。我认为这很容易理解。

首先我将单词添加到一个字符数组中,然后我检查第一个字母和所有其他字母向后。然后像这样继续下一个字符。使用 if else 和 for 循环来得到答案,最后使用哈希集我得到了最长的回文。

public static void main(String[] args) 

    Scanner in = new Scanner(System.in);

System.out.println(longestPalSubstr(in.nextLine().toLowerCase()));







static String longestPalSubstr(String str) 

       char [] input = str.toCharArray();
       Set<CharSequence> out = new HashSet<CharSequence>();

      int n1 = str.length()-1;


      for(int a=0;a<=n1;a++)
       
          for(int m=n1;m>a;m--)
          

          if(input[a]==input[m])
          

           String nw = "",nw2="";

           for (int y=a;y<=m;y++)
           

                nw=nw+input[y];
           
           for (int t=m;t>=a;t--)
           

               nw2=nw2+input[t];
           


           if(nw2.equals(nw))
           

                out.add(nw);


               break;
           
       

     

   


    int a = out.size();
    int maxpos=0;
    int max=0;
    Object [] s = out.toArray();

    for(int q=0;q<a;q++)
    

        if(max<s[q].toString().length())
        
            max=s[q].toString().length();
            maxpos=q;
        
    


   String output = "longest palindrome is : "+s[maxpos].toString()+" and the lengths is : "+ max; 
   return output;




此方法将返回最大长度回文数及其长度。这是我尝试并得到答案的一种方式。无论是奇数还是偶数,这个方法都会运行。

【讨论】:

此方法将给出任意长度随机单词的输出。偶数或奇数。【参考方案4】:
class Solution:
    def longestPalindrome(self, s):   
        paliandr = ''
        len_s = len(s)


        def if_pal_singl(s,i,dabl):
            pal = s[i-dabl:i+1]
            indx_left = i-dabl
            indx_right = i
            while (indx_left-1 in range(len_s) and indx_right+1 in range(len_s)):
                indx_left -=1
                indx_right +=1
                if s[indx_left] == s[indx_right]:
                    pal = s[indx_left]+pal+s[indx_right]
                else: 
                    break
            return pal  


        dabl = 0        
        for i in range(1,len_s-1):
            if s[i] == s[i+1]:
                dabl+=1
                continue
            pal = if_pal_singl(s,i,dabl)
            dabl = 0
            if len(pal) > len(paliandr):
                paliandr = pal
        print (paliandr)
if __name__ == "__main__":
    Solution().longestPalindrome('abababab')

【讨论】:

以上是关于在 Python 中查找字符串中最长的回文的主要内容,如果未能解决你的问题,请参考以下文章

最长(大)回文串的查找(字符串中找出最长的回文串)PHP实现

查找字符串中的最长回文字符串---Manacher算法

脑子要烧坏了:使用manache算法查找最长回文子字符串

脑子要烧坏了:使用manache算法查找最长回文子字符串

第5题 查找字符串中的最长回文字符串---Manacher算法

第5题 查找字符串中的最长回文字符串---Manacher算法