数据结构之字符串的模式匹配(朴素算法+KMP)
一、引言
子串的定位操作通常称为串的模式匹配,他求的是子串(常称模式串)在主串中的位置,这里采用定长顺序存储结构,给出一种不依赖于其他操作的暴力匹配算法
数据结构定义
首先,我们定义一个结构体 SString
来表示串,其中包含字符数组 ch
和整型变量 length
表示串的长度。
#define MAXLEN 255
typedef struct {
char ch[MAXLEN + 1]; // 用于存储字符的数组
int length; // 串的实际长度
} SString;
子串与模式串的区别
- 子串
是主串的一部分,他是一定
存在的 - 模式串
不一定
能在主串中找到
二、朴素算法(简单的模式匹配算法)
int Index(SString S,SString T){
int i=1,j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i]==T.length){
++i,++j;//继续比较后继字符
}
else{
i=i-j+2;
j=1; //指针后退重新开始匹配
}
if(j>T.length)
return i-T.length;
return 0;
}
}
注意点
- 使用此算法匹配失败时,主串指针i会
疯狂回溯
- 若主串有m个,模式串有n个,最多对比
n-m+1
个子串 - 该算法的最坏时间复杂度为
O(m+n)
三、KMP算法
KMP算法(Knuth-Morris-Pratt算法)是一种用于在文本串(主串)中查找子串(模式串)的高效算法。它通过利用已经部分匹配的信息来减少不必要的字符比较次数,从而提高查找效率。由三个伟人提出,各取一个字母命名
原理概述
KMP算法的关键在于构建一个next
数组,这个数组用于指示在匹配失败时,模式串应该向右移动多少位。其核心思想是利用模式串自身的特性,减少主串和模式串的重复比较。
next
数组的构建
next
数组是根据模式串自身的前缀和后缀的最长公共元素长度来构建的。具体步骤如下:
- 初始化:
next[1] = 0
,表示模式串只有一个字符时,无需移动。 - 遍历构建:从模式串的第二个字符开始计算,依次填充
next
数组。- 如果当前字符匹配失败(即主串和模式串当前比较的字符不相等),则根据
next
数组回退模式串的位置。 next[j]
的值表示当模式串的第j
个字符匹配失败时,模式串应该向右移动的位置。- 如果
T.ch[j]
和T.ch[next[j]]
相等,则next[j+1] = next[j] + 1
;否则,继续向前回溯,直到找到一个匹配或者无法再回溯为止。
- 如果当前字符匹配失败(即主串和模式串当前比较的字符不相等),则根据
算法实现
以下是一个简化的KMP算法实现,用于在主串S
中查找模式串T
的起始位置:
int Index(SString S,SString T,int next[]){
int i=1,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;
return 0;
}
}
应用及效率
KMP算法适用于需要频繁查找多个模式串的情况,如文本编辑器中的查找功能。它的时间复杂度为O(m + n)
,其中m
为主串长度,n
为模式串长度,具有较高的查找效率。
注意点
- KMP算法利用部分匹配的信息,避免在主串中回溯过多次数,从而提高了字符串匹配的效率。它的核心在于构建
next
数组,通过这一数组来指导模式串的移动,使得匹配过程更加智能和高效。 - next 数组与主串无关,及只知道模式串的元素,即可求出
- 时间复杂度为
O(m+n)
,其中,球next数组的时间复杂度O(m)
, 模式匹配过程中的最坏时间复杂度为O(n)
- 简化过程是:根据模式串T,求出 next 数组,在利用 next 数组进行匹配(主串指针不回溯)
- next 数组的快速求法:
- next[1] , next[2] 无脑为0
- 其他next,在不匹配的位置前,划一道优美的分界线,模式串一步一步往后退,直到分界线之前能对上,或者模式串完全跨过分界线,此时 j 指向哪里,next数组的值就是多少
五、KMP算法的进一步优化
通过主串与字符串的比较,我们可以知道在主串与字符串不相同的位置之前的元素是多少,由此,变通过构建 next 数组,来让模式串后移,提高效率。
除此之外,通过主串与字符串的比较,我们也可以知道,在不相同的位置,主串的元素一定不是模式串该位置的元素,由此,便可以多后移几位,来搭建 nextval 数组
,来进一步提高效率
我们可以通过以下这个方法,来快速求得 nextval 数组
nextval[1]=0;
for(int j=2; j<T.length; j++) {
if(T.ch[next[j]]==T.ch[j])
nextval[j]=nextval[next[j]];
else
nextval[j]=next[j];
}
- 优化思路
构建next 数组的同时,根据模式串当前位置的字符是否与 next 数组所指示的下一个字符相等,决定是否继承上一次的nextval 值。
六、总结
KMP算法通过 next 数组的 预处理
,实现了在字符串匹配过程中的高效移动模式串的能力,而 nextval 数组的优化进一步提升了算法的效率。通过合理的优化,KMP算法在实际应用中能够显著减少比较次数,提高匹配效率。
学习KMP算法时,应从暴力法的弊端入手
,思考如何去优化它,实际上,已匹配相等的序列就是模式串的某个前缀,因此每次回溯就相当于模式串与模式的某个前缀比较
,这种频繁的重复比较适合效率低的原因。这时,可以从分析模式串本身的结构入手
,以便得知当模式匹配到某个字符不相同时,应该往后滑动到什么位置,即已匹配相等的前缀和模式串若收尾重合,则对齐他们,对齐的部分显然无需再比较,下一步则是直接从主串的当前位置继续进行比较