数据结构—串的模式匹配


数据结构-串(第四章)的整理笔记,若有错误,欢迎指正。

Brute—Force算法

  • Brute—Force(暴力)简称为BF算法,也称简单匹配算法,采用穷举方法。其思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。
  • 时间复杂度:
    最坏情况下要进行(n-m+1)m次比较,时间复杂度为O(m×n)。
    最好情况下的时间复杂度为O(m),即主串的前m个字符正好等于模式串的m个字符。
    平均时间复杂度O(m×n)
  • 其中,n和m分别为主串和模式串的长度

算法实现

int Index(SqString s, SqString t)
{
	int i = 0, j = 0;
	while (i < s.length && j < t.length) //两个串都没有扫描完时循环
	{
		if (s.data[i] == t.data[j]) //当前比较的两个字符相同
		{
			i++; j++; //依次比较后续的两个字符
		}
		else //当前比较的两个字符不相同
		{
			i = i - j + 1; //扫描目标串的i回退,子串从头开始匹配
			j = 0;
		}
	}
	if (j >= t.length) return i - t.length; //j越界,表示t是s的子串
	else return -1; //匹配失败,返回-1
}

分析

  • 运行结果:
    在这里插入图片描述
    请添加图片描述

KMP算法

  • KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。其核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。


求KMP算法中next数组的两种方法

方法一

  • next数组的前两个空位每次都可以无脑写0和1(如果写成-1和0,则数组中的值全部也要减1)
  • 具体求法看下图:
    在这里插入图片描述
    最终求得串"google"的next数值为
j123456
t[j]google
next[j]011121

或者也可以写为

j012345
t[j]google
next[j]-100010

方法二

  • 字符串的前缀、后缀和部分匹配值
  1. 前缀指除最后一个字符以外,字符串的所有头部子串;
  2. 后缀指除第一个字符外,字符串的所有尾部子串;
  3. 部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。
  • 下面以’ ababa为例进行说明:
  1. 'a’的前缀和后缀都为空集,最长相等前后级长度为0。
  2. 'ab’的前缀为{a},后缀为{b},{a}∩{b}= ∅ \emptyset ,最长相等前后缀长度为0。
  3. 'aba’的前缀为{a,ab},后缀为{a,ba},{a,ab}∩{a,ba}={a},最长相等前后缀长度为1。
  4. 'abab"的前缀{a,ab,aba},后缀{b,ab,bab},{a,ab,aba}∩{b,ab,bab}={ab},最长相等前后缀长度为2。
  5. 'ababa’的前缀{a,ab,aba,abab},后缀{a,ba,aba,baba},{a,ab,aba,abab}∩{a,ba,aba,baba}={a,aba},最长相等前后缀长度为3。
  • 故字符串’ ababa’的部分匹配值为00123。
  • 将部分匹配值写成数组形式,就得到了部分匹配值(Partial Match,PM)的表。
编号12345
Sababa
PM00123
  • 下面用PM表来进行字符串匹配:(主串为:ababcababab,子串为ababa)
主串ababcababab
子串ababa
第一趟ababa
  • 第一趟匹配过程:发现a与c不匹配,前面的4个字符’abab’是匹配的,查表可知,最后一个匹配字符’b’对应的部分匹配值为2,因此按照公式:移动位数=已匹配的字符数-对应的部分匹配值,算出子串需要向后移动4-2=2位。
主串ababcababab
子串ababa
第二趟ababa
  • 第二趟匹配过程:发现a与c不匹配,前面2个字符’ab’是匹配的,最后一个匹配字符’b’对应的部分匹配值为0,算出子串需要向后移动2-0=2位。
主串ababcababab
子串ababa
第三趟ababa
  • 第三趟匹配过程:发现a与c不匹配,此时第一个字符就不匹配,子串向后移动1位。
