【数据结构】详解KMP算法

    xiaoxiao2021-04-13  26

    详解KMP算法

    1、基本算法

    KMP算法主要用来快速地在主串S内pos位置及以后匹配到子串T的位置。一般的匹配思路:定义两个指针p1和p2,分别指向主串S 的pos位置和子串T的第一个字符,然后比对两个指针处的字符是否相等,如果相等继续比对下一个字符,直到子串所有字符比对完;如果在该比对过程中,出现比对不成功,则将p1往后移一位,p2重新返回到子串的第一个字符,然后重新开始比对;若比对成功,则返回子串T在主串S的位置,否则返回0。其具体的算法代码如下所示:

    int index(string source, string sub, int pos){ char *ptrSource = &source[0]; char *ptrSub = &sub[0]; int len_source = source.length(); int len_sub = sub.length(); int i = pos, j = 1; while(i <= len_source && j<= len_sub){ if(*(ptrSource + i - 1) == *(ptrSub + j - 1)) { i++; j++; } else{ i = i - j + 2;//i = (i - (j - 1) + 1) j = 1; } } if(j > len_sub) return (i - len_sub); else return 0; }

    上述基本的算法虽然容易理解,但是会存在第一个指针p1不断回退以及算法复杂度较高的现象,为此D. E. Knuth和J. H. Morris以及V. R. Pratt提出了一种KMP算法,该算法主要的改进在于p1不会回退,子串在匹配时尽量的靠右“滑动”匹配主串T。

    为了形象的说明,举个例子:

    第一趟匹配:

    此时第三个字符不匹配,p1停留在主串T的T[2]处(下标从0开始)。

    第二趟匹配:

    此时第五个字符不匹配,p1停留在主串T的T[6]处。

    第三趟匹配:

    此时匹配成功,但是此轮匹配时,p2指向的不在是子串T的首字符串,而是第二个字符T[1]。并且在这三轮的匹配中,指向主串S的指针p1没有回退过。因此KMP算法中的重点在于确定下一轮匹配中,p2指针指向子串T的哪个字符,或者说,从子串T中的哪个字符与当前指向主串S的指针p2所指向的字符开始作比对。

    2、基本原理

    假设主串S如下所示:

    子串T如下所示:

    假设当子串的第j个字符在主串的第i个字符处匹配失败,并且重新开始新的一轮匹配时,主串中的第i个字符应该与子串的第k个字符重新比对(关系如下图所示)。

    主串S:

    子串T:

    此时将子串滑动;

    主串S:

    子串T:

    子串T滑动后,第k个字符和主串的第i个字符重新开始新一轮比对。

    子串T滑动前,对于主串S,此时Si-j+1~Si-1和子串T中的P1~Pj-1是已经匹配成功的(共匹配成功j-1个字符)。子串T滑动后,子串T的第k 个字符(k < j)跟主串S中的第i个字符进行比对(假设的情况),即Si-k+1~Si-1和子串T中的P1~Pj-1 是匹配的字符串(共匹配k-1个字符)。

    经过对比可以发现:在子串中,Pj-k+1~Pj-1对应的字符串和P1~Pk-1对应的字符串相同。

    于是,令 next[j] 表示当子串 T 中的第 j 个字符与主串 S 的当前字符( p1 指向的字符)匹配失败时,在子串 T 中需要重新和主串 S 中该字符进行比对的字符的位置。指向子串 T 的指针 p2 然后指向该字符,再进行比对。如果发现该新位置的字符仍然匹配不了,就继续将 p2 指向 next[next[j]] 位置的字符 … 一直进行下去。此时如果发现 p2 指向的子串的字符和 p1 指向的匹配了,则将 p1 和 p2 都加一让其指向下一个字符串。如果直到 next[…next[j]…]==0 ,然后将 p2 指向子串 T 的第一个字符, p1 指向主串 S 的下一个字符。

    KMP算法代码如下:

    int KMP(string source, string sub, int pos){ char *ptrSource = &source[0]; char *ptrSub = &sub[0]; int len_source = source.length(); int len_sub = sub.length(); int *next = new int [len_sub]; if(!next) exit(-1); getNext(sub, next); int i = pos, j = 1; while(i <= len_source && j <= len_sub){ if(j == 0 || *(ptrSource + i - 1) == *(ptrSub + j - 1)){ i++; j++; } else{ j = next[j-1]; } } if (j > len_sub) return (i - len_sub); else return 0; }

    以上KMP算法是基于子串对应的next数组的。此时,问题转化为怎么计算子串T对应的next数组了。

    3、next数组

    由定义得知,next[1] = 0(子串第一个字符都不匹配的时候,直接将子串整体移动一个字符);

    假设next[j] = k,则表示当子串T的第j个字符串与主串S中的当前字符比对失败时,重新将子串T 中的第k 个字符(k < j)和主串当前字符进行比对。因此会有:在子串中,Pj-k+1~Pj-1对应的字符串和P1~Pk-1对应的字符串相同

    求next[j+1]?(数学归纳法)

    当Pk==Pj时,如下图所示,当这两个字符相等时,当子串T的第j+1个字符和主串S匹配失败时,可以直接将子串的第k+1个字符对齐主串的当前字符。因此next[j+1] = k + 1。

    当Pk != Pj时,如下图所示,问题转化为子串T1的第j个字符Pk和子串T2第k个字符进行比对的情况。而此时需要按照子串T的next[k] = k1处的字符对齐到子串T1的第j个字符。如果此时的Pnext[k] == Pj,则表示next[k+1] = k1 + 1,如果此时Pnext[k] 和 Pj仍然不相等,则继续类推,直到Pj和子串中的某个字符匹配或者没有找到任何匹配的,则next[k+1]=1。

    求一个字符串对应的next数组算法代码如下:

    void getNext(string sub, int next[]){ int i = 1; next[0] = 0; int j = 0; char *p = &sub[0]; int len_sub = sub.length(); while (i < len_sub){ if(j == 0 || *(p + i - 1) == *(p + j - 1)){ ++i; ++j; next[i-1] = j; } else j = next[j-1]; } }

    转载请注明原文地址: https://ju.6miu.com/read-668614.html

    最新回复(0)