数据结构——串(五)

数据结构——串(五)

作者:黑衣侠客


一、串的定义

串是由零个或多个字符组成的有限序列,又名字符串

一般记为s=“a1a2…an”(n≥0),其中,s是串的名称,用双引号(有些书用单引号)括起来的字符序列是串的值,注意单引号不属于串的内容。ai(1≤i≤n)可以是字母、数字或其他字符,i就是该字符在串中的位置。串中的字符数目n称为串的长度,定义中我们说到了有限是指长度n是一个有限的数值。零个字符的串称为空串,它的长度为零,可以直接用双引号表示,也可以用希腊字母“Φ”来表示。所谓的序列,说明串的相邻字符之间具有前驱和后继的关系。

空格串:

  • 空格串是只包含空格的串,注意:空格串是有内容长度的,而空串则没有空格长度,而且空格串中可以不止一个空格

子串与主串:

  • 串中任意个数的连续字符组成的子序列称为该串的子串,包含子串的串叫做主串。子串在主串中的位置就是第一个字符在主串中的序号。

二、串的比较

两个数字很容易比较大小,但是两个字符串应该如何比较呢?例如:silly、stupid这样两个英文单词,这两个字符串应该如何比较?实际上,它们是在比较字母的前后顺序。它们的第一个字母都是s,我们认为不存在大小差异,而第二个字母,由于i字母比t字母要靠前,所以i<t,于是我们说, silly<stupid

事实上,串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的符号。

计算机中的常用字符是使用标准的ASCLL编码来实现的,更准确一点就是,由7位二进制数表示一个字符,总共可以表示128个字符。后来发现一些特殊符号的出现,128个不够用,于是扩展ASCLL码由8位二进制数表示一个字符,总共可以表示256个字符,这已经足够满足以英语为主的语言和特殊符号进行输入、存储、输出等操作的字符需要了。但是,单我们国家就有除汉族外的满、回、藏、蒙古、维吾尔等多个少数民族文字,换做全世界估计要有成百上千种语言文字了,显然这256个字符是不够的,因此,后来就有了Unicode编码,比较常用的就是由16位的二进制数表示一个字符,这样就可以表示大约65万多个字符,足够表示世界上所有语言的所有字符了。当然,为了和ASCLL码兼容,Unicode的前256个字符与ASCLL码完全相同。==因此,我们想要在C语言中比较两个串是否相等,必须是他们串的长度以及它们各个对应位置的符号都相等时,才算相等。即两个串:s=“a1a2…an”,t=“b1b2…bm”,当且仅当n=m,且a1=b1,a2=b2,…,an=bm时,我们认为s=t;

当两个串不相等时,我们这样判断它们的大小:
给定两个串: s=“a1a2…an”,t=“b1b2…bm”,当满足以下条件之一时,s<t。

  • n<m,且ai=bi(i=1,2,…,n)
    例如:当s=“hap”,t=“happy”,就有s<t。因为t比s多出了两个字母。
  • 存在某个k≤min(m,n),使得ai=bi(i=1,2,…,k-1),ak<bk。
    例如:当s=“happen”,t=“happy”,因为两串的前4个字母均相同,而两串第5个字母(k值),字母e的ASCLL码是101,而字母y的ASCLL码是121,很显然,e<y,所以s<t。

三、串的抽象数据类型

相比于线性表,串更多的是查找子串的位置、得到指定位置的子串、替换子串等操作

ADT	串(string)
Data
	串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
	StrAssign(T,*chars):生成一个其值等于字符串常量chars的串T
	StrCopy(T,S):串s存在,由串S赋值得串T
	ClearString(S):串S存在,将串清空
	StringEmpty(S):若串S为空,返回true,否则返回false
	StrLength(S):返回串S的元素个数,即串的长度
	StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0
	Concat(T,S1,S2):用T返回由S1和S2联接而成的新串
	SubString(Sub,S,pos,len):串S存在,1≤pos≤StrLength(S),0≤len≤StrLength(S)-pos+1,用Sub返回串S的第pos个字符起长度为len的子串。
	Index(S,T,pos):串S和T存在,T是非空串,1≤pos≤StrLength(S)。若主串S中存在和串T值相同的子串
	Replace(S,T,V):串S、T和V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
	StrInsert(S,pos,T):串S和T存在,1≤pos≤StrLength(S)+1。在串S的第pos个字符之前插入串T。
	StrDelete(S,pos,len):串S存在,1posStrLength(S)-len+1。从串S中删除第pos个字符起长度为len的子串。
