首页 > 编程语言 >【数据结构】串的表示与模式匹配算法

【数据结构】串的表示与模式匹配算法

时间:2024-02-17 17:44:25浏览次数:30  
标签:子串 字符 主串 号位 length next 算法 数据结构 模式匹配

串是内容受限的线性表(栈和队列是操作受限的线性表)

串(string)是零个或多个任意字符组成的有限序列

S:串名

a1a2a3 ... an:串值

n:串长

当 n = 0 时,表示空串,空串用 \(\phi\) 表示

子串:一个串中任意个连续字符组成的子序列(含空串)

例如“abc”的子串有

“”、“a”、“b”、"c"、"ab"、"bc"、"ac"、"abc"

真子串是指不包含自身的所有子串

主串:包含子串的串

字符位置:字符在序列中的序号(从1开始)

子串位置:子串第一个字符在主串中的位置

空格串:由一个或多个空格组成的串(与空串不同)

例:字符串a、b、c、d

a = "BEI",长度为3

b = "JING",长度为4

c = "BEIJING",长度为7

d = "BEI JING",长度为8

c 的子串为 a、b

d 的子串为 a、b

a 在 c 中的位置是1,a 在 d 中的位置是1

b 在 c 中的位置是4,b 在 d 中的位置是5

串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才相等

表示

顺序存储结构

#define MAXLEN 255
typedef struct String {
	char data[MAXLEN + 1];
	int length;
}String;

不使用数组中的0号下标以方便操作

length:串的当前长度

链式存储结构

#define CHUNKSIZE 80
typedef struct Chunk {
	char data[CHUNKSIZE];
	struct Chunk* next;
}Chunk;

typedef struct ChunkLinkString {
	Chunk* head;
	Chunk* tail;
	int curlen;
}String;

head:串的头指针

tail:串的尾指针

curlen:串的当前长度

模式匹配

确定某子串(模式串)在主串中第一次出现的位置

两种模式匹配算法

  • BF算法(Brute Force)
  • KMP算法(由D.E.Knuth、J.H.Morris、V.R.Pratt共同提出)

BF算法

从主串的第一个字符开始,依次和子串各个字符逐个比较,如果比较过程中出现不相等的字符,则从主串的第二个字符开始重新比较,以此类推,如果某次比较完成后主串和子串的每个字符全部相等,则返回本次比较时开始的位置,即子串在主串中的位置

假设主串长度 A,子串长度 B,则每次比较的起始位置的范围为1到 A-B+1,每次比较的次数的范围为 1 到 B

参考实现方式1

S 和 T 都为非空串

int BF_1(String* S, String* T)
{
	int i = 1;
	int j = 1;

	int S_length = S->length;
	int T_length = T->length;

	int position = 1;
	int flag = 0;

	while (position <= S_length - T_length + 1)
	{
		flag = 0;
		for (j = 1; j <= T_length; j++)
		{
			if (S->data[i] != T->data[j])
			{
				flag = 1;
				position++;
				i = position;
				break;
			}
			i++;
		}
		if (flag == 0)
		{
			return position;
		}
	}
	return 0;
}

i:表示每次比较时,字符在主串中的位置

j:表示每次比较时,字符在子串中的位置

position:记录主串中每次比较的起始位置

flag:用来判断每次比较是否有不相等的情况,如果有,flag为1,匹配失败,如果没有,flag 为0,匹配成功

			if (S->data[i] != T->data[j])
			{
				flag = 1;
				position++;
				i = position;
				break;
			}

如果字符不相等,则要更新 position 的值,让 position 值加1

参考实现方式2

返回0表示匹配失败

