笔记: 串

一、串

串, 为限制数据类型的线性表,数据对象限定为字符集,如中文字符,英文字符,数字字符,标点字符等,分为
[顺序串],静态开辟或者动态开辟
[链串],为了避免存储密度过低,数据域一般定义数组存储多个元素,数组大小为结点的大小, 其他与链表相同
:即字符串,为字符组成的有限序列,记为S = ‘a1a2a3a4…an
子串:串中任意个连续的字符组成的子序列
子串在主串中位置:子串的第一个字符在主串中的位置

类型定义:

//静态开辟顺序串
#define MaxSize 1000
typedef struct
{
	char ch[MaxSize];
	int length;
}SString;
//动态开辟顺序串
#define MaxSize 255
typedef struct
{
	char* ch;
	int length;
}HString;
//链串
#define Default 4
typedef struct LNode
{
	char ch[Default];
	struct LNode* next;
	int length;
}LNode,*LinkString;

记录串的长度,也可以不用定义变量length,改用ch[0]来记录长度,这样做的好处是使第一个字符的位序与存储于数组的下标相同,缺点是ch[0]只有一个字节,只能记录0~255的变化范围,串长度超过255不可取
最好的处理办法是闲置ch[0],并额外定义变量length

常用操作函数

Empty(S)
Length(S)
ClearString(&S) 作用等同于InitString
DestoryString(&S)
SubString(&Sub, S, pos, len) 返回串S中pos位置起长度为len的子串给串Sub
StrAssign(&T, chars) 将chars赋值给串T
StrCopy(&T, S) 串S值复制给串T
Concat(&T, S1, S2) 串S1连接串S2返回给串T
StrCompare(S, T) 按序比较字符ASCII码, 若S>T返回>0, 若S<T返回<0, 若字符完全相等且出现S或T无字符进行比较,则返回S.length - T.length

定位操作
int Index(S, T) 若S中存在子串T, 返回第一次出现的位置,否则返回0

二、串的模式匹配

常用操作中的定位操作又称串的模式匹配, 具体算法如下

int Index(SString S, SString T)
{
	int n = StrLength(S);
	int m = SreLength(T);
	SString sub;
	for (int i = 1, i < n - m + 1, i++) {
		SubString(sub, S, i, m);
		if(StrCompare(sub, T) == 0)
			return i;
	}
	return 0;
}

相比之下有更佳算法,如下

朴素模式匹配算法

在这里插入图片描述
实质为不调用其他基本操作的最基本定位操作.方法为:
在主串中建立k指针,在k位置开辟出与模式串长度相同的子串,建立子串指针i与模式串指针j并默认指向串头,检索当前字符,若匹配,i,j向右移动继续检索,若不匹配,放弃当前子串,k移动至下一个子串,i,j重置至串头

int Index(SString S, SString T)
{
	int k = 1;  //k指向主串S当前匹配位置
	int i = k;   //i指向子串当前对比位置
	int j = 1;   //j指向模式串T当前对比位置
	while (i <= S.length&& j <= T.length) {
		if (S.ch[i] == T.ch[j]) {			//当前子串位置i和模式串位置j匹配
			i++;
			j++;
		}  else  {							//k游标向右走,i跟随k,j重置至模式串头
			k++;
			i = k;
			j = i;
		}
	}
	if (j > T.length)    //j移动到模式串末尾之后代表匹配成功
		return k;
	else
		return 0;
}

KMP算法

对于朴素模式匹配算法,最坏情况是:每次子串指针i和模式串指针j移动到最后一位发现不匹配,故不得不令i回溯到下一个子串的串头,j回溯至模式串的串头,如果坏情况反复出现,即反复出现指针回溯,会造成比较高的时间复杂度
KMP实际是对朴素模式匹配算法的优化,取消子串指针,改为定义在主串按序移动的指针i,模式串指针j与i不匹配时,检索模式串匹配成功部分的前后缀,若有相同前后缀,找出最大的公共前后缀,此后缀位置存储下一次检索过程中匹配成功的部分,向右移动模式串公共前后缀偏移量的长度,使前缀移动到后缀的位置,故j不需要回溯至串头,只有无公共前后缀时,j才回溯至模式串串头.整个过程中i不回溯


第一次发生不匹配时,匹配成功的部分为 ABBAB i指向B,j指向模式串第6位

在这里插入图片描述
匹配成功的部分有最大公共前后缀 AB , 偏移量为3
在这里插入图片描述
模式串向右移动3个位置,即j指针回溯 3 个位置, 指向模式串第3位,之前的后缀部分成为新匹配成功的部分,此时匹配成功,i,j继续向右移动在这里插入图片描述
此时发生了第二次不匹配,最大公共前后缀偏移量为5
在这里插入图片描述
模式串向右移动5个位置,即j指针回溯 5 个位置, 指向模式串第2位,此时虽然i,j匹配,但模式串剩余长度超过主串剩余长度,匹配失败

