KMP算法是模式匹配的一种算法,他的思想是,当我们进行模式匹配的时候,不用像暴力解法那样一个一个字符去比较,而是从前一串字符的最大前后缀开始匹配,这样就节省了不少时间
数学推导:
假如i代表主串指针,j代表子串指针
如果有:
i2 == j2;
j1 != j2;
那么i2 != j1,我们在后一个字符失配 的时候,可以直接跳过i2
先来看暴力算法
//截取字符串//
Status SubString(String a[], int pos,int t,String b[]) {
//对字符串a,取从pos开始且长度为t的子串传给b
//这里的pos是值数组元素下标
int i, j;
if (strlen(a) < t - pos) {
printf("字符串长度过小");
return ERROR;
}
for (i = pos,j =0 ; t > 0; t--,i++,j++) {
b[j] = a[i];
}
b[j] = '\0';//这一步一定要有,没有的话就会变成烫
return OK;
}
//模式匹配的暴力算法//
Status Brute_Force(String a[], String b[], int pos) {
//在a中pos之后寻找b的子串,找到的话返回元素下标,否则返回0
int n, m, i;
String sub[255];
if (pos > 0) {
//我们之后第0号元素都存放字符串长度
n = strlen(a);
m = strlen(b);
i = pos;
while (i <= n - m + 1) {
//n-m+1为i可以寻找的元素下标的最大范围
SubString(a, i, m, sub);
if (strcmp(sub, b) != 0) {
//如果截取的字符串和子串不等的话
i++;
}
else {
//如果找到了的话
printf("匹配的位置开始坐标为%d\n", i);
return i;
}
}
//运行到这里代表没找到
printf("\n不匹配\n");
return ERROR;
}
}
接下来看KMP算法,KMP算法分两部分,第一部分是求next数组,也就是当某一个位置失配的时候,应该直接跳到子串的哪一个位置进行比较(这里说的就是失配的那个位置)。。
next数组第一个元素都是等于0的,第0个元素不用管,后面的元素就是前面的字符串最大匹配的前后缀数量加一。
比如第二个字符和第一个字符不一样,那么第二个字符前面的字符串数量就是1(只有一个字符),但是匹配的前后缀为0(1个字符不存在匹配的前后缀),那么next[2]=0+1=1;
next数组的写入函数:
//next数组的实现
void get_next(String b[],int *next) {
//next函数0号元素不存放数据
//1号元素存放的是0
//从二号元素开始判定前面字符串最大匹配前后缀的字母个数,并且吧个数加一存放入数组中
//i表示后缀,j表示前缀
//如果T[i]==T[j],i和j就加一,并且next[i]=j,这是因为这里已经判定了最大匹配前后缀
//如果不匹配的话,j就要回溯到当前元素失配时的第一个指导元素
//然后再进行判断
//在整个过程中,前缀是固定的,而后缀是相对的
int i = 1, j = 0;//i是后缀,j是前缀
printf("b[0]=%d\n", b[0]);
next[1] = 0; //这个别忘了
while (i < b[0]) {
//0号元素存放字符串长度,所以总长度要减一
if (j == 0 || b[i] == b[j]) {
//如果这两个值相等的话,代表后一个元素的next值为最大匹配前后缀j加一
i++;
j++;
next[i] = j;
}
else {
//如果不相等或者j没有退回到0(也就是不会i处的next值不等于1)
//回溯
j = next[j];
}
}
}
然后就是KMP算法,当我们得到了next数组之后,就可以用数组进行模式匹配,还是和暴力解法一样,ij分别指向主串和子串(模式串),加入相等就ij分别加一,不相等就用next数组为向导对模式串进行指南(指导它该从哪一个位置开始进行比较)。
//KMP算法主函数
Status Index_KMP(String a[], String b[], int pos) {
//在a中查询pos之后的能与b匹配的串的首地址
//如果存在就返回首地址
//否则返回0
//0号元素存放的是字符串的长度(包括0号元素)
int i = pos;
int j = 1;//这个用来指向主串
int next[MAXSIZE];
get_nextval(b, next);//获得next数组值
while (i <= a[0] && j <= b[0]) {
//在两个指针都不越界的情况下
if (j == 0 || b[j] == a[i]) {
//前一个条件代表第一个元素都不匹配,则增加一个元素
//后一个条件代表匹配,则继续下一次的匹配
//当j等于b[0]时实际上匹配已经结束,这里为了加一之后的判定匹配成功
i++;
j++;
printf("%c == %c\n", b[j], a[i]);
}
else {
j = next[j];//回溯
printf("回溯到%d\n", j);
}
}
//跳出循环的时候如果j大于子串长度则表示匹配
if (j > b[0]) {
printf("匹配到的首地址为%d", (i - b[0]));
return i - b[0];
}
else {
printf("没有匹配项");
return ERROR;
}
}
但是,我们的next函数是可以进行改进的。
原理是这样的,假如i3 != j3,j2 == j3,那么,j2 != i3,也就是说此时重复的比较就是多余并且可以去掉的
改进next函数的思路是:在进行next赋值的时候我们要多考虑一步,如果将要赋值的后缀和加一后的前缀相等的话,那么他们的next值实际上应该是一样的,如果不这么做的话,实际上执行KMP算法的时候会先指导到前一个相同的字符,然后再执行它的next数组值,这样做就很多余而且浪费时间。
//改进之后的next函数
void get_nextval(String b[], int *nextval) {
//实际上这个算法就是将原先next数组中的重复的比较部分直接删除
//因为假如i3!=j3,j2!=j3,那么i3!=j2
int i = 1, j = 0;//i为后缀,j为前缀
nextval[1] = 0;
while (i < b[0]) {
//实际上i-1的时候计算的是i+1的next数组值
if (j == 0 || b[i] == b[j]) {
i++;
j++;
if (b[i] != b[j]) {
//如果加一了之后不相等,和之前的一样
nextval[i] = j;
}
else {
//如果相等的话
nextval[i] = nextval[j];//这样就不用重复比较了
}
}
else {
//回溯
j = nextval[j];
}
}
}
下面附上所有的代码
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OK 1
#define ERROR 0
#define MAXSIZE 20
typedef char Elemtype;
typedef char String;
typedef int Status;
//截取字符串//
Status SubString(String a[], int pos,int t,String b[]) {
//对字符串a,取从pos开始且长度为t的子串传给b
//这里的pos是值数组元素下标
int i, j;
if (strlen(a) < t - pos) {
printf("字符串长度过小");
return ERROR;
}
for (i = pos,j =0 ; t > 0; t--,i++,j++) {
b[j] = a[i];
}
b[j] = '\0';//这一步一定要有,没有的话就会变成烫
return OK;
}
//模式匹配的暴力算法//
Status Brute_Force(String a[], String b[], int pos) {
//在a中pos之后寻找b的子串,找到的话返回元素下标,否则返回0
int n, m, i;
String sub[255];
if (pos > 0) {
//我们之后第0号元素都存放字符串长度
n = strlen(a);
m = strlen(b);
i = pos;
while (i <= n - m + 1) {
//n-m+1为i可以寻找的元素下标的最大范围
SubString(a, i, m, sub);
if (strcmp(sub, b) != 0) {
//如果截取的字符串和子串不等的话
i++;
}
else {
//如果找到了的话
printf("匹配的位置开始坐标为%d\n", i);
return i;
}
}
//运行到这里代表没找到
printf("\n不匹配\n");
return ERROR;
}
}
//KMP算法//
//next数组的实现
void get_next(String b[],int *next) {
//next函数0号元素不存放数据
//1号元素存放的是0
//从二号元素开始判定前面字符串最大匹配前后缀的字母个数,并且吧个数加一存放入数组中
//i表示后缀,j表示前缀
//如果T[i]==T[j],i和j就加一,并且next[i]=j,这是因为这里已经判定了最大匹配前后缀
//如果不匹配的话,j就要回溯到当前元素失配时的第一个指导元素
//然后再进行判断
//在整个过程中,前缀是固定的,而后缀是相对的
int i = 1, j = 0;//i是后缀,j是前缀
printf("b[0]=%d\n", b[0]);
next[1] = 0; //这个别忘了
while (i < b[0]) {
//0号元素存放字符串长度,所以总长度要减一
if (j == 0 || b[i] == b[j]) {
//如果这两个值相等的话,代表后一个元素的next值为最大匹配前后缀j加一
i++;
j++;
next[i] = j;
}
else {
//如果不相等或者j没有退回到0(也就是不会i处的next值不等于1)
//回溯
j = next[j];
}
}
}
//改进之后的next函数
void get_nextval(String b[], int *nextval) {
//实际上这个算法就是将原先next数组中的重复的比较部分直接删除
//因为假如i3!=j3,j2!=j3,那么i3!=j2
int i = 1, j = 0;//i为后缀,j为前缀
nextval[1] = 0;
while (i < b[0]) {
//实际上i-1的时候计算的是i+1的next数组值
if (j == 0 || b[i] == b[j]) {
i++;
j++;
if (b[i] != b[j]) {
//如果加一了之后不相等,和之前的一样
nextval[i] = j;
}
else {
//如果相等的话
nextval[i] = nextval[j];//这样就不用重复比较了
}
}
else {
//回溯
j = nextval[j];
}
}
}
//KMP算法主函数
Status Index_KMP(String a[], String b[], int pos) {
//在a中查询pos之后的能与b匹配的串的首地址
//如果存在就返回首地址
//否则返回0
//0号元素存放的是字符串的长度(包括0号元素)
int i = pos;
int j = 1;//这个用来指向主串
int next[MAXSIZE];
get_nextval(b, next);//获得next数组值
while (i <= a[0] && j <= b[0]) {
//在两个指针都不越界的情况下
if (j == 0 || b[j] == a[i]) {
//前一个条件代表第一个元素都不匹配,则增加一个元素
//后一个条件代表匹配,则继续下一次的匹配
//当j等于b[0]时实际上匹配已经结束,这里为了加一之后的判定匹配成功
i++;
j++;
printf("%c == %c\n", b[j], a[i]);
}
else {
j = next[j];//回溯
printf("回溯到%d\n", j);
}
}
//跳出循环的时候如果j大于子串长度则表示匹配
if (j > b[0]) {
printf("匹配到的首地址为%d", (i - b[0]));
return i - b[0];
}
else {
printf("没有匹配项");
return ERROR;
}
}
int main()
{
String a[255], b[255];
strcpy_s(a, "5abcde");
strcpy_s(b, "2cd");
b[0] = 2;
a[0] = 5; //这里必须写成int型才能比较大小,否则比较大是ASCII码
Index_KMP(a, b, 1);
return 0;
}
输出结果: