作者:高城
链接:https://www.zhihu.com/question/36149122/answer/66867065
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
面试官:
现在请你把我当做完全不了解KMP算法的人,向我解释一下KMP算法的原理。
面霸:
KMP算法俗称“看(K)毛(M)片(P)算法”,用于快速文本匹配。试想你有两条字符串src和pat,需要判断pat是否在src中出现,如果出现就给出出现的具体位置。假设src的长度为N,pat的长度为M,首先我们来讨论一下一个平凡的蛮力解。
如果你想判断两个字符串是否相等,你会写一个这样的循环(在纸上写下):
for(int i = 0; i < M; i++) if(src[ i ] != dst[ i ]) return false;
return true;
这里你最多比较了M次。回到原来的问题,有了这个子函数,你只要依次判断(一边在纸上写符号)src[i, i + M - 1], i = 0, 1, 2, … 与dst是否相等就行了。复杂度是N乘以M。
那么如何优化呢?我换一种说法,为什么能够优化呢?因为你做了重复的事情。我们换一种写法,就能更直观地发现重复在哪里。
void bruteMatch(char* src, char* pat, vector<int> &ans)
{
int N = strlen(src), M = strlen(pat);
if(M == 0 || N < M) return;
int i = 0, j = 0;
while(i <= N - M)
{
j = 0;
while(j < M && src[i] == pat[j])
{
i++; j++;
}
if( j == M ) ans.push_back(i - M);
i = i - j + 1;
}
}
你看这个变量i(一边用手指),它每次比较完一轮,就退回到原来的位置的下一个位置重新开始匹配。难道我刚刚做了那么多次比较,得到的信息就白白扔掉了吗?KMP三人找到了一种简单的利用这信息的方法,只要花点力气对模式串pat做一下预处理,就能使这匹配程序里的循环变量i只进不退,从而达到N+M的复杂度。
我们把这个匹配过程想象成,模式串依附着源串向后移动。你看(一边画图),i在这个位置,j在这个位置,走了这么一段路才发生失配了,意味着这两条(src[i - j, i - 1]和pat[0, j-1])是公共子串,也就是说此时src的这条子串的信息完全包含于pat的前缀子串之中,原则上我如果对pat做了充分的了解,就可以保持i不变,而单单令pat往后移动。假如我可以令pat移动一步而i不变,那说明这两大段是相等的;假如这两大段不相等,那么我至少可以令pat移动两步。观察发现,我应该令pat移动多少步,取决于pat[0, j - 1]的最长的相等{前缀、后缀}的长度。
KMP的精髓就在于,用了一个线性的算法,得到了每次在pat[ j ]发生失配时,应该让pat往后移动多少步,这个值对应于pat[0, j - 1]的最长相等{前缀、后缀}的长度。这些值所存的数组叫做next数组。
我需要写出计算next数组的函数,才能具体解释这个算法。
面试官:
可以了,说到最长相等前后缀这点就足够了。接下来请你手写一个归并排序的代码……
附一套高效的KMP代码:
void get_next(const string &pat, vector<int> &next)
{
// 通过该函数得到next数组之后,当在src[i]和pat[j]处发生失配时,保持i不动,
// j变更为next[j],就相当于把pat向后移动了(j - next[j])步
int i = 0, j = -1, p_len = pat.length();
next[0] = -1;
while (i < p_len)
{
if (j == -1 || pat[i] == pat[j])
{
i++;
j++;
if (pat[i] != pat[j])
next[i] = j;
else
next[i] = next[j];
}
else
{
j = next[j];
}
}
}
这个函数的过程可以看作将pat和自身做匹配的过程。第13行至第16行的判断比较令人费解,其实那是一个加速优化。语句j = next[ j ]也可以看作用动态规划做的优化。get_next可以写成下面的非优化形式,并且它的复杂度也是O(M):
void get_next(const string &pat, vector<int> &next)
{
// 通过该函数得到next数组之后,当在src[i]和pat[j]处发生失配时,保持i不动,
// j变更为next[j],就相当于把pat向后移动了(j - next[j])步
int i = 0, j = -1, p_len = pat.length();
next[0] = -1;
while (i < p_len)
{
if (j == -1 || pat[i] == pat[j])
{
i++;
j++;
next[i] = j;
}
else
{
j--;
}
}
}
然后是利用next数组进行匹配的代码:
//add the positions to a vector
void indice_kmp(const string &str, const string &pat, vector<int> &ans)
{
int i = -1, j = -1;
int len1 = str.length(), len2 = pat.length();
vector<int> next(len2 + 2, 0);
get_next(pat, next);
while(i < len1)
{
if (j == -1 || str[i] == pat[j])
{
i++;
j++;
if(j == len2){
ans.push_back(i - len2);
}
}
else
{
// (j - next[j])就是pat向后移动的步数
j = next[j];
}
}
}
关于复杂度:无论是get_next还是indice_kmp,在每个while循环单体中,存在一个严格增量:i + (模式串往后移动的步数),因此该算法的复杂度是O(N + M).
最后说一句:看毛片有害身心健康。
有首歌是这么唱的:
You VOTE ME UP~~~~ so I can stand on moutains ~~~~