【数据结构】串的模式匹配(KMP算法)

什么是串

线性存储的一组数据
特殊操作集:

  • 求串的长度
  • 比较两串是否相等
  • 两串相接
  • 求子串
  • 插入子串
  • 匹配子串
  • 删除子串

匹配子串
目标:
给定一段文本string:s0s1……sn-1
给定一个模式:pattern = p0p1 …… pm-1
求 pattern 在 string 中出现的位置
这里的pattern可能会很长,所以直接简单粗暴的那种匹配方式在这里不是特别合适
因此可以写一个函数接口:
Position PatternMatch(char *string, char *pattern)

简单实现
方法1:C的库函数strstr
char *strstr(char *string, char *pattern)

char*表示返回的是返回pattern的p0在string中出现的位置的字符串,如果没有找到返回NULL

#include <stdio.h>
#include <string.h>
typedef char typedef char* Position;
int main()
{ 
	char string[] = "This is a simple example.";
	char pattern[] = "simple";
	Position p = strstr(string, pattern);
	printf("%s\n", p);
	return 0;
}

返回的p输出后是“simple example”

strstr内部的比较逻辑:
在这里插入图片描述
p0和s0比较√
p1和s1比较√
p2和s2比较×
pattern(相当于)向右移动一位
p0和s1比较√
……
时间复杂度为O(MN)(str.len和pat.len)


KMP算法

这个算法将原本的O(MN)简化到T=O(M+N)
模拟实现:
在这里插入图片描述
一个一个比对过去后发现前面五个字符“abcab”是完全匹配的,到了第六个字符就不相等了,如果按照常规的做法那应该是pattern往前挪动一位再重新开始比较。

KMP算法中发现:pattern已经匹配的字串中,它首部的一部分小字串,和尾部的一部分小字串它们是一模一样的,能匹配上的,并且这个字串是已经能配上的最长的子串了
在这里插入图片描述
这告诉了我们如果匹配不上后,把pattern往前挪动一位是没有意义的,不可能匹配上的。因此KMP算法是将pattern移动到尾部匹配到的子串的位置上,这样肯定是能匹配上的。这样进行比较也就不需要从头开始比较了。
在这里插入图片描述
比较的位置就是从string的x处继续开始,也就是说string的指针是不会退回去的,而是一直往前走,因此对于string最多只会遍历N次(string的length)。

因此在进行字符串的匹配之前,先对pattern进行一次分析,有哪些位置是有重复出现的,这样方便下次直接跳过去
在这里插入图片描述
这个是pattern数组,i是pattern的数组下标,j是match的数组下标,i=j是无意义的。
下面开始对match进行解析:
感谢KMP算法bySYCstudio的详解
在这里插入图片描述
第0轮,这段不需要在循环中,因为一开始(就一个字符)肯定不会有匹配的前后缀的,直接令match[0] = -1

第一轮,i=1,j=match[i]=-1. pattern[i] = b,因为match[i-1=0] = -1,此时这个b不能接在a的后面(毕竟前面没有相同前缀后缀),再判断pattern[j+1]!=pattern[b],表示就算是最开头也没有符合的,因此match[i] = -1
在这里插入图片描述
第二轮,i=2,j=match[i]=-1. pattern[i] =c,因为match[i-1=1] = -1,此时这个c不能接在b的后面(毕竟前面没有相同前缀后缀),再判断pattern[j+1]!=pattern[b],表示就算是最开头也没有符合的,因此match[i] = -1
在这里插入图片描述
第三轮,i=3,j=match[i]=-1. pattern[i] = a,因为match[i-1=2] = -1,此时这个a不能接在c的后面(毕竟前面没有相同前缀后缀),再判断pattern[j+1]和pattern[i],都为a相等,所以match[i] = j+1=0,这个0代表了前面0~i字符的最长相同前缀后缀的前缀结束处为pattern[0],长度为0+1=1
在这里插入图片描述

