【问题标题】:Optimize Time/Space Complexity for Solving Palindromes优化解决回文的时间/空间复杂度
【发布时间】:2020-10-31 11:03:26
【问题描述】:

最近 Twitter 提出了这个问题:

回文是一个前后读相同的字符序列。给定一个字符串 s,找出 s 中最长的回文子串。

例子:

Input: "banana"
Output: "anana"

Input: "million"
Output: "illi"


class Solution: 
    def longestPalindrome(self, s):
      # Fill this in.
        
# Test program
s = "tracecars"
print(str(Solution().longestPalindrome(s)))
# racecar

所以我就这样解决了

class Solution: 
    def longestPalindrome(self, s):
        index = 0
        longestPalindrome = ""
        for x  in s:
            subStr = s[index + 1:]
            nextIndex = subStr.find(x)
            while nextIndex != -1:
                txt = x + subStr
                pland = txt[:nextIndex + 2]
                if self.isPalindromicSubString(pland):
                    if len(pland) > len(longestPalindrome):
                        longestPalindrome = pland
                nextIndex = subStr.find(x,nextIndex + 1)
            index = index + 1
        return longestPalindrome

    def isPalindromicSubString(self,subStr):
        index = 0
        reverseIndex = -1
        isItPalindromic = True
        for y in subStr:
            if y != subStr[reverseIndex]:
               isItPalindromic = False
            index = index + 1
            reverseIndex = reverseIndex - 1  
        return isItPalindromic

# Test program
s = "abcdef aka"
print(str(Solution().longestPalindrome(s)))
# racecar

效果很好,时间复杂度为 O(N^2) 有什么方法可以做得更好,并给我关于时间和空间复杂度的cmets

【问题讨论】:

  • 这是一个众所周知的问题:en.wikipedia.org/wiki/Longest_palindromic_substring
  • 最后,似乎即使是众所周知的解决方案最终仍然是 O(N^2)
  • @h0r53 来自维基百科文章:>[Manacher 算法] 为最长回文子串问题提供了线性时间解
  • 谢谢,我只知道这是一个众所周知的问题,我已经解决了,但我想知道它是否可以通过更好的解决方案来解决
  • @h0r53 实际上它在 for 循环中包含 while 循环,所以它可能是 o(n^2) alog

标签: python performance optimization


【解决方案1】:

首先,我不知道您为什么要为此使用 clas。如果不使用 OOP,我们在 python 中不会编写类,而你不是

简介

我经历了回文子串 *1 的中间部分,
从中间往左走,直到子串不再是回文
或遇到字符串结尾

代码

