朴素的模式匹配算法和快速模式匹配算法(KMP)
字符串的模式匹配
- 寻找字符串p在字符串t中首次出现的起始位置称为字符串的模式匹配,其中称p为模式(Pattern),t为正文(Text)
1、朴素的模式匹配算法 - 基本思想:用p中的每个字符去与t中的字符一一比较。比较成功返回1;比较失败,正文t的下标向后移动一位,直到比较成功。如若t的剩余字符的长度小于p的长度还未匹配成功,则返回-1。
- n代表正文t的长度,m代表模式字符串p的长度。
int index(seqstring p,seqstring t) {
int i = 0;//扫描正文t
int j;//扫描模式p
int success = 0;//匹配成功的标志
while((i<=p.length - t.length) && (!success)) {//如果整体匹配不成功,且t剩余字符长度够长。
j = 0;
success = 1;
while((j < p.length) && success) {//局部匹配成功
if(p.str[j] != t.str[i + j])
success = 0;
else
j ++;
}
i ++;
}
if(success)
return (i - 1);
else
return -1;
}
- 时间复杂度O((n-m+1)*m)
2、快速模式匹配算法(KMP算法)
- 4-1表示此次匹配从p0与tk开始比较,当比较到pi与tr时出现不等情况。
- 4-2表示【p0pi-2】=【p1pi-1】。模式p中pi之前存在长度为i-1的真前缀和真后缀的匹配。
- 4-3表示【p0pi-3】=【p2pi-1】。模式p中pi之前存在长度为i-2的真前缀和真后缀的匹配。
- 考虑一般情况:如模式p中pi之前最长真前缀和真后缀匹配的长度为j,当pi !=tr时,则下一步只需从pj与tr开始继续后继对应字符比较。
- 根据上述分析,在模式匹配的过程中,每当出现pi != tr时,下一次与tr进行比较的pj和模式p中pi之前最长真前缀和真后缀匹配的长度密切相关。
- 于是,可以针对模式p定义一个数组next[m],其中next[i]表示当pi != tr时,pi之前最长真前缀和最长真后缀的长度j,那么,获取到这个值后,下一次比较就可以从pj开始和tr比较,即pnext[i]和tr比较。
//求next数组
void getnext(seqstring p,int next[]) {
int i = 0;
int j = -1;
next[0] = -1;
while(i < p.length-1) {
if(j==-1 || p.str[i] == p.str[j]) {
next[++ i] = ++ j;
} else {
j = next[j];
}
}
}
- next[0] = -1 表明当p0 != tr时将从p-1与tr开始继续后记对应字符的比较;然而p-1是不存在的,我们可以将这种情况理解成下一步将从p0与tr+1开始后记对应字符比较。
- next[i]时已得到pi之前的最长真前缀和真后缀的长度j。
- 如果此时进一步有pj=pi,则pi+1之前的最长真前缀与真后缀的匹配长度为j+1,即next[i+1]=j+1。
- 如果此时进一步有pj != pi,这里本质上还是一个模式匹配问题(p和p模式匹配),因此应该检查pnext[j]和pi是否相等,如果相等,则next[i+1]=next[j] + 1。如果不相等,则再取pnext[next[j]]与pi进行比较,直至要将p-1与pi比较为止,此时next[i+1]=0.
//字符串KMP模式匹配算法实现
int kmp(seqstring t,seqstring p,int next[]) {
int i = 0;
int j = 0;
while(i<t.length && j < p.length) {
if(j == -1 || t.str[i] == p.str[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if(j == p.length) //模式匹配成功
return i-p.length;
else //模式匹配失败
return -1;
}
- 时间复杂度O(m+n)
3、KMP模式匹配小结 - 这里的重点就在于最长真前缀和真后缀的是否相同。
- 当某处出现不匹配的时候,我们就可以通过最长真前缀和最长真后缀的匹配长度j来移动p并且从pj开始继续比对,而就不仅仅是像朴素模式匹配算法那样移动一位且从头开始比对。