第四轮,i=4,j=match[i-1]=0. pattern[i] = b,因为match[i-1] = 0,表示这个b可能可以接在a的后面,因为这个a已经找到了自己的前缀,再判断pattern[j+1=1]和pattern[i],两者相同,那么match[i] = j+1=1,这个1代表了前面0~i字符的最长相同前缀后缀的前缀结束处为pattern[1],长度为2
在这里插入图片描述
第五轮,i=5,j=match[i-1]=1. pattern[i]=c,因为match[i-1]=1,表示这个c可能可以接在b的后面,因此判断pattern[j+1=2]和pattern[i],都为c且相等,因此match[i] = j+1=2.
在这里插入图片描述
第六轮,i=6,j=match[i-1]=2. pattern[i]=a,因为match[i-1]=2,表示这个a可能可以接在c的后面,判断pattern[j+1=3]和pattern[i],发现相等,因此match[i] = j+1=3
在这里插入图片描述
第七轮,i=7,j=match[i-1]=3. pattern[i]=c,因为match[i-1]=3,表示可能接在后面,去判断pattern[j+1=4]=b和pattern[i]=c,发现并不相等,表示就算是最开头也没有符合的,因此match[i] = -1
在这里插入图片描述
第八轮,i=8,j=match[i-1]=-1,因为j=-1所以不考虑前面是否能匹配了,直接去看pattern[j+1]和pattern[i],相等,那么match[i] = j + 1 = 0
第九轮可以自己口述出来了吧~
在这里插入图片描述
代码实现:


	int len = strlen(pattern);
	for(i=1;i<len;i++){
		j = match[i-1];
		while((pattern[j+1]!=pattern[i])&&j>=0)
			j = match[j]; 
		if(pattern[j+1]==pattern[i])//相同,可接上的前缀后缀更长 
			match[i] = j+1;
		else
			match[i] = -1;
	}
} 

上面是最难的内容了,接下来就是KMP算法本体的实现
在这里插入图片描述
我们的目的是pattern到一次匹配失败后,跳到有重复的地方,那么对于pattern的指示数j,跳到pattern[match[j-1]+1]就可以继续进行了,这一步指的就是令j = match[j-1]+1,如果此时match[j-1]==-1,那么就将j=0,跳到pattern[0]

#include<bits/stdc++.h>
using namespace std;
typedef int Position;
void BuildMatch(char *pattern,int *match); 
Position KMP(char *A,char *B);
void BuildMatch(char *pattern,int *match){
	match[0] = -1;//match[0]肯定是-1 
	int i,j;
	int len = strlen(pattern);
	for(i=1;i<len;i++){
		j = match[i-1];
		while((pattern[j+1]!=pattern[i])&&j>=0)//abcabdabcabc,当遇到最后一个c的时候,c应该是为2,而不是-1,需要递归地返回它所在的地方 
			j = match[j]; 
		if(pattern[j+1]==pattern[i])//相同,可接上的前缀后缀更长 
			match[i] = j+1;
		else
			match[i] = -1;
	}
} 
Position KMP(char *str,char *pat){
	int n = strlen(str);
	int m = strlen(pat);
	if(n<m)	return -1; 
	int i=0,j=0;
	int *match = new int[sizeof(pat)];
	BuildMatch(pat,match);
	while(i<n&&j<m){
		if(str[i]==pat[j]){
			i++;j++;
		}else if(j>0){
			j = match[j-1] + 1;//如果不等并且j这时候指向pat内,那么j就要回到其对应的相同前缀后缀处 
		}else{//str!=pat && j<0-->j==0 相当于pat往右移,也就是i指向了str的下一位 
			i++;
		} 
	}
	return (j==m)?(i-m):-1;
}
int main(){
	char* ch={"abaabaabbabaaabaabbabaab"};
	char* pa={"abaabbabaab"}; 
	cout<<KMP(ch,pa);
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值