朴素串匹配算法说明
串匹配算法最常用的情形是从一篇文档中查找指定文本。需要查找的文本叫做模式串,需要从中查找模式串的串暂且叫做查找串吧。
为了更好理解KMP算法,我们先这样看待一下朴素匹配算法吧。朴素串匹配算法是这样的,当模式串的某一位置失配时将失配位置的上一位置与查找串的该位置对齐再从头开始比较模式串的每一个位置。如下图所示。
KMP串匹配算法解析
KMP串匹配算法是Knuth-Morris-Pratt算法的简称,KMP算法的思想就是当模式串的某一位置失配时,能不能将更前面的位置与查找串的该位置对齐,并且直接从该位置开始比较。按照这个思路走,问题叫变成了:当模式串的某一位置失配时要找到一个更前面的位置与查找串的该位置对齐。模式串的某个位置失配时的这个更前面的位置就叫做回溯位,通常用next表示,它的计算公式是:
next[i]= { 0; 当 i = 1
k; 对于串P,存在1 <= k < 使得 P1..Pk-1 == Pi-k+1..Pi-1
1; 其他情况 }
这个公式对应的串的下标是从1开始的。这个公式只说明:模式串中某一位置(不包含此位置)之前部分具有首尾相同的子串(即自匹配,比如ABCABA最后一个A之前头部和尾部都包含了子串AB)时,如果该位置失配可以直接将头部子串的下一个位置和该处对齐(比如模式串ABCABA在最后一个A处失配可以直接滑动模式串将C对齐原来最后那个A对齐的位置),这样可以去掉模式串在某位置失配时该位置之前的子串在朴素匹配算法中存在的冗余比较(如果用朴素匹配算法,需要将模式串ABCABA移动三次才能使得C对齐原来最后那个A对齐的位置)。模式串中某一位置(不包含此位置)之前部分不具有首尾相同的子串时,在该位置失配时可以直接让模式串的开始位置对齐该位置。如下图。
这里只给出了算法的说明,但是如何能够证明算法是正确的呢?这个说麻烦也麻烦,说简单也简单。为什么麻烦呢?因为我没办法用形式化的语言给出证明过程,就像数学里面的证明过程一样。其实自己通过形象思维演示一下串匹配的滑动过程就能够相信这个算法肯定是正确的。我也懒得给出证明过程。
接下来给出KMP算法的完整代码。
#include <iostream> #include <iomanip> #include <vector> #include <string> #include <cstdlib> using namespace std; void get_next(const string & M,vector<int> & next); int KMP_match(const string & S,const string & M,int pos); int main( ) { string S="abcdefghabcdefghhiijiklmabc"; string T="hhiij"; int pos =KMP_match(S,T,3); cout<<"\n"<<pos<<endl; system("pause"); return 0; } void get_next(const string & M,vector<int> & next) { //按模式串生成vector<int> next(M.size(),-1); //这里的串的第1个元素下标是0 int i = -1, j = 0; int M_len = M.size()-1; do { if((i < 0) || (M[i] == M[j])) { i++; j++; next[j] = i; } else i = next[i]; cout<<"i="<<right<<setw(3)<<i <<" j="<<right<<setw(3)<<j <<" next["<<j<<"] ="<<right<<setw(3)<<next[j]<<endl; }while( j < M_len); } int KMP_match(const string & S,const string & M,int pos) { int j = pos, i = 0;//这里的串的第1个元素下标是0 int S_len = S.size(); int M_len = M.size(); if((S_len-pos) < M_len) return -1; vector<int> next(M.size(),-1); get_next(M,next); while (i<M_len && j<S_len) { if (i < 0 || S[j]==M[i]) { ++i; ++j; } else i = next[i];//j不变,i跳动 } if (i == M_len) return j-i;//匹配成功 else return -1; }
KMP串匹配算法的优化
接着看上面EBAEB的匹配例子。其中第2次比较根本没有必要,可以直接跳到第3次。这次比较有一个特点:当模式串滑过一段距离后模式串中参与比较的字符和前次参与比较的字符相同,都是B。按照上面那个公式求上述模式串回溯位置时的一个情形如下:
此时确定的是模式串中索引为4的元素失配时模式串的回溯位置,这时索引位下一个元素和比较位的下一个元素相同(都为B)。同时这也是模式串中索引为4的字符失配后滑动完成的情形,即最后一个B和查找串种的D不匹配时,由于next[4] = 1,需要把索引为1的字符(就是第2个字符)B对其到D。但是这个B和D是不是已经比较过一次了啊。这是因为不仅模式串失配位置之前的部分能够自匹配,而且模式串中包含失配位置的之前部分也能自匹配。模式串中包含失配位置的之前部分组成的子串也具有相同的首尾时,失配位置的回溯位置可以直接采用首部字串的回溯位置,对于串EBAEB,可以让next[4]直接等于next[1]。那么优化的next函数如下:
next[i]= { 0; 当i = 1 k; 对于串P,存在1 <= k < 使得 P1..Pk-1 == Pi-k+1..Pi-1 & Pk != Pi next[k]; 对于串P,存在 1 <= k < i使得 P1..Pk == Pi-k..Pi 1; 其他情况 }
接下来给出改进后的就next数组的代码。
void get_next(const string & M,vector<int> & next) { //按模式串生成vector<int> next(M.size(),-1); //这里的串的第1个元素下标是0 int i = -1, j = 0; int M_inx = M.size() - 1; do { if((i < 0) || (M[i] == M[j])) { i++; j++; if(M[i] != M[j]) next[j] = i; else next[j] = next[i]; } else i = next[i]; }while( j < M_inx); }