数据结构-串

 王道章节内容

 知识框架

考纲内容

字符串模式匹配


串的定义和实现

定义

抽象数据类型描述

串的逻辑结构与线性表类似,不同之处在于串针对的是字符集

顺序存储结构

定义

串的顺序存储结构

  • 用一组地址连续的存储单元来存储串中的字符序列;
  • 按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区;
  • 一般用定长数组来定义。

定长数组的基本特性:

  • 固定大小:数组的大小(即它可以容纳的元素数量)是在创建时定义的,并且在程序运行期间保持不变。
  • 连续存储:数组中的元素在内存中是连续存储的,这使得随机访问非常快。
  • 索引访问:可以通过索引来直接访问数组中的任何元素。索引通常从0开始。
  • 类型化:数组中的所有元素都具有相同的类型。 

 定长顺序存储表示

堆分配存储表示

链式存储结构

块链存储结构

串的模式匹配

定义

串的模式匹配

子串的定位操作,例如一个英文单词在一篇英文文章的定位。

朴素的模式匹配算法

算法思路

  • 简单说,就是对主串 S 的每一个字符作为子串 T 开头,与要匹配的字符串进行匹配;
  • 对主串 S 做大循环,每个字符开头做子串 T 的长度的小循环,指导匹配成功或者全部遍历完成。

/* 朴素的模式匹配法 */
int Index(String S, String T, int pos) 
{
	int i = pos;	/* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
	int j = 1;				/* j用于子串T中当前位置下标值 */
	while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
	{
		if (S[i] == T[j]) 	/* 两字母相等则继续 */
      	{
			++i;
         	++j; 
      	} 
      	else 				/* 指针后退重新开始匹配 */
      	{  
         	i = i-j+2;		/* i退回到上次匹配首位的下一位 */
         	j = 1; 			/* j退回到子串T的首位 */
      	}      
	}
	if (j > T[0]) 
		return i-T[0];
	else 
		return 0;
}

代码解释

i = i - j + 2;

 缺陷

也有教材说为O((n - m + 1)* n),显然哈哈。

KMP 模式匹配算法

KMP 模式匹配算法,即克努特-莫里斯-普拉特算法。

引入

从上面的朴素的模式匹配中,我们知道,有些过程是不必要的,这取决于前面的元素后边是否有所重合,例如子串 abcde,长度为5,我们可以注意到开头的 a 与之后的都不重合,而假设前3位匹配上了,那么从第2和第3位开始的匹配操作都是不必要的了,直接从第四位开始匹配即可;

又比如,abcefabde,可以看出,前缀 ab (1,2)与后缀 ab(7,8) 有所重合,假设前8位匹配上了,那么同样第7位之前的匹配操作都是不必要的了,直接从第7位开始即可;

这里需要注意,我举的例子都是“匹配数(即最大 i 值)>=相同后缀首字符位置数”的,如果是 < 的情况,那自然要回溯到 i - j + 2 位置开始;

如果利用这一特性去减少主串 i 的回溯,即不让 i 变小,那么我们只要考虑子串 j 的变化即可,即 j下一次从哪里开始,又因为后缀重合部分是已经确定的、毋庸置疑的,所以 j 的变化显然就与“当前字符之前的串的前后缀之间的相似度”有关;

怎样更好地理解以上这些呢,我们可以把 “主串”类比为一个“漆黑的电影院里的一排座位”,“子串”类比为“一摞子顺序固定的票”,手里的票的数目是已知的,票号和电影院里的座位号都是未知的。每一次匹配都是一次“开灯”,我们只能通过“开灯”得到的信息进行进一步的操作。

电影院座位与票的对应:

  • 每个座位代表主串中的一个字符位置。
  • 每张票代表子串中的一个字符。

开灯:

  • 开灯意味着我们正在比较主串中的一个字符与子串中的一个字符。
  • 当我们开灯时,我们能看到一个座位与一张票之间的对应关系。
  • 如果座位号与票号匹配,那么我们向前移动到下一个座位和下一张票。
  • 如果不匹配,我们需要考虑如何重新开始匹配。

优化匹配过程:

  • 当我们遇到不匹配的情况时,我们需要决定如何继续匹配。
  • 如果子串中有前后缀重合的部分,那么我们可以跳过一些不必要的座位,直接从一个合理的座位开始比较。
  • 例如,如果我们已经比较了子串的一部分,而这一部分与子串的末尾有重合,那么我们可以直接跳到重合部分的末尾开始比较。

推导

《大话》和王道针对 j 的变化(即相似度)分别给出了两种表示方法,这两种方法是殊途同归的。

王道表示:

到这里实际上与《大话》一样了哈哈哈哈哈。

《大话》表示:

这里看的时候可以用“左闭右开”区间来理解,如 i:[1,i)

根据经验可知,最开始 k = 0;接着第二个若没有相等,则 k = 1;接着还没有则回溯;如果前后缀一个字符相等,k = 2;如果前后缀 n 个字符相等,则 k = n + 1 。

