一、介绍
串的模式匹配其实就是找到子串在主串中的位置。例如:串A='ABCD' 字符B在串A中的位置就是2,串BC在A中的位置就是2。位置不是位序,位置是从1开始。
介绍串的定长顺序存储表示
串的结构类似于线性表的顺序存储结构,串是用一组地址连续的存储单元存储串值的字符序列。一般用一个定长的数组来表示串的每个字符分量(为了表示串是从1开始而不是0一般将数组位序为0的位置舍弃):如串中保存串 ABCD。
串长有两种表示方法:
方法一是使用一个额外的变量len来存放串的长度 如下图
方法二是在串值后面加一个不计入串长的结束符“\0”。此时串长为隐含值 如下图
就用方式二 直接指定length长度 最后一个字符是“\0”也方便计算
方法三是使用一个额外的变量len来存放串的长度 如下图(一般就用这种)
而串的数据结构是这样的:
typedef struct{
char ch[MaxSize];
int length;
}SString;
串的生成(直接给出代码 含义见注释):
//串生成 直接输入字符数组即可
void StrAssign(SString& S, char chars[])//生成串S
{
int i = 0;
while (chars[i] != '\0')
{
//第一个位置不用
S.ch[i+1] = chars[i];//将字符串常量的值赋值给S
i++;
}
S.length = i;
}
二、串的模式匹配
串的模式匹配其实就是找到子串在主串中的位置。通过暴力算法可引出KMP算法的好处,先看看暴力算法:
#include<iostream>
#include<cstring>
using namespace std;
#define MAXLEN 255 //预定义最大串长为255
struct SString {
char ch[MAXLEN]; //每个分量存储一个字符
int length; //串的实际长度
};
//S为主串 T为子串
int Index(SString S, SString T) {
int i = 1, j = 1;
while (i <= S.length && j <= T.length)
if (S.ch[i] == T.ch[j]) { //继续比较后续字符
++i;
++j;
} else {
i = i - j + 2; //指针后退重新开始匹配
j = 1;
}
if (j > T.length)
return i - T.length; //匹配成功
else
return 0; //匹配失败
}
int main() {
SString S, T;
strcpy(S.ch, "#ababcabcacdab");
S.length = 13;
strcpy(T.ch, "#abcac");
T.length = 5;
cout << Index(S, T) << endl;
return 0;
}
三、两种模式匹配算法的比较
朴素模式匹配算法:
朴素模式匹配,主串指针有回溯
KMP算法:
在保证指针 i 不回溯的前提下,当匹配失败时,让模式串向右移动最大的距离;
二、KMP 算法的实现
KMP算法相较于朴素模式匹配算法的优势在于:在保证指针 i 不回溯的前提下,当匹配失败时,让模式串向右移动最大的距离
为此计算模式串向右移动的距离是极为重要的,一般使用 next 数组来存储相关距离值。
1. next 数组的计算
要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值。
前缀指除最后一个字符以外,字符串的所有头部子串;
后缀指除第一个字符外,字符串的所有尾部子串;
部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。
下面以’ ababa’为例进行说明:
‘a’ 的前缀和后缀都为空集,最长相等前后缀长度为0。
‘ab’ 的前缀为{a),后缀为{b},{a}n{b} =,最长相等前后缀长度为0。
'aba’的前缀为{a,ab},后缀为{a,ba},{a,ab)n{a, ba}={a),最长相等前后缀长度为l。
'abab’的前缀{a,ab,aba}n后缀{b,ab,bab}={ab},最长相等前后缀长度为2。
'ababa '的前缀{a,ab,aba,abab}n后缀{a, ba,aba,baba}={a,aba},公共元素有两个,最长相等前后缀长度为3。
故字符串’ ababa’的部分匹配值为 00123。
(二)、由部分匹配值得到next数组
对算法的改进方法:
已知:右移位数 = 已匹配的字符数 - 对应的部分匹配值(PM)。
写成:Move=(j-1)-PM[j-1]。
使用部分匹配值时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将PM表右移一位,这样哪个元素匹配失败,直接看它自己的部分匹配值即可。
将上例中字符串’abcac’的PM表右移一位,就得到了next数组:
我们注意到:
1)第一个元素右移以后空缺的用-1来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,而不需要计算子串移动的位数。
2)最后一个元素在右移的过程中溢出,因为原来的子串中,最后一-个元素的部分匹配值是其下一个元素使用的,但显然已没有下一个元素,故可以舍去。
这样,上式就改写为
Move=(j-1)-next[j-1]
相当于将子串的比较指针j回退到
j=j-Move=j-((j-1)-next [j] )=next.[j]+1
有时为了使公式更加简洁、计算简单,将next数组整体+1。.因此,上述子串的next数组也可以写成
最终得到子串指针变化公式 j = next[j]。在实际匹配过程中,子串在内存里是不会移动的,而是指针在变化,书中画图举例只是为了让问题描述得更加形象。
next[j] 的含义是:在子串的第 i 个字符与主串发生失配时,则跳到子串的next[j] 位置重新与主串当前位置进行比较。
2.next 数组的算法实现:
void get_next(SString T, int *next){
int i = 1;
int j = 0;
next[1] = 0;
while (i < T.length){
if (j==0 || T.ch[i] == T.ch[j]) {
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
3.基于next的KMP算法的实现:
int Index_KMP(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; //匹配成功
else
return 0;
}
性能分析
尽管普通模式匹配的时间复杂度是O(mn)
,KMP 算法的时间复杂度是Om+n)
,但仕双俏况下,普通模式匹配的实际执行时间近似为O(m +n),因此至今仍被采用。KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯
。
最坏时间复杂度:O(m+n)
其中,求 next 数组时间复杂度为 O(m),模式匹配过程最坏时间复杂度 O(n)
4. KMP 算法完整代码:
#include <stdio.h>
#include <string.h>
void Next(char *T, int *next){
int i = 1;
next[1] = 0;
int j = 0;
while (i<strlen(T)) {
if (j==0 || T[i-1]==T[j-1]) {
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
}
int KMP(char *S, char *T){
int next[10];
Next(T, next); //根据模式串T,初始化next数组
int i = 1;
int j = 1;
while (i<=strlen(S)&&j<=strlen(T)) {
//j==0:代表模式串的第一个字符就和当前测试的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移
if (j==0 || S[i-1]==T[j-1]) {
i++;
j++;
}
else {
j = next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值
}
}
if (j>strlen(T)) { //如果条件为真,说明匹配成功
return i-(int)strlen(T);
}
return -1;
}
int main() {
int i = KMP("ababcabcacbab", "abcac");
printf("%d", i);
return 0;
}
运行结果:
6