主串ababcababab
子串ababa
第四趟ababa
  • 子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,故KMP算法可以在O(n+m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率。

算法实现

void Get_Next(SqString t, int next[]) //由模式串t求出next数组
{
	int i = 0, j = -1; //i扫描t,j记录t[i]之前与t开头相同的字符个数
	next[0] = -1; //设置next[0]的值
	while (i < t.length - 1) //求t所有位置的next值
	{
		if (j == -1 || t.data[i] == t.data[j]) //k为-1或比较的字符相等时
		{
			i++; j++; //i、j依次移到下一个字符
			next[i] = j; //设置next[i]=j
		}
		else j = next[j]; //j回退
	}
}

int Index(SqString s, SqString t)
{
	int next[MaxSize], i = 0, j = 0;
	Get_Next(t, next);
	while (i < s.length && j < t.length) 
	{
		if (j == -1 ||s.data[i] == t.data[j]) 
		{
			i++; j++;
		}
		else 
			j = next[j];
	}
	if (j >= t.length) return i - t.length; 
	else return -1; 
}

分析

  • 运行结果:
    在这里插入图片描述
    请添加图片描述

改进的KMP算法

  • 从j=1开始,依次判断 t [ j ] t[j] t[j]是否等于 t [ n e x t [ j ] ] t[{next[j]}] t[next[j]]。相等则将 n e x t [ j ] next[j] next[j]修正为 n e x t [ n e x t [ j ] ] next[next[j]] next[next[j]],直至两者不相等为止。
  • j = 1      t [ 1 ] ≠ ( t [ n e x t [ 1 ] ] → t [ 0 ] )      n e x t [ 1 ] = 0 j=1\;\;t[1]≠(t[next[1]]\rightarrow t[0])\;\;next[1]=0 j=1t[1]=(t[next[1]]t[0])next[1]=0
  • j = 2      t [ 2 ] ≠ ( t [ n e x t [ 2 ] ] → t [ 0 ] )      n e x t [ 2 ] = 0 j=2\;\;t[2]≠(t[next[2]]\rightarrow t[0])\;\;next[2]=0 j=2t[2]=(t[next[2]]t[0])next[2]=0
  • j = 3      t [ 3 ] = ( t [ n e x t [ 3 ] ] → t [ 0 ] )      n e x t [ 3 ] = ( n e x t [ n e x t [ 3 ] ] → n e x t [ 0 ] ) = − 1 j=3\;\;t[3]=(t[next[3]]\rightarrow t[0])\;\;next[3]=(next[next[3]]\rightarrow next[0])=-1 j=3t[3]=(t[next[3]]t[0])next[3]=(next[next[3]]next[0])=1
  • j = 4      t [ 4 ] = ( t [ n e x t [ 4 ] ] → t [ 1 ] )      n e x t [ 4 ] = ( n e x t [ n e x t [ 4 ] ] → n e x t [ 0 ] ) = 0 j=4\;\;t[4]=(t[next[4]]\rightarrow t[1])\;\;next[4]=(next[next[4]]\rightarrow next[0])=0 j=4t[4]=(t[next[4]]t[1])next[4]=(next[next[4]]next[0])=0
  • j = 5      t [ 5 ] ≠ ( t [ n e x t [ 5 ] ] → t [ 2 ] )      n e x t [ 5 ] = 2 j=5\;\;t[5]≠(t[next[5]]\rightarrow t[2])\;\;next[5]=2 j=5t[5]=(t[next[5]]t[2])next[5]=2
  • j = 6      t [ 6 ] ≠ ( t [ n e x t [ 6 ] ] → t [ 1 ] )      n e x t [ 6 ] = 1 j=6\;\;t[6]≠(t[next[6]]\rightarrow t[1])\;\;next[6]=1 j=6t[6]=(t[next[6]]t[1])next[6]=1

对于每一个串,next数组的next[0]=-1,next[1]=0;nextval数组的nextval[0]=-1,nextval[1]=0,这些值都是固定的!

j0123456
t[j]abcabaa
next[j]-1000121
nextval[j]-100-1021

算法实现

void Get_Nextval(SqString t, int nextval[])
{
	int i = 0, j = -1;
	nextval[0] = -1;
	while (i < t.length)
	{
		if (j == -1 || t.data[i] == t.data[j])
		{
			i++; j++;
			if (t.data[i] != t.data[j])
				nextval[i] = j;
			else nextval[i] = nextval[j];
		}
		else j = nextval[j];
	}
}

int Index(SqString s, SqString t)
{
	int nextval[MaxSize], i = 0, j = 0;
	Get_Nextval(t, nextval);
	while (i < s.length && j < t.length) 
	{
		if (j == -1 ||s.data[i] == t.data[j]) 
		{
			i++; j++;
		}
		else 
			j = nextval[j];
	}
	if (j >= t.length) return i - t.length; 
	else return -1; 
}

Brute—Force算法和KMP算法的比较

  1. Brute—Force算法时间复杂度:O(m×n);KMP算法时间复杂度:O(m+n)。
  2. Brute—Force算法简单且易于理解,但效率不高,主要原因是主串指针在某些情况下需回溯;而KMP算法在此基础之上有较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
  3. !注意:KMP算法的平均时间复杂度虽然优于BF算法,但并不等于说任何情况下KMP算法都优于BF算法。当模式串的next数组中next[0]=-1,而其他元素值均为0时,KMP算法退化为BF算法。

完整代码

#include<stdio.h>
#define MaxSize 50
typedef char ElemType;
typedef struct
{
	ElemType data[MaxSize];
	int length;
}SqString;

void StrAssign(SqString& s, char cstr[]) //s为引用型参数
{
	int i;
	for (i = 0; cstr[i] != '\0'; i++)
		s.data[i] = cstr[i];
	s.length = i; //设置串s的长度
}

void PrintString(SqString s)
{
	for (int i = 0; i < s.length; i++)
		printf("%c", s.data[i]);
}

int Index(SqString s, SqString t) //简单匹配算法
{
	int i = 0, j = 0;
	while (i < s.length && j < t.length) //两个串都没有扫描完时循环
	{
		if (s.data[i] == t.data[j]) //当前比较的两个字符相同
		{
			i++; j++; //依次比较后续的两个字符
		}
		else //当前比较的两个字符不相同
		{
			i = i - j + 1; //扫描目标串的i回退,子串从头开始匹配
			j = 0;
		}
	}
	if (j >= t.length) return i - t.length; //j越界,表示t是s的子串
	else return -1; //匹配失败,返回-1
}

void Get_Next(SqString t, int next[]) //由模式串t求出next数组
{
	int i = 0, j = -1; //i扫描t,j记录t[i]之前与t开头相同的字符个数
	next[0] = -1; //设置next[0]的值
	while (i < t.length - 1) //求t所有位置的next值
	{
		if (j == -1 || t.data[i] == t.data[j]) //k为-1或比较的字符相等时
		{
			i++; j++; //i、j依次移到下一个字符
			next[i] = j; //设置next[i]=j
		}
		else j = next[j]; //j回退
	}
}

int KMPIndex(SqString s, SqString t) //KMP算法
{
	int next[MaxSize], i = 0, j = 0;
	Get_Next(t, next);
	while (i < s.length && j < t.length)
	{
		if (j == -1 || s.data[i] == t.data[j])
		{
			i++; j++;
		}
		else
			j = next[j];
	}
	if (j >= t.length) return i - t.length;
	else return -1;
}

void Get_Nextval(SqString t, int nextval[]) //由模式串t求出nextval的值
{
	int i = 0, j = -1;
	nextval[0] = -1;
	while (i < t.length)
	{
		if (j == -1 || t.data[i] == t.data[j])
		{
			i++; j++;
			if (t.data[i] != t.data[j])
				nextval[i] = j;
			else nextval[i] = nextval[j];
		}
		else j = nextval[j];
	}
}

int KMPIndex1(SqString s, SqString t) //改进的KMP算法
{
	int nextval[MaxSize], i = 0, j = 0;
	Get_Nextval(t, nextval);
	while (i < s.length && j < t.length)
	{
		if (j == -1 || s.data[i] == t.data[j])
		{
			i++; j++;
		}
		else
			j = nextval[j];
	}
	if (j >= t.length) return i - t.length;
	else return -1;
}

int main()
{
	SqString s, t;
	int j;
	int next[MaxSize], nextval[MaxSize];
	ElemType cstr1[] = "abcabcdabcdeabcdefabcdefg";
	StrAssign(s, cstr1);
	printf("s:");
	PrintString(s);
	ElemType cstr2[] = "abcdeabcdefab";
	StrAssign(t, cstr2);
	printf("\nt:");
	PrintString(t);
	printf("\n简单匹配算法:\n");
	printf("  t在s中的位置=%d\n", Index(s, t));
	Get_Next(t, next); //由模式串t求出next的值
	Get_Nextval(t, nextval); //由模式串t求出nextval的值
	printf("   j    ");
	for (j = 0; j < t.length; j++) printf("%4d", j);
	printf("\n");
	printf(" t[j]   ");
	for (j = 0; j < t.length; j++) printf("%4c", t.data[j]);
	printf("\n");
	printf(" next   ");
	for (j = 0; j < t.length; j++) printf("%4d", next[j]);
	printf("\n");
	printf("nextval ");
	for (j = 0; j < t.length; j++) printf("%4d", nextval[j]);
	printf("\n");
	printf("KMP算法:\n");
	printf("  t在s中的位置=%d\n", KMPIndex(s, t));
	printf("改进的KMP算法:\n");
	printf("  t在s中的位置=%d\n", KMPIndex1(s, t));
	return 0;
}

运行结果

在这里插入图片描述

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值