1、引言
还是国际惯例,先贴上leetcode上的原题:
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.
这是一道关于字符串的题目,找寻字符串中的最大回文子串,这让我回想到考研时候数据结构中最让我头疼的KMP算法,真好趁这道题目的机会重新温故一下字符串的应用,主要以字符串的匹配,以及最大回文串,最大回文子序列三个方面来实现。
因为回顾比较多,所以本文有些长,并且部分脱离了Leetcode范畴。
2、KMP算法
kmp算法又称“看毛片”算法,是一个效率非常高的字符串匹配算法。不过由于其难以理解,所以在很长的一段时间内一直没有搞懂。虽然网上有很多资料,但是鲜见好的博客能简单明了地将其讲清楚。在此,综合网上比较好的几个博客(参见最后),尽自己的努力争取将kmp算法思想和实现讲清楚。
kmp算法完成的任务是:给定两个字符串O和f,长度分别为n和m,判断f是否在O中出现,如果出现则返回出现的位置。常规方法是遍历a的每一个位置,然后从该位置开始和b进行匹配,但是这种方法的复杂度是O(nm)。kmp算法通过一个O(m)的预处理,使匹配的复杂度降为O(n+m)。(以上来源http://blog.youkuaiyun.com/yutianzuijin/article/details/11954939/)
在之前阅读相关博客和资料的时候,我一直不太明白,怎么确保匹配子串发生移动时,在最大小标情况下保证之前的元素与被匹配数组元素相同。
以下将对此进行解释,但愿自己能够说清楚吧。(实例来源http://www.matrix67.com/blog/archives/115)
假如,A=”abababaababacb”,B=”ababacb”,我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
第一次比对到i==5,因为i=6时候发现A与B元素不相同,故而开始进行回头重比较:
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
这样问题来了,KMP算法采用什么机制或者说具体是如何进行比对保证j==4的时候B[5]前面的元素与A[7]之前的四个元素相同呢?
其实道理很简单,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组P[ j ],表示当匹配到B数组的第j个字母而第j+1个字母不能匹配了时,新的j最大是多少。P[j]应该是所有满足B[ 1…P[ j ] ]=B[ j-P[ j ]+1 … j ]的最大值。
因此,代码可以理解为扫描字符串A,并更新可以匹配到B的什么位置。
于是,我们需要一个算法实现P数组的预处理过程。
伪代码如下:
p[1] <- 0;
j <- 0;
for i <- 2 to m do{
while j > 0 and B[j+1] ≠ B[i] do j <- P[j];
if B[j+1] = B[i] then j <- j+1;
p[i] <- j;
}
其实代码很简单,实现了对于B数组的一次自我匹配,但是其中用到了摊还思想,需要细细品味。
有了P数组,那么得到KMP整体算法就没那么难了,
j <- 0;
for i <- 1 to m do{
while j > 0 and B[j+1] ≠ A[i] do j <- P[j];
if B[j+1] = A[i] then j <- j+1;
if j = m then writeln('Pattern occurs with shift ',i-m);
p[i] <- j;
}
附上python代码:
class KMP(object):
def kmpAlgorithmPre(self, a):
p=[0]*len(a)
i = 0
for j in range (1,len(a)-1):
while (i>0 and a[i]!=a[j]):
i = p[i]
# print(i)
if a[i] == a[j]:
i = i+1
p[j] = i
print(p)
return p
def kmpAlorithm(self,a,b):
pre = self.kmpAlgorithmPre(b)
m = -1
samesub=[]
temp = []
for n in range(0,len(a)-1):
while(m>0 and b[m+1]!=a[n]):
temp = []
#print(m)
m = pre[m]
if b[m+1] == a[n]:
m = m+1
temp.append(b[m])
if len(temp)>len(samesub):
samesub = temp[:]
if m ==len(b)-1:
m = pre[m]
return samesub
3、Manacher算法
此部分参考自:http://blog.youkuaiyun.com/dyx404514/article/details/42061017
下面介绍Manacher算法的原理与步骤。
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:
(1)Len数组简介与性质
Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
对于上面的例子,可以得出Len[i]数组为:
Len数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。
有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。
(2)Len数组的计算
首先从左往右依次计算Len[i],当计算Len[i]时,Len[j](0<<=j<< i)已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:
第一种情况:i<=P
那么找到i相对于po的对称位置,设为j,那么如果Len[j]<< P-i,如下图:
那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i] > = Len[j]。因为Len[j] < P-i,所以说i+Len[j]< P。由对称性可知Len[i]=Len[j]。
如果Len[j] >= P-i,由对称性,说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]
第二种情况: i>P
如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。
(3)时间复杂度分析
Manacher算法的时间复杂度分析和Z算法类似,因为算法只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于T字符串中的每一个位置,只进行一次匹配,所以Manacher算法的总体时间复杂度为O(n),其中n为T字符串的长度,由于T的长度事实上是S的两倍,所以时间复杂度依然是线性的。
下面是算法的实现,注意,为了避免更新P的时候导致越界,我们在字符串T的前增加一个特殊字符,比如说‘$’,所以算法中字符串是从1开始的。
伪代码如下:
Manacher(st[0...n], len)
{
mx <- 0;
ans <- 0;
po <- 0;
for i <- 1 to len do{
if mx >i then L[i] <- min(mx-i,L[2*po-i]);
else L[i] <- 1;
while st[i-L[i]] = st[i+L[i]] do L[i] <- L[i]+1;
if L[i] +i >mx then {
mx <- L[i]+i;
po<-i;}
ans <- max(ans, L[i]);
}
return ans-1;
}
附上python实现代码:
class manacherAlogrithm(object):
def preDeal(self,str):
s=list(str)
for i in range(0,len(s)*2+1,2):
s.insert(i,'#')
return s
def manacher(self,str):
mx =0
ans = 0
po = 0
s = self.preDeal(str)
L = [0]*len(s)
s.append('@')
for i in range(0,len(s)-2):
if mx > i:
L[i] = min(mx-i, L[2*po-i])
else :
L[i] = 1
while s[i-L[i]] == s[i+L[i]]:
L[i]= L[i]+1
if L[i]+i>mx:
mx = L[i]+i
po = i
if ans < L[i]:
sw = s[i-L[i]+1:i+L[i]-1]
ans = L[i]
sw = filter(lambda x:x!='#',sw)
sw = ''.join(sw)
return sw
送入Leetcode跑跑:
效果还行
附上LeetCode中最佳的代码,学习学习:
class Solution:
def longestPalindrome(self, s):
# Transform S into T.
# For example, S = "abba", T = "^#a#b#b#a#$".
# ^ and $ signs are sentinels appended to each end to avoid bounds checking
T = '#'.join('^{}$'.format(s))
n = len(T)
P = [0] * n
C = R = 0
for i in range (1, n-1):
P[i] = (R > i) and min(R - i, P[2*C - i])
# equals to i' = C - (i-C)
# Attempt to expand palindrome centered at i
while T[i + 1 + P[i]] == T[i - 1 - P[i]]:
P[i] += 1
# If palindrome centered at i expand past R,
# adjust center based on expanded palindrome.
if i + P[i] > R:
C, R = i, i + P[i]
# Find the maximum element in P.
maxLen, centerIndex = max((n, i) for i, n in enumerate(P))
return s[(centerIndex - maxLen)//2: (centerIndex + maxLen)//2]
跑了一下,也没提高多少跟帖子说的时间不一样(差距有些大),可能改了数据集吧,就不放结果了。