LeetCode刷题实战5:判断回文子串

 

算法的重要性,我就很少说了吧,想去大厂,就必需要通过基础知识和业务逻辑面试+算法面试。因此,为了提升你们的算法能力,这个公众号后续天天带你们作一道算法题,题目就从LeetCode上面选 !今天和你们聊的问题叫作判断回文子串,这道题颇有意思,咱们先来看题面:

 

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.面试

https://leetcode.com/problems/longest-palindromic-substring/

 

翻译算法

 

给定一个字符串s,要求它当中的最长回文子串。能够假设s串的长度最大是1000。数组

 

 

样例
  •  
Example 1:
Input: "babad"Output: "bab"Note: "aba" is also a valid answer.Example 2:
Input: "cbbd"Output: "bb"
分析

 

虽然LeetCode里给这道题的难度是Medium,但实际上并不简单,咱们经过本身思考很难想到最佳解法。咱们先把各类算法放在一边,先从最简单的方法开始。最简单的方法固然是暴力枚举,可是这道题和以前的字符串问题不一样。咱们在暴力枚举的时候,并不须要枚举全部的起始位置,再判断这个子串是否回文。实际上咱们能够利用回文串两边相等的性质,直接枚举回文串的中心位置,若是两边相等就往两边延伸。这样咱们最多须要枚举n个回文中心,每次枚举最多遍历n次。因此最终的复杂度是O(n²)。有经验的同窗看到这个复杂度就能反应过来,这明显不是最优解法。可是对于当前问题,暴力枚举当然不是最佳解法,但其实也算得上是不错了,并无咱们想的那么糟糕,不信的话,咱们来看另外一个看起来高端不少的解法。动态规划(DP)

 

这道题中利用回文串的性质还有一个trick,对于一个字符串S,若是咱们对它进行翻转,获得S_,显然它当中的回文子串并不会发生变化。因此若是咱们对翻转先后的两个字符串求最长公共子序列的话,获得的结果就是回文子串。ide

 

算法导论当中对这个问题的讲解是使用动态规划算法,便是对于字符串S中全部的位置i和S_中全部的位置j,咱们用一个dp数组记录下以i和j结尾的S和S_的子串可以组成的公共子序列的最大的结果。学习

 

显然,对于i=0,j=0,dp[i][j] = 0(假设字符串下标从1开始)优化

 

咱们写出DP的代码:翻译

  •  
for i in range(1, n):  for j in range(1, m):    if S[i] == S_[j]:      dp[i][j] = dp[i-1][j-1] + 1    else:      dp[i][j] = max(dp[i-1][j], dp[i][j-1])

咱们不难观察出来,这种解法的复杂度一样是。而且空间复杂度也是O(n),也就是说咱们费了这么大劲,并无起到任何优化。因此从这个角度来看,暴力搜索并非这题当中很糟糕的解法。code

 

分析到了这里,也差很少了,下面咱们直接进入正题,这题的最佳解法,O(n)时间内获取最大回文子串的曼彻斯特算法。blog

 

曼切斯特算法回文串除了咱们刚刚提到的性质以外,还有一个性质,就是它分奇偶。简而言之,就是回文串的长度能够是奇数也能够是偶数。若是是奇数的话,那么回文串的回文中心就是一个字符,若是是偶数的话,它的回文中心实际上是落在两个字符中间。举个例子:ABA和ABBA都是回文串,前者是奇回文,后者是偶回文。这两种状况不一致,咱们想要一块儿讨论比较困难,为了简化问题,咱们须要作一个预处理,将全部的回文串都变成奇回文。怎么作呢,其实很简单,咱们在全部两个字符当中都插入一个特殊字符#。好比:abba -> #a#b#b#a#这样一来,回文中心就变成中间的#了。咱们再来看本来是奇回文的状况:aba -> #a#b#a#回文中心仍是在b上,依然仍是奇回文。
预处理的代码:
  •  
def preprocess(text):    new_str = '#'    for c in text:        new_str += c + '#'    return new_str

 

曼切斯特算法用到三个变量,分别是数组p,idx和mr。咱们接下来一个一个介绍。leetcode

 

首先是数组radis,它当中存在的是每一个位置能构成的最长回文串的半径。注意,这里不是长度,是半径。

 

咱们举个例子:

 

 

  •  
字符串S     # a # b # b # a #radis      1 2 1 2 5 2 1 2 1
咱们先不去想这个radis数组应该怎么求,咱们来看看它的性质。首先,i位置的回文串的半径是radis[i],那么它的长度是多少?很简单: radis[2] * 2- 1。那么,这个串中去掉#以后剩下的长度是多少?也就是说预处理以前的长度是多少?答案是radis[i] - 1,推算也很简单,总长度是radis[i] * 2 - 1,其中#比字母的数量多一个,因此原串的长度是(radis[i] * 2 - 1 - 1)/2 = radis[i] - 1。也就是说原串的长度和radis数组就算是挂钩了。idx很好理解,它就是指的是数组当中的一个下标,最后是mr,它是most_right的缩写。它记录的是在当前位置i以前的回文串所向右能延伸到的最远的位置。听起来有些拗口,咱们来看个例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