endADT

对于不同的高级语言,其实对串的基本操作会有不同的定义方法,所以,我们在用某个语言操作字符串时,需要先查看此高级语言对字符串的基本操作方法。

Index的实现算法:

//T为非空串。若主串S中第pos个字符之后,存在与T相等的子串
//则返回第一个这样的子串在S中的位置,否则返回0
int Index(String S, String T,int pos)
{
	int n,m,i;
	String sub;
	if(pos > 0)
	{
		n=StrLength(S);			//得到主串S的长度
		m=StrLength(T);			//得到子串T的长度
		i=pos;
		while(i<=n-m+1)
		{
			SubString(sub,S,i,m);		//取主串第i个位置,长度与T相等子串给sub
			if(StrCompare(sub,T)!=0)	//如果两串不相等
				++i;
			else						//如果两串相等
				return i;
		}
	}
	return 0;						//若无子串与T相等,返回0
}

其中,用到了StrLength、SubString、StrCompare等基本操作来实现

四、串的存储结构

串的存储结构分两种

1. 串的顺序存储结构

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

对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由存储区,叫做“堆”。这个堆可由C语言的动态分配函数malloc()和free()函数来管理。

2. 串的链式存储结构

对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每一个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以存放多个字符,最后一个结点如果没有被占满,可以使用“#”或其他非串值字符补全。
在这里插入图片描述

串的链式存储结构除了在连接串与串操作时有一定的方便之外,总的来说没有顺序存储灵活,性能也远不如顺序存储结构要好。

五、朴素的模式匹配算法

子串的定位操作通常叫做串的模式匹配,这一部分非常重要。

假设,我们要从下面的主串S="goodgoogle"中,找到T="google"这个子串的位置。我们通常需要以下几个步骤:

  1. 主串S第一位开始,S与T前三个字母都匹配成功,但S第四个字母是d而T的是g。因此,第一位匹配失败。
    在这里插入图片描述
    其中,竖直线代表相等,闪电状代表不相等。
  1. 主串S第二位开始,主串S首字母是o,要匹配的T首字母是g,匹配失败。
    在这里插入图片描述
  1. 主串S第三位开始,主串S首字母是o,要匹配的T首字母是g,匹配失败。
    在这里插入图片描述
  1. 主串S第四位开始,主串S首字母是d,要匹配的T首字母是g,匹配失败。
    在这里插入图片描述
  1. 主串S第五位开始,S与T,6个字母全匹配,匹配成功。
    在这里插入图片描述

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

现在我们来用数组的方法来实现串的模式匹配:

//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0
//T非空,1≤pos≤StrLength(S)
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;
}

六、KMP模式匹配算法

学习kmp算法有几个很好的例子:

1. next数组值推导:

在这里插入图片描述


  1. T=“abcdex”
j123456
模式串Tabcdex
next[j]011111
  1. 当j=1时,next[1]=0;
  2. 当j=2时,j由1到j-1就只有字符"a",属于其他情况next[2]=1;
  3. 当j=3时,j由1到j-1串是"ab",显然"a"与"b"不相等,属于其他情况,next[3]=1;
  4. 同理,最终此T串的next[j]为011111;

  1. T=“abcabx”
j123456
模式串Tabcabx
next[j]011123
  1. 当j=1时,next[1]=0;
  2. 当j=2时,同上例说明,next[2]=1;
  3. 当j=3时,next[3]=1;
  4. 当j=4时,next[4]=1;
  5. 当j=5时,此时j由1到j-1的串是"abca",前缀字符"a"与后缀字符"a"相等,因此,next[5]=2;
  6. 当j=6时,j由1到j-1的串是"abcab",由于前缀字符"ab"与后缀"ab"相等,所以next[6]=3;

我们可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1;


  1. T=“ababaaaba”
