字符串模式匹配的KMP算法

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个元素不用管,后面的元素就是前面的字符串最大匹配的前后缀数量加一。

扫描二维码关注公众号,回复: 2435506 查看本文章

比如第二个字符和第一个字符不一样,那么第二个字符前面的字符串数量就是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;
}

输出结果:


猜你喜欢

转载自blog.csdn.net/haohulala/article/details/80118239