最近在看算法,看到KMP算法,其实有点绕,不是太好理解,就介绍一下自己对于这个算法的理解以及实现。
KMP算法思想
Knuth-Morris-Pratt算法(简称KMP),是由D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的一个改进算法。KMP算法是模式匹配中的经典算法,和BF算法(又称蛮力匹配算法,需要通过不断回溯依次移动进行模式串的匹配)相比,KMP算法的不同点是消除BF算法中主串S指针回溯的情况,从而完成串的模式匹配,这样的结果使得算法的时间复杂度为O(n+m)。
KMP算法描述
KMP算法每当一趟匹配过程中出现字符比较不等时,主串S中的i指针不需回溯,而是利用已经得到的“部分匹配”结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。
下面主要介绍KMP算法的主要核心其实就是next数组和nextval数组的求解,然后就可以顺理成章的完成匹配任务。
next算法
先定义一下主串为“S1S2...Sn”,模式串为“T1T2...Tm”
next算法主要就是来确定当匹配过程中产生“失配”时,模式串“向右滑动”可滑动的距离有多远,也就是说,当主串中字符与模式串中字符“失配”时,主串中字符应与模式串中的哪个字符再进行比较?
然后说一下next算法中涉及到的重要的“前缀”和“后缀”的概念
"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
以"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。
说一下自己对于next数组的理解,构建数组的过程其实就是,以前一个字符Tj为基础,next[j] = k, 这表明在模式串中存在‘T1T2...Tk-1’=‘Tj-k+1Tj-k+2...Tj-1’,也就是说在j-1个字符中,前k-1个字符(前缀)和后k-1个字符(后缀)相等,如果增加一个Tj字符,求next[j+1]的值,可以分为两种情况:
(1)如果Tj = Tk, 则表明在模式串中‘T1T2...Tk’=‘Tj-k+1Tj-k+2...Tj’,也就是代表着加入一个字符后,前k个字符和后k个字符是相等的,这个时候只需要next[j+1] = next[j] + 1 就可以了。
(2)如果Tj ≠ Tk, 则表明在模式串中‘T1T2...Tk’≠‘Tj-k+1Tj-k+2...Tj’。此时可以把求next函数值看做是一个模式匹配的问题,整个模式串既是主串又是模式串。我们匹配不到k个值,完全可以通过之前保存的前k个值的next[]的值去找小于k个值k’(1<k’<k<j)的前缀和后缀的匹配,也就是要通过next[k]的值去进行判断。
① 如果Tj = Tk’, 则next[j+1] = next[k] + 1
② 如果Tj ≠ Tk’, 则k’ = next[k], 然后一直重复下去,若指导最后j=0时都一直比较不成功,则next[j] = 1。
上代码:
void Get_Next(SString T, int next[])
{
int k = 0, j = 1;
next[1] = 0;
while(j < T.length)
{
if (k == 0 || T.ch[j] == T.ch[k])
{
++j;
++k;
next[j] = k;
}
else
k = next[k];
}
}
nextval算法
nextval算法思想比较简单,就是当遇到类似于“AAAAB”这样的匹配串时,前面4个字符是相同的,如果通过next值进行移动的话比较耗时,完全可以通过直接向右滑动4个字符的位置直接进行比较最快。
算法思想
计算第j个字符的nextval值时,要看第j个字符是否和第j个字符的next值指向的字符相等。若相等,则nextval[j] = nextval[next[j]];否则,nextval[j] = next[j]。(可以理解nextval就是对next做了进一步的优化,因为nextval是在next数组的基础上计算的)
上代码:
void Get_NextVal(SString T, int next[], int nextval[])
{
int j = 2, k = 0;
Get_Next(T, next);
nextval[1] = 0;
while(j <= T.length)
{
k = next[j];
if (T.ch[j] == T.ch[k])
nextval[j] = nextval[k];
else
nextval[j] = next[j];
j++;
}
}
KMP匹配算法
最后把KMP最终匹配的算法的代码附上:
int Index_KMP(SString S, int pos, SString T)
{
int i = pos, j = 1;
while (i <= S.length && j <= T.length)
{
if (j == 0 || S.ch[i] == T.ch[j])
{
++i;
++j;
}
else
j = next[j]; // 模式串向右滑动
}
if (j > T.length)
return i - T.length; // 匹配成功时,返回匹配起始位置
else
return 0;
}
KMP算法分析
KMP算法是在已知模式串的next或nextval的基础上执行的,如果不知道它们二者之一,则没有办法使用KMP算法。虽然有next和nextval之分,但它们表示的意义和作用完全一样,因此在已知next或nextval进行匹配时,匹配算法不变。
通常,模式串的长度m比主串的长度n要小很多,且计算next或nextval函数的时间复杂度为O(m)。因此,对于整个匹配算法来说,所增加的计算next或nextval是值得的。
BF算法的时间复杂度为O(n*m),但是实际执行近似于O(n+m),因此至今仍被采用。KMP算法仅当模式串与主串之间存在许多“部分匹配”的情况下,才会比BF算法快。KMP算法的最大特点是主串的指针不需要回溯,整个匹配过程中,主串仅需从头到尾扫描一次,对于处理从外设输入的庞大文件很有效,可以边读边匹配。