此时i小于mr,mr对应的回文中心是id。那么i在id的回文范围当中,对于i而言,咱们能够获取到它关于id的对称位置:id * 2 - i,咱们令它等于i_。知道这个对称的位置有什么用呢?很简单,咱们能够快速的肯定radis[i]的下界。在遍历到i的时候,咱们已经有了i_位置的结果。经过i_位置的结果,咱们能够推算i位置的范围。

 

radis[i]  >= min(radis[i_], mr-i)

 

为何是这个结果呢?

 

咱们把状况写全,假设mr-i > radis[i_]。那么i_位置的回文串所有都落在id位置的回文串里。这个时候,咱们能够肯定radis[i]=radis[i_]。为何呢?

 

由于根据对称原理,若是以i为中心的回文串更长的话,咱们假设它的长度是radis[i_]+1。会致使什么后果呢?若是这个发生,那么根据关于id的对称性,这个字符串关于id的对称位置也是回文的。那么radis[i_1]也应该是这么多才对,这就构成了矛盾。若是你从文字描述看不明白的话,咱们来看下面这个例子:

 

 

  •  
S:       c a b c b d b c b a cradis:     x_  i_  5   i   x

 

 

 

在这个例子当中,mr-i=5,radis[i_]=2。因此mr - i > radis[i_]。若是radis[i]=3,那么x的位置就应该等于id的位置,同理根据对称性,x_的位置也应该等于id的位置。那么radis[i_]也应该是3。这就和它等于2矛盾,因此这是不可能出现的,在mr距离足够远的状况下,radis[i_]的值限制了i位置的可能性。咱们再来看另外一种状况,若是mr - i < radis[i_]时会怎么样呢?在这种状况下,因为mr距离i太近,致使i对称位置的半径没法在i位置展开。可是mr的右侧可能还存在字符,这些字符能够构成新的回文吗?
  •  
字符串S     XXXXXXXXSXXXXXXXXXXXXXXXradis        i_    id    i mr
也就是说S[mr+1]会和S[i*2-mr-1]的位置相同吗?其实咱们能够不用判断就能够知道答案,答案是不会。举个例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

根据对称性,若是mr+1的位置对于i能够构成新的对称。因为radis[i_] > mr-i,也就是说对于i_位置而言,它的对称范围可以辐射到mr对称点的左边。咱们假设这个地方的字母是a,根据对称性,咱们能够得出mr+1的位置也应该是a。如此一来,这两个a又能构成新的对称,那么id位置的半径就能够再拓展1,这就构成了矛盾。因此,这种状况下,因为mr-i的限制,使得radis[i]只能等于mr - i。那什么状况下i位置的半径能够继续拓展呢?只有mr - i == radis[i_]的时候,id构成的回文串的左侧对于i_可能构不成新的回文,可是右侧却存在这种可能性。举个例子:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

 

在上图这个例子当中,i_的位置的回文串向左只能延伸到ml,由于ml-1的位置和关于i_对称的位置不相等。对于mr的右侧,它彻底能够既和i点对称,又不会影响raids[id]的正确性。这个时候,咱们就能够经过循环继续遍历,拓展i位置的回文串。

 

整个过程的分析虽然不少,也很复杂,可是写成代码却并很少。

 

 

  •  
# 初始化idx, mr = 0, 0# 为了防止超界,设置字符串从1开始for i in range(1, n):  # 经过对称性直接计算radis[i]  radis[i] = 1 if mr < i else min(radis[2 * idx - i], mr - i)  # 只有radis[i_] = mr - i的时候才继续往下判断  if radis[2 * idx - i] != mr - i and mr > i:    continue  # 继续往下判断后面的位置  while s[radis[i] + i] == s[i - radis[i]]:    radis[i] += 1  # 更新idx和mr的位置  if radis[i] + i > mr:    mr = radis[i] + i    idx = i

 

 

到这里,曼切斯特算法就算是实现完了。虽然咱们用了这么多篇幅去介绍它,但是真正写出来,它只有几行代码而已。不得不说,实在是很是巧妙,第一次学习可能须要反复思考,才能真正理解。

 

不过咱们还有一个问题没有解决,为何这样一个两重循环的算法会是 O(n)的复杂度呢?

 

想要理解这一点,须要咱们抛开全部的虚幻来直视本质。虽然咱们并不知道循环进行了多少次,可是有两点能够确定。经过这两点,咱们就能够抓到复杂度的本质。

 

第一点,mr是递增的,只会变大,不会减少。

第二点,mr的范围是0到n,每次mr增长的数量就是循环的次数。

 

因此即便咱们不知道mr变化了多少次,每次变化了多少,咱们依然能够肯定,这是一个O(n)的算法。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相关文章
相关标签/搜索