def find_longest_p(s):
    pal_start = 0; pal_end = 1 # indexes of longest
    for half_index in range(0, len(s)*2-1):
        # about 1st range bounds
        # it goes over middles of palindromes
        # which are at some char or between 2 chars
        # that char is s[half_index/2]
        #    where for odd it is s[x.5] meaning between s[x] a& s[x+1]
        
        c = b = -1 # start with value because for else is asking for b,c
        
        for b in range((half_index + half_index % 2) // 2 - 1, -1, -1):
            # starts from first index to the left, goes to 0
            # "a b c d" (str "abcd", disregard spaces)
            #  0123456  (half indexes)
            # for half_index == 4
            #    b goes -> 1 -> 0
            # for half_index == 3
            #    b goes -> 1 -> 0
            
            c = half_index - b # symetric index of b
            if c < len(s) and s[b] == s[c]: # if not, either end of string or no longer palindrome
                continue
            lenght = c - b - 1
            if lenght > pal_end - pal_start:
                pal_start = b + 1
                pal_end = c
            break
        else:
            # out of bounds to the left
            # arithmetic changes a little
            lenght = c - b + 1
            if lenght > pal_end - pal_start:
                pal_start = b
                pal_end = c + 1
    return s[pal_start : pal_end]

复杂性

这似乎有 O(n^2)
实际上并非如此,因为在 平均 字符串中,找不到太多回文。
意味着第二个循环主要有一个迭代
花费 O(n) 时间 复杂度,
以及额外的 O(1) 空间,定义 3 个变量,以及一些中间产品,
O(n) 空间,如果您包括复制和切片(这是不可避免的)

脚注

*1 回文中间可以在某个索引处或在两个索引之间
因此,我经历了双重索引
中间是:0, 0.5, 1, 1.5, ...
我去了:0、1、2、3、...

【讨论】:

  • 这个答案不适用于像“bba”这样的字符串,并且对于像“aaaaaaaaaaaaa”这样的字符串具有 O(n^2) 最坏情况时间复杂度
  • 我确实说过平均
【解决方案2】:

这是一个 Python 实现的 Manacher 算法,来自 Kevin 链接的 Wikipedia article

def longest_palindrome_substring(s: str) -> str:
    # Add sentinel chars between chars of s and around s, to handle even length palindromes
    # Different outer chars to exit palindrome expanding loop without boundary checking
    # in case the entire string is a palindrome
    with_boundaries = "@|" + "|".join(s) + "|!"

    # Length of palindrome centered at each index in the new string
    palindrome_lengths = [0 for _ in with_boundaries]

    # Center of current palindrome
    center_current = 0
    # Right boundary of current palindrome
    right_current = 0

    # Track largest palindrome length and center index
    max_len = 0
    max_center = 0
    for i in range(2, len(with_boundaries) - 2):
        # If i is inside a bigger palindrome, copy the length of the mirror palindrome
        # e.g. *abacaba*
        #            ^  the second aba inside abacaba must have the same length of the first
        if i < right_current:
            center_mirror = 2 * center_current - i
            # add only the length of the mirror palindrome inside the current one
            palindrome_lengths[i] = min(right_current - i, palindrome_lengths[center_mirror])

        # Increase the length of the palindrome from the center
        while (
            with_boundaries[i + palindrome_lengths[i] + 1]
            == with_boundaries[i - (palindrome_lengths[i] + 1)]
        ):
            palindrome_lengths[i] += 1

        # Update current right boundary and current center index
        if i + palindrome_lengths[i] > right_current:
            right_current = i + palindrome_lengths[i]
            center_current = i

        # Keep track of the longest
        if palindrome_lengths[i] > max_len:
            max_len = palindrome_lengths[i]
            max_center = i
    # return from max_center - max_len to max_center + max_len, filtering out sentinel chars
    return "".join(
        c
        for c in with_boundaries[max_center - max_len : max_center + max_len + 1]
        if c not in "@|!"
    )

虽然它看起来确实具有二次复杂度 O(n^2),但在 for 循环中有一个 while 循环,它实际上只有 O(n),如 wikipedia 上所述

【讨论】:

    【解决方案3】:

    事实证明,您实际上可以使用动态生成的正则表达式来解决这个问题。

    # Question: https://stackoverflow.com/questions/62838784/optimize-time-space-complexity-for-solving-palindromes
    
    import re
    import time
    
    
    def longestPalindrome(string, longest_first=True):
        palindrome = None
        order = range(len(string)//2, -1, -1) if longest_first else range(1, len(string)//2+1)
        for n in order:
            # Example: re.sub(r'^.*(\w)(\w)(\w)(\w)(\w)?\3\2\1.*$', r'\1\2\3\4\3\2\1', s)
            regex_match = "".join([
                r'^.*',
                r'(\w)' * n,
                r'(\w)?',
                ''.join([ f'\\{i}' for i in range(n,0,-1) ]),
                r'.*$'
            ])
            if re.match(regex_match, string):
                regex_replace = "".join([ f'\\{i}' for i in list(range(1,n+2))+list(range(n,0,-1)) ])
                palindrome    = re.sub(regex_match, regex_replace, string)
                if longest_first:
                    return palindrome  # return the first match
            else:
                if not longest_first:
                    break  # return the last match
    
        return palindrome
    
    
    if __name__ == '__main__':
        for longest_first in [True, False]:
            print(f'\nLongest First = {longest_first}')
            for sentence in [
                "banana",
                "tracecars",
                "detartrated",
                "saippuakivikauppias",
                "this is not the palindrome you are looking for"
            ]:
                start_time = time.perf_counter()
                answer     = longestPalindrome(sentence, longest_first)
                time_taken = time.perf_counter() - start_time
                print(f'len({len(sentence):2d}) in {1000*time_taken:6.2f}ms = longestPalindrome({sentence}, {longest_first}) == {answer}')
    
        assert longestPalindrome("banana")      == "anana"
        assert longestPalindrome("tracecars")   == "racecar"
        assert longestPalindrome("detartrated") == "detartrated"
    
    
    Longest First = True
    len( 6) in   0.79ms = longestPalindrome(banana, True) == anana
    len( 9) in   0.39ms = longestPalindrome(tracecars, True) == racecar
    len(11) in   0.41ms = longestPalindrome(detartrated, True) == detartrated
    len(19) in   0.59ms = longestPalindrome(saippuakivikauppias, True) == saippuakivikauppias
    len(46) in  13.19ms = longestPalindrome(this is not the palindrome you are looking for, True) == oo
    
    Longest First = False
    len( 6) in   0.06ms = longestPalindrome(banana, False) == anana
    len( 9) in   0.08ms = longestPalindrome(tracecars, False) == racecar
    len(11) in   0.19ms = longestPalindrome(detartrated, False) == detartrated
    len(19) in   0.46ms = longestPalindrome(saippuakivikauppias, False) == saippuakivikauppias
    len(46) in   0.04ms = longestPalindrome(this is not the palindrome you are looking for, False) == oo
    

    有两种算法选项,一种首先搜索可能的最长回文,然后缩小正则表达式直到找到匹配项,另一种首先检查是否存在 2 个字符的回文,然后继续增长正则表达式,直到找到最长的匹配。

    在最长的第一个版本中:

    对于真正的回文,最佳情况的时间复杂度是O(N),因为正则表达式只需要遍历字符串一次即可验证。

    最坏情况下的时间复杂度是针对非回文的。主循环将运行 N/2 次。第一个循环的正则表达式时间复杂度需要读取一半的字符串以排除,然后在第二个循环检查两个位置组合:O(N/2) + O(2*(N/2-1))O(3*(N/2-2)) 等等。 Wolfram Alpha 说这是O(N^3/48 + N^2/8 + N/6) ~= (N/3)^3 + (N/3)^2。所以整体时间复杂度约为O((N/4)^4)

    但是,即使对于短句,实际执行时间仍然在毫秒范围内,这可能足够快,不必担心。

    最短优先

    如果您将假设更改为大多数输入不会是回文,那么在逐步扩展到更大的子字符串之前测试是否存在长度为 2 的回文可能会更快。这是最佳和最坏情况时间复杂度之间的权衡。非回文可以很快被排除,但长回文会花费一些额外的时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-07-28
      • 2020-11-13
      • 2016-02-13
      • 2012-08-14
      • 1970-01-01
      • 1970-01-01
      • 2020-04-15
      相关资源
      最近更新 更多