j123456789
模式串Tababaaaba
next[j]011234223
  1. 当j=1时,next[1]=0;
  2. 当j=2时,next[2]=1;
  3. 当j=3时,next[3]=1;
  4. 当j=4时,j由1到j-1的串是"aba",前缀字符"a"与后缀字符"a"相等,next[4]=2;
  5. 当j=5时,j由1到j-1的串是"abab",由于前缀字符"ab"与后缀字符"ab"相等,所以next[5]=3;
  6. 当j=6时,j由1到j-1的串是"ababa",由于前缀字符"aba"与后缀字符"aba"相等,所以next[6]=4;
  7. 当j=7时,j由1到j-1的串是"ababaa",由于前缀字符"ab"与后缀"aa"并不相等,只有"a"相等,所以next[7]=2;
  8. 当j=8时,j由1到j-1的串是"ababaaa",只有"a"相等,所以next[8]=2;
  9. 当j=9时,j由1到j-1的串是"ababaaab",由于前缀字符"ab"与后缀字符"ab"相等,所以next[9]=3;

  1. T=“aaaaaaaab”
j123456789
模式串Taaaaaaaab
next[j]012345678
  1. 当j=1时,next[1]=0;
  2. 当j=2时,next[2]=1;
  3. 当j=3时,j由1到j-1的串是"aa",前缀字符"a"与后缀字符"a"相等,next[3]=2;
  4. 当j=4时,j由1到j-1的串是"aaa",由于前缀字符"aa"与后缀字符"aa"相等,所以,next[4]=3;
  5. 同上;
  6. 当j=9,j由1到j-1的串是"aaaaaaaa",由于前缀字符"aaaaaaaa"与后缀"aaaaaaa"相等,所以next[9]=8;

2. KMP模式匹配算法实现

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

这段代码的目的是为了计算出当前要匹配的串T的next数组。

//返回子串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;
}

3. KMP模式匹配算法的改进

后来发现,KMP算法还是存在缺陷的,比如:我们的主串S=“aaaabcde”,子串T=“aaaaax”,其next数组值分别为012345,在开始时,当i=5、j=5时,我们发现"b"与"a"不相等。如图1:因此j=next[5]=4;如图2:此时b与第4位置的a依然不等,j=next[4]=3;,如图3:后一次是图4和图5,直到j=next[1]=0时,根据算法,此时i++、j++,得到i=6、j=1,如图6所示。
在这里插入图片描述
我们发现,当中②③④⑤步骤,其实是多余的判断。由于T串的第二、三、四、五位置的字符都与首位的"a"相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值,因此,我们对next函数进行了改良。

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

实现匹配算法,只需要将"get_next(T,next)";改为"get_nextval(T,next)"即可


4. nextval数组值推导

  1. T=“ababaaaba”
j123456789
模式串Tababaaaba
next[j]011234223
nextval[j]010104210

先算出next数组的值分别为001234223 ,然后再进行判断。

  1. 当j=1时,nextval[1] =0;
  2. 当j=2时,因为第二位的字符"b"的next值是1,而第一位就是"a",它们不相等,所以nextval[2]=next[2]=1;维持原值。
  3. 当j=3时,因为第三位字符"a"的next值为1,所以与第一位的"a"比较得知它们相等,所以nextval[3]=nextval[1]=0;
  4. 当j=4时,第四位的字符"b"next值为2,所以与第二位的"b"相比较得到结果是相等的,因此nextval[4]=nextval[2]=1;
  5. 当j=5时,next值为3,第五个字符"a"与第三个字符"a"相等,因此nextval[5]=nextval[3]=0;
  6. 当j=6时,next值为4,第六个字符"a"与第四个字符"b"不相等,因此nextval[6]=4;
  7. 当j=7时,next值为2,第七个字符"a"与第二个字符"b"不相等,因此nextval[7]=2;
  8. 当j=8时,next值为2,第八个字符"b"与第二个字符"b"相等,因此nextval[8]=nextval[2]=1;
  9. 当j=9时,next值为3,第九个字符"a"与第三个字符"a"相等,因此nextval[9]=nextval[3]=1;
  1. T=“aaaaaaaab”
j123456789
模式串Taaaaaaaab
next[j]012345678
nextval[j]000000008

首先,算出next数组的值分别为012345678,再进行判断

当j=1时,nextval[1]=0;
当j=2时,next值为1,第二个字符与第一个字符相等,所以nextval[2]=nextval[1]=0;
同样的道理,其后都为0…
当j=9时,next值为8,第九个字符"b"与第八个字符"a"不相等,所以nextval[9]=8;

总结一下,改过的KMP算法,它是在计算出next值的同时,如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值