int BF_2(String* S, String* T)
{
	int i = 1;
	int j = 1;
	while (i <= S->length && j <= T->length)
	{
		if (S->data[i] == T->data[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 2;
			j = 1;
		}
	}
	if (j >= T->length)
	{
		return i - T->length;
	}
	else
	{
		return 0;
	}
}

i 是比较时字符在主串中的位置

j 是比较时字符在子串中的位置

		if (S->data[i] == T->data[j])
		{
			i++;
			j++;
		}
		else
		{
			i = i - j + 2;
			j = 1;
		}

不断比较,如果相等,则继续比较,如果不相等,则 i 的值更新为下一次比较时的起始位置

例如:

当 i == 6,j == 4时,发现不匹配,则 i 的值需要更新变为4,即 i-j+2,j 的值更新变为1

当每个字符都匹配时,此时 j 的值为子串长度+1

	if (j >= T->length)
	{
		return i - T->length;
	}
	else
	{
		return 0;
	}

这里写成 if (j == T->length + 1) 也可以

时间复杂度

设主串长度为 n,子串长度为 m,则最坏情况是

主串前面的 n - m 个位置都一直匹配到子串的最后一位,最后 m 位也各比较了1次

于是比较的总次数为:

\[(n-m)*m+m=(n-m+1)*m \]

若 m << n

则算法的时间复杂度为\(O(n*m)\)

KMP算法

前言

当发现某个字符不匹配时,按照BF算法,需要从头开始重新比较

例如:

指向主串中待比较的字符的箭头称为主串指针

指向子串中待比较的字符的箭头称为子串指针

此时,由于字符不匹配,主串指针需要回退,指向主串中的5号位置,子串指针需要指向子串中的1号位置,继续逐个进行比较

然而,注意到当字符匹配失败时,有一个信息是BF算法中没有用到,那就是匹配失败前的每个字符是相等的

KMP算法就利用了这个信息,使得主串指针可以不断向前而不回退,算法的时间复杂度降为\(O(n+m)\)

思路

概念介绍

串的前后缀:假设串 S = AB,则 A 为串 S 的前缀,B 为串 S 的后缀,其中 A 和 B 都不能为空串

例如:串 S = "code",则

串 S 的前缀为:"c"、"co"、"cod"

串 S 的后缀为:"o"、"od"、"ode"

过程模拟

KMP算法的核心就在于找出子串的最大公共前后缀,其具体的实现过程为:

当某个字符匹配失败时,在子串中,寻找该字符前所构成的串中的最大公共前后缀,然后将子串指针指向最大前缀的下一位置,主串指针保持不变,继续进行比较

例如:

此时主串中8号位置的字符与子串中5号位置的字符匹配失败,观察到子串中5号位置前的最大公共前后缀为"ab"(最大公共前后缀显然不包括自身,即本例中的"abab"),于是子串指针指向最大前缀的下一位置,即子串中的3号位置,主串指针不变,如图所示

证明正确性

为什么模式串只需要移动到最大前缀的下一位置然后继续匹配就可以呢?在此前的位置进行匹配就一定不会成功吗?

假设有如下主串和模式串

主串中'c'和模式串中'z'匹配失败,模式串中'z'前的最大公共前后缀为"ab",此时下一步的正确做法如下

如果在前面的位置进行匹配,如:

那么此时只有当主串中的'y'等于子串中的'x'时,匹配可以继续

然而,若'y' == 'x'

则模式串中的'z'前的最大公共前后缀为”abxab“

和原先条件相矛盾

个人非常粗糙的浅显的解释

next数组

为了求出模式串的最大公共前后缀,需要用一个数组来记录这些信息,这就是 next 数组,next 数组中各个数字的含义是:当该字符匹配失败时,子串指针需要跳到子串中的第 x 个位置重新进行匹配,其中 x 为 next 数组中的值,x 的大小是子串指针前所有字符构成的子串的最大公共前后缀的长度加1

例如:

假设在子串的第4个位置,即字符 b 时匹配失败,此时子串指针前的子串为"aba",其最大公共前后缀的长度为1,于是子串指针需要跳到子串中的第2个位置进行重新匹配

假设在子串的第5个位置,即字符 c 时匹配失败,此时子串指针前的子串为"abab",其最大公共前后缀的长度为2,于是子串指针需要跳到子串中的第3个位置进行重新匹配

这里的数组是省略了0号位的空间的

求解next

假设有模式串"ababac",则其 next 数组如下

当 j == 1 时,规定 next[j] = 0,在之前举的例子中,其值为1,从写代码的角度来看,规定为0会更方便操作

注意到:

  1. next[j] 的值每次最多加1
  2. 模式串中最后一位字符不产生任何影响,无论它是什么,结果都一样

在求解 next 数组的过程中,在已经知道之前的最大公共前后缀是什么之后,只要比较下一字符是否相等即可

例如:

假设 j == 3,3号位前的子串为"ab",比较1号位和2号位的字符,不相等,无最大公共前后缀,next[3] = 1

j == 4 时,4号位前的子串为"aba",比较1号位和3号位的字符,相等,最大公共前后缀长度为1,next[4] = 2

j == 5 时,5号位前的子串为"abab",在 j == 4时,我们已经知道1号位等于3号位了,此时只需要比较2号位和4号位即可,发现相等,于是最大公共前后缀长度为2,next[5] = 3

j == 6 时,6号位前的子串为"ababa",同理,此时比较3号位和5号位的字符,相等,于是 next[6] = 4

如果比较时字符不相等,该如何处理?

例如:

j == 7 时,7号位前的子串为"ababac",比较4号位和6号位的字符,发现不相等,此时 next[7] 的值该是多少呢?

在处理这样的情况时,仍然需要用到最大公共前后缀这一特点

假设现在有一模式串的长度为17,已知 next[16] == 8,即1到7号所构成的子串和9到15号构成子串相等,此时需要比较8号位和16号位的字符,假设其不相等,则比较 next[8] 号位置的字符和16号位的字符

假设 next[8] == 3,说明1到2号所构成的子串和6到7号构成子串相等,又由于1到7号所构成的子串和9到15号构成子串相等,于是推出1到2号所构成的子串和14到15号构成的子串相等,如图所示

红框和蓝框代表相等的串

如若还不等,则比较 next[3] 号位置的字符和16号位的字符,原理同上

next实现
void get_next(String* T, int next[])
{
	int i = 1;
	next[1] = 0;
	int j = 0;
	while (i < T->length)
	{
		if (j == 0 || T->data[i] == T->data[j])
		{
			i++;
			j++;
			next[i] = j;
		}
		else
		{
			j = next[j];
		}
	}
}

i:一个不断向前的变量,用于指向最大公共后缀的下一字符

j:一个会回退的变量,用于指向最大公共前缀的下一字符

其中

while (i < T->length)

小于号而不是小于等于,因为模式串中最后一个字符对 next 数组的值没有影响

if (j == 0 || T->data[i] == T->data[j])

j == 0 是和 next[1] = 0 相配合的,个人认为此处代码极为精妙

KMP实现
int KMP(String* S, String* T, int next[])
{
	int i = 1;
	int j = 1;
	while (i <= S->length && j <= T->length)
	{
		if (j == 0 || S->data[i] == T->data[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	if (j == T->length + 1)
	{
		return i - T->length;
	}
	else
	{
		return 0;
	}
}

i:主串指针

j:子串指针

注意,在定义串时,是舍弃了0号位空间的

所以有

while (i <= S->length && j <= T->length)

其中是小于等于而不是小于

因为用结构体表示串时,是舍弃了数组中0号位的空间的

标签:子串,字符,主串,号位,length,next,算法,数据结构,模式匹配
From: https://www.cnblogs.com/changbaiqiusha/p/18017908

相关文章

  • 对最大公约数求法和扩展欧几里得算法的简要概述
    目录1.最大公约数(gcd)1.1更相减损术时间复杂度分析1.2辗转相除法(欧几里得算法)时间复杂度分析2.最小公倍数(lcm)3.裴蜀定理(贝祖定理)3.1扩展欧几里得算法(exgcd)1.最大公约数(gcd)数论中,通常用\(d\|\a\)表示\(d\)能整除\(a\),即\(......
  • 代码随想录算法训练营第十七天 | 110.平衡二叉树 (优先掌握递归)| 404.左叶子之和 (优先
    257.二叉树的所有路径 已解答简单 相关标签相关企业 给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。示例1:输入:root=[1,2,3,null,5]输出:["1->2->5","1->3"]示例2:输入:ro......
  • 代码随想录算法训练营第十六天 | 104.二叉树的最大深度| 559.n叉树的最大深度|222.完
    222.完全二叉树的节点个数 已解答简单 相关标签相关企业 给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中......
  • 代码随想录算法训练营第十五天 | 层次遍历 | 101. 对称二叉树 | 226.翻转二叉树
    226.翻转二叉树 已解答简单 相关标签相关企业 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。 示例1:输入:root=[4,2,7,1,3,6,9]输出:[4,7,2,9,6,3,1]示例2:输入:root=[2,1,3]输出:[2,3,1]示例3:输入:root=[]输出:[......
  • CRC算法原理和代码实现
    前言​ 由于现在的工作涉及到协议的对接,而协议使用CRC进行校验。并且在MATLAB传C的过程中有可能需要使用到CRC来校验数据。所以在这篇文章中对CRC校验的有关知识进行梳理,也是自己对这方面知识点的梳理和总结吧。什么是CRC校验​ CRC(CyclicRedundancyChecksum循环冗余校验)校验......
  • 关于一种维护凸包的根号数据结构
    本文介绍了笔者由一道题的根号做法受到启发,独自摸索出来的一个数据结构。由于笔者能力和精力有限,无法查找已有的资料,如果有哪位巨已经提出来了记得踩我一脚。介绍这个数据结构维护凸包,支持以下操作:\(O(\sqrt{n})\)在线加入/删除任意一条线段\(O(\sqrt{n}\log\sqrt{n})\)在......
  • 算法竞赛经典入门(第2版)习题1
    目前在准备一个竞赛,头绪并不是很清楚,根据知乎的推荐入了一本书《算法竞赛入门经典》(第2版)...下面是写的例题和习题答案也算是简单记录一下学习过程吧。//三位数反转#include<stdio.h>intmain(){intn;scanf("%d",&n);printf("%d%d%d\n",n%10,n/10%10,n/100)......
  • 算法第一课:复杂度引入
    算法复杂度算法复杂度分两种,时间复杂度和空间复杂度。分别代表了算法的用时,以及算法所占用的内存空间。复杂度越小,运行效率越高。复杂度表示法一般用大写字母\(O\)表示,称为大\(O\)表示法。比如\(O(n)\),\(O(n^2)\)等。这里的\(n\)代表了算法的输入规模,比如数组的长度,链......
  • 读十堂极简人工智能课笔记03_遗传算法与进化
    1. 寻找正确答案1.1. 卡尔·西姆斯1.1.1. 计算机图形艺术家和研究者1.1.2. 演示过数字进化之创造性和新颖性的先驱1.1.3. 1994年1.1.3.1. 创造一批能游泳、走路、跳跃,甚至互相竞争的虚拟动物震惊了整个科学界1.1.3.2. 它们的人工大脑却是个极其复杂的网络,信息经由传......
  • 基于双树复小波变换和稀疏表示的多光谱和彩色图像融合算法matlab仿真
    1.算法运行效果图预览  2.算法运行软件版本MATLAB2022a 3.算法理论概述     基于双树复小波变换(Dual-TreeComplexWaveletTransform,DT-CWT)和稀疏表示的多光谱和彩色图像融合算法是一种先进的图像融合技术,旨在将多光谱图像(MultispectralImages,MSI)和彩......