综上,next [ j ] 的意思是:当子串的第 j 个字符与主串发生失配时,跳到子串的 next [ j ] 位置进行比较。

代码实现

王道与《大话》实现基本类似,故不特别标明。

求要匹配的子串的 next 数组:

/* 通过计算返回子串T的next数组。 */
void get_next(String T, int *next) 
{
	int i,k;
  	i=1;
  	k=0;
  	next[1]=0;
  	while (i<T[0])  /* 此处T[0]表示串T的长度 */
 	{
    	if(k==0 || T[i]== T[k])   /*这里指相似
		{
      		++i;  
			++k;  
			next[i] = k; /*
    	} 
		else 
			k= next[k];	/* 若字符不相同,则k值回溯上一个 */
  	}
}

这里的代码可能不好理解,特别是针对 k 和 i,这里我们作出以下解释:

变量说明:

  • i: 当前正在处理的模式串 T 中的位置索引。
  • k: 已经匹配的字符个数,或者说当前正在尝试匹配的模式串 T 中的位置索引。

i 和 k 的作用:

  • i: 在每次循环中,i 指向模式串 T 中当前要比较的字符。初始时,i 为 1,指向模式串的第一个字符。
  • kk 指向模式串 T 中正在尝试匹配的前缀的最后一个字符。初始时,k 为 0,表示还没有匹配任何符。

k 值在循环中是动态的,有时作为值直接存储为next,有时作为中间状态值0进行下一步的迭代。(所以k值在循环中并不是处处符合数学式,但最后展现是符合预期的)

为什么呢?

因为 next[ j ] 和 k 数量意义上不是一直互通的,一个是成熟的,一个是初始的。

让我们用字符串 "abcac" 作为例子来具体解释 ik 的变化: 

匹配查找:

/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
/*  T非空,1≤pos≤StrLength(S)。 */
int Index_KMP(String S, String T, int pos) 
{
	int i = pos;		/* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
	int j = 1;			/* j用于子串T中当前位置下标值 */
	int next[255];		/* 定义一next数组 */
	get_next(T, next);	/* 对串T作分析,得到next数组 */
	while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
	{
		if (j==0 || S[i] == T[j]) 	/* 两字母相等则继续,与朴素算法增加了j=0判断 */
      	{
         	++i;
         	++j; 
      	} 
      	else 			/* 指针后退重新开始匹配 */
      	 	j = next[j];/* j退回合适的位置,i值不变 */
	}
	if (j > T[0]) 
		return i-T[0];
	else 
		return 0;
}

对比朴素的模式匹配算法,去除了 i 的回溯,多了以下部分:

	int next[255];		/* 定义一next数组 */
	get_next(T, next);	/* 对串T作分析,得到next数组 */


      	else 			/* 指针后退重新开始匹配 */
      	 	j = next[j];/* j退回合适的位置,i值不变 */

改进

/* 求模式串T的next函数修正值并存入数组nextval */
void get_nextval(String T, int *nextval) 
{
  	int i,k;
  	i=1;
  	k=0;
  	nextval[1]=0;
  	while (i<T[0])  /* 此处T[0]表示串T的长度 */
 	{
    	if(k==0 || T[i]== T[k]) 	/* T[i]表示后缀的单个字符,T[k]表示前缀的单个字符 */
		{
      		++i;  
			++k;  
			if (T[i]!=T[k])      /* 若当前字符与前缀字符不同 */
				nextval[i] = k;	/* 则当前的j为nextval在i位置的值 */
      		else 
				nextval[i] = nextval[k];	/* 如果与前缀字符相同,则将前缀字符的 */
											/* nextval值赋值给nextval在i位置的值 */
    	} 
		else 
			k= nextval[k];			/* 若字符不相同,则k值回溯 */
  	}
}

解释:

nextval[k]:

  • nextval[k] 表示在位置 k 上的最长相同前后缀的长度,但如果前后缀相同,则向前追溯到下一个不同的字符。
  • 例如,如果 T 为 "ababa",那么 nextval[5] 会是 2,而不是 4,因为 "ababa" 的最长相同前后缀是 "ab" 而不是 "abab"

这里的操作相当于“先看看”到底前缀有几个连续相同,然后再“一起挪动”。

实现匹配算法,只需改动声明 “ get_next( T, next ); ” 为 “ get_nextval( T, next);” 即可。

理解

这里结合“电影院开灯找座位”和“英文文字找单词”的例子,找之前先入为主能找着“单词”,通过一次又一次的“开灯”所得到的有限信息,做出“移位操作”;

比如说,从一段“无限长度”的话(有头不知尾),从前往后,先找a开头的,再找ab开头的,不匹配时考虑往后是选择怎样的“前缀”继续找;

倘若出现连续的情况,比如单词为aaaac,那就找到好几个aaa开头的,当发现文章中从第几个开始不匹配时,自然而然整个挪到后边比较。

所以KMP就是 next + 匹配。

碎碎念

要怎样奔跑的努力,让所有的闲言碎语都追赶不及。。。

  • 33
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王悦雨的向北日记

我找不到很好的原因

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值