我一直在努力了解KMP算法。我仍然没有清楚地理解kmp算法背后的推理。假设我的文字为bacbababaabcbab
,格式为abababca
。通过使用与sub(pattern)
的正确后缀匹配的最长正确前缀sub(pattern)
的长度规则,我填充了table[]
。
a b a b a b c a
0 0 1 2 3 4 0 1
现在我开始使用我的模式和表格对文本应用KMP算法。
在找到上述文本的索引4后,我们通过查看length(l)=5;
匹配table[l-1]=3;
根据KMP算法,我们可以跳过长度最多2个字符并可以继续。
bacbababaabcbab
---- XX |||
abababca
这里我没有得到转移背后的逻辑。我们为什么要转变?有人可以澄清我的困惑吗?
答案 0 :(得分:5)
要理解KMP算法背后的逻辑,首先应该了解,这种KMP算法如何比蛮力算法更好。
Idea
在模式转换之后,朴素算法忘记了有关先前匹配符号的所有信息。因此,它可能会一次又一次地将文本符号与不同的模式符号进行重新比较。这导致其最坏的情况复杂度为Θ(nm)(n:文本的长度,m:模式的长度)。
Knuth,Morris和Pratt [KMP 77]的算法利用了先前符号比较所获得的信息。它永远不会重新比较匹配模式符号的文本符号。因此,Knuth-Morris-Pratt算法搜索阶段的复杂性在于O(n)。
但是,为了分析其结构,需要对模式进行预处理。预处理阶段的复杂度为O(m)。由于m <= n,Knuth-Morris-Pratt算法的总体复杂度为O(n)。
text:bacbababaabcbab 图案:abababca
在蛮力方法中, 将图案逐个滑动到文本上并检查是否匹配。如果找到匹配项,则再次按1滑动以检查后续匹配。
void search(char *pat, char *txt)
{
int M = strlen(pat);
int N = strlen(txt);
/* A loop to slide pat[] one by one */
for (int i = 0; i <= N - M; i++)
{
int j;
/* For current index i, check for pattern match */
for (j = 0; j < M; j++)
{
if (txt[i+j] != pat[j])
break;
}
if (j == M) // if pat[0...M-1] = txt[i, i+1, ...i+M-1]
{
printf("Pattern found at index %d \n", i);
}
}
}
上述算法的复杂度为O(nm)。 在上面的算法中,我们从未使用过我们处理过的比较数据,
Bacbababaabcbab //let I be iterating variable over this text
Abababca //j be iterating variable over pattern
当i = 0且j = 0时,存在不匹配(text [i + j]!= pat [j]),我们递增i直到匹配为止。 当i = 4时,有一个匹配(text [i + j] == pat [j]),增加j直到我们发现不匹配(如果j = patternlength我们发现模式),在给定的例子中我们发现j =不匹配5当i = 4时,在文本中的idex 4 + 5 = 9处发生不匹配。匹配的子字符串是ababa, **
Why we need to choose longest proper prefix which is also proper suffix :
**
从上面可以看出:我们看到不匹配发生在9,其中模式以ababa子串结束。
现在,如果我们想要利用我们迄今为止所做的比较,那么我们可以跳过(递增)i超过1,然后比较的数量将减少,从而导致更好的时间复杂度。
现在我们可以对处理后的比较数据“ababa”采取什么优势。
如果我们仔细看到:字符串ababa的前缀 aba 与文本进行比较并匹配,后缀 aba 的情况也是如此。但是'a'与
Bacbababaabcbab
||||||
||||| x
|||| ||
ababab
但是根据天真的方法,我们将i增加到5.但是我们知道通过查看它,我们可以设置i = 6,因为aba的下一次出现发生在6.因此,我们不是要与文本中的每个元素进行比较预处理模式以找到最长的正确前缀,这也是正确的后缀(称为边界)。在上面的'ababa'示例中,最长边界的长度为3( aba )。因此增加:子串的长度 - 最长边界的长度=&gt; 5-3 = 2 如果我们的比较在aba处失败,则最长边界的长度为1且j = 3,因此增加2。
有关如何预处理的更多信息:http://www-igm.univ-mlv.fr/~lecroq/string/node8.html#SECTION0080 http://www.inf.fh-flensburg.de/lang/algorithmen/pattern/kmpen.htm
答案 1 :(得分:1)
我不确定你在这一点上是否有理解的问题,所以,如果你不介意的话,我只会描述(尽可能多的解释)整个算法。您的问题的答案可能在最后一段,但您最好全部阅读以更好地理解我的术语。
在KMP算法中,实际上,您计算的值几乎与表中的值相同(通常称为前缀函数)。因此,当您在文本中定位i时,您需要计算在位置i中结束的文本中的子字符串的最大长度,该长度等于模式的某个前缀。很明显,当且仅当该子串的长度等于模式的长度时,您才在文本中找到该模式。
那么,你如何快速计算这个前缀函数值? (我想你使用一些O(n ^ 2)算法计算这些模式,这是不够快的)。
我们假设我们已经为文本的第一个i-1
符号做了所有操作,现在我们正在使用位置i
。我们还需要前一个文本符号的前缀 - 函数值:p[i-1]
。
让我们比较text [i]和pattern [p [i-1]](如果你不介意的话,将索引从0开始)。我们已经知道pattern[0:p[i-1]-1] == text[i-1+p[i-1],i-1]
:这是p[i-1]
的定义。所以,如果text[i] == pattern[p[i-1]]
,我们现在知道pattern[0:p[i-1]] == text[i-1+p[i-1]
,i]',这就是p [i] = p [i - 1]的原因。但有趣的部分始于text[i] != pattern[p[i-1]]
。
当这些符号不同时,我们开始跳跃。原因是我们希望尽可能快地找到下一个可能的前缀。那么,我们如何做到这一点。只需查看图片here并按照说明(黄色部分是text[i-1]
找到的子字符串)。我们正在尝试找到一些字符串s
:s:=s1+text[i]
。由于前缀函数定义s1=s2, c=test[i]
。但我们已经知道(通过查找text[i-1]
的值)图片中的两个黄色部分是相同的,因此s3
实际上等于s1
,所以s3=s1
。所以我们可以找到s1
的长度:它是table[p[i-1]]
。现在,如果c1=text[i]
,我们应该停止:我们找到p[i]
,它是s1.length + 1
。如果c1!=text[i]
,我们可以重复相同的跳跃,现在查看模式的第一个table[table[p[i-1]]]
符号,所以我们继续直到找到答案,或者我们到达前0个符号在这种情况下p[i]:=0
。