1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
8.
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
14.
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
15.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
16.
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
2.next数组的求解思路
通过上文完全可以对kmp算法的原理有个清晰的了解,那么下一步就是编程实现了,其中最重要的就是如何根据待匹配的模版字符串求出对应每一位的最大相同前后缀的长度。我先给出我的代码:
void InitNext(const char sub[],int next[])
{
int i, k;//i:模版字符串下标;k:最大前后缀长度
int length = strlen(sub);//模版字符串长度
next[0] = 0;//模版字符串的第一个字符的最大前后缀长度为0
for (i = 1,k = 0; i < length; ++i)//for循环,从第二个字符开始,依次计算每一个字符对应的next值
{
while(k > 0 && sub[i] != sub[k])//递归的求出sub[0]···sub[i]的最大的相同的前后缀长度k
k = next[k-1]; //不理解没关系看下面的分析,这个while循环是整段代码的精髓所在,确实不好理解
if (sub[i] == sub[k])//如果相等,那么最大相同前后缀长度加1
{
k++;
}
next[i] = k;
}
}
现在我着重讲解一下while循环所做的工作:(红色部)
【注:sub[k-1]是后缀的最后一位!其所对应的相同的前缀的那一位就是sub[j-1](j==next[k-1])】
#include
#include
void InitNext(const char sub[],int next[])
{
int q,k;
int length = strlen(sub);
next[0] = 0;
for (q = 1,k = 0; q < length; ++q)
{
while(k > 0 && sub[q] != sub[k])
k = next[k-1];
if (sub[q] == sub[k])
{
k++;
}
next[q] = k;
}
}
int kmp(const char base[],const char sub[],int next[])
{
int n = strlen(base);
int length = strlen(sub);
InitNext(sub, next);
for (int i = 0,q = 0; i < n; ++i)
{
while(q > 0 && sub[q] != base[i])
q = next[q-1];
if (sub[q] == base[i])
{
q++;
}
if (q == length)
{
printf("Pattern occurs with shift:%d\n",(i-length+1));
}
}
}
int main()
{
int i;
int next[20]={0};
char base[] = "ababxbabcdabddfdsss";
char sub[] = "abcdabd";
printf("%s\n",base);
printf("%s\n",sub);
kmp(base, sub, next);
for (i = 0; i < strlen(sub); ++i)
{
printf("%d ",next[i]);
}
printf("\n");
return 0;
}
算法总结第三弹 manacher算法,前面讲了两个字符串相算法——kmp和拓展kmp,这次来还是来总结一个字符串算法,manacher算法,我习惯叫他 “马拉车”算法。
相对于前面介绍的两个算法,Manacher算法的应用范围要狭窄得多,但是它的思想和拓展kmp算法有很多共通支出,所以在这里介绍一下。Manacher算法是查找一个字符串的最长回文子串的线性算法。
在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如abba,noon等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。
计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为O(n^3)的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为O(n^2),但是当n比较大的时候仍然无法令人满意,Manacher算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
下面介绍Manacher算法的原理与步骤。
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:
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。
首先从左往右依次计算Len[i],当计算Len[i]时,Len[j](0<=j
第一种情况:i<=mx
Len[i]=min(mx-i,Len[2*po-i]);//在mx - i 和 Len[j] 中取个小
//2*po-i 就是 j
分析:
找到i相对于po的对称位置,设为j,那么,
①如果Len[j]
那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i] >= Len[j]。因为Len[j] < mx - i,所以说i + Len[j] < mx。由对称性可知Len[i] = Len[j],即Len[i] = Len[2*po-i]。2*po-i就是j(i的对称点)P[i]初始化成该回文子串的值在进行扩展搜索回文子串的半径是否能够增大,省去了P[i]从0开始搜索的一些步骤。
②如果Len[j] >= mx - i,如下图。
由对称性,说明以i为中心的回文串可能会延伸到mx之外,而大于mx的部分我们还没有进行匹配,所以要从mx+1位置开始一个一个进行匹配,直到发生失配,从而更新mx和对应的po以及Len[i]。
所以两种情况取min就行啦。
第二种情况: i > mx
如果i比mx还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新mx的位置和对应的po以及Len[i]。
Manacher算法的时间复杂度分析和Z算法类似,因为算法只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于T字符串中的每一个位置,只进行一次匹配,所以Manacher算法的总体时间复杂度为O(n),其中n为T字符串的长度,由于T的长度事实上是S的两倍,所以时间复杂度依然是线性的。
下面是算法的实现,注意,为了避免更新P的时候导致越界,我们在字符串T的前增加一个特殊字符,比如说‘$’,所以算法中字符串是从1开始的。
牛客网AC代码:
http://www.nowcoder.com/practice/b4525d1d84934cf280439aeecc36f4af?tpId=49&tqId=29360&rp=1&ru=/ta/2016test&qru=/ta/2016test/question-ranking
class Palindrome {
public:
int getLongestPalindrome(string A, int n) {
// write code here
if(n < 1 || A.size() != n)
return 0;
string B = "@";//转换后的字符串
for(int i = 0; i < n; ++i) {
B += "#";
B += A[i];
}
B += "#@";
vector len(B.size(), 0);//存储以i为中心的回文串长度的数组
//初始化完成,下为Manacher算法计算过程
int po = 0, mx = 0, ans = 0;
//mx为当前计算回文串最右边字符的最大值,po是相对应的中心节点;ans用于return,记录的是vector中的最大值
for(int i = 1; i < B.size(); ++i) {
if(mx >= i)
len[i] = min(len[2 * po - i], mx - i);//在Len[j] 和 mx - i中取个小
else
len[i] = 1;//否则,只能从中心轴开始匹配
while(B[i - len[i]] == B[i + len[i]] && B[i + len[i]] != '@' && B[i - len[i]] != '@')
len[i]++;
if(len[i] + i > mx) {//若新计算的回文串右端点位置大于mx,要更新po和mx的值
mx = len[i] + i;
po = i;
}
ans = max(len[i], ans);
}//for循环结束
return ans - 1;//返回Len[i]中的最大值-1即为原串的最长回文子串的长度
}
};