KMP算法理解

来源:互联网 发布:mac 启动 磁盘工具 编辑:程序博客网 时间:2024/05/29 15:47

作者:高城
链接: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 vectorvoid 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 ~~~~

0 0