注:发生不匹配移动模式串后,j指针回溯后的位序为公共前后缀长度 + 1,因此可检索模式串,模拟每个位序发生不匹配时,j应该回溯到的位序.因此可针对模式串建立next数组
在这里插入图片描述
p代表模式串的子串元素,存储在此元素下标代表的位序发生不匹配时应回到的位序.
上述过程中求next数组手算比较容易,但代码实现起来很困难,方法思想如下:

  1. 设立指向最大公共前缀的后一个元素的指针j,与跟随数组位序与模式串对比位置移动的指针i
  2. 故next[i] = j 则P1P2P3…Pj-1 = Pi-j+1…Pi-1, i指向Pj
  3. 讨论next[i+1]时Pj = Pi与Pj != Pi的情况
  4. 若Pj=Pi,则代表P1P2P3…Pj-1 Pj =Pi-j+1…Pi-1Pi,next[i+1] = next[i] + 1
  5. 若Pj != Pi,视为一个新KMP匹配过程,将P1P2P3…Pj-1 Pj比作模式串与Pi-j+1…Pi-1Pi进行对比,因为Pj != Pi,且P1P2P3…Pj-1与Pi-j+1…Pi-1有相同的最大公共前后缀,故将j回溯,j=next[j],此时j之前的元素与i之前对应数量的字符已经匹配,都为之前两个子串的最大公共前后缀,j指向的字符发生变化,对比新j指针与i指针指向的字符是否匹配,此时又将问题转化为Pj是否等于Pi
    在这里插入图片描述
    代码实现:
int* Build_next(SString T)
{
	int* next = (int*)malloc(T.length * sizeof(int));
	memset(next, 0, T.length * sizeof(int));  //此时next[1] = 0;
	int i =	1, j = 0;  //i在模式串上按序移动,j指向模式串i位置前最大公共前缀的后一个元素
	while (i < T.length) {  
		if (j == 0|| T.ch[i] == T.ch[j]) {  //j=0代表初始状态或j已经从第1位回溯到0  
			i++;
			j++;
			next[i] = j;  //j在自增之前是上一个next的元素,
		}  else  {
			j = next[j];  //回溯
		}
	}
	return next;
}

KMP算法进行,初始时i,j = 1,
匹配时i,j向右移动继续比对
不匹配时,若j != 1,代表有匹配成功的部分,j回溯至对应数组元素所存储的位序,即j = next[j]并重新进行比对, 若j = 1,代表第一位不匹配,此时next[j] = 0,j回溯到0,i,j向右移动继续比对
在这里插入图片描述
如图所示针对模式串建立next数组,假若第9位不匹配,j回溯数组下标9存储的位序,即第3位,若再不匹配,回溯数组下标3存储的位序,即第1位,若再不匹配,i向右移动继续比对

KMP算法代码实现:

int Index_KMP(SString S, SString T)
{
	int* next;
	next = Build_next(T);
	int i = 1;			//标记主串
	int j = 1;			//标记模式串
	//S.length - i  >= T.length - j 即主串剩余长度不小于模式串剩余长度
	while (i <= S.length - T.length + j&& j <= T.length) { 
		if (j == 0|| S.ch[i] == T.ch[j]) {
			i++;
			j++;
		}  else  {
			j = next[j];
		}
	}
	if (j > T.length)
		return i - T.length;
	else
		return 0;
}

优化的KMP算法

KMP算法相对于朴素模式匹配算法,减少了模式串指针的回溯,增加了效率.但在特殊情况下存在一些问题

在这里插入图片描述在这里插入图片描述
如图所示,假如模式串第四位发生了匹配失败的情况,根据其next数组,应令指针j回溯到第一位进行比对,但实际上第四位与第一位的字符完全相同,即j回溯之后的比对必然失败,为了避免这种"无用"的回溯,可以对next数组进行优化
在这里插入图片描述
如图所示,当Pj=Pnext[j]时,需要将next[j]递归修正为next[next[j]],修正后进行判定,若Pj=Pnext[next[j]],则需再次递归修正,以此反复直至两者不相等,这样就避免了j指针"无用"的回溯,因此可对Build_next函数做相应修改实现next数组的更新,更新后命名为nextval数组,相应的构建函数名为Build_nextval
代码实现:

int* Build_nextval(SString T)  //根据模式串直接建立nextval数组
{
	int* nextval = (int*)malloc(T.length * sizeof(int));
	memset(nextval, 0, T.length * sizeof(int));  
	int i =	1, j = 0;  
	while (i < T.length) {  
		if (j == 0|| T.ch[i] == T.ch[j]) {  //j=0代表初始状态或j已经从第1位回溯到0  
			i++;
			j++;
			if (T.ch[i] != T.ch[j]) { 
				nextval[i] = j;   //P~j~!=P~next[j]~ ,nextval值与next数组相同
			}  else  {
			nextval[i] = nextval[j];  //否则nextval值进行递归
			}	
		}  else  {
			j = nextval[j];  //回溯
		}
	}
	return nextval;
}

因此,KMP算法的优化,实质是在构建next数组中改为构建nextval数组,在子串与模式串不匹配时,j按照nextval数组的值进行回溯,即j = nextval[j];

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值