考研复习之数据结构笔记(八)数组和串(下)(包含串的相关内容)

本文详细介绍了字符串(串)的基础概念、顺序存储与链式存储实现、经典模式匹配算法,特别是重点剖析了KMP算法及其优化,包括原始KMP、改进版next数组和nextval优化。涵盖了串的定长顺序存储结构、堆分配存储以及在实际项目中的应用实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、串的定义

1.1相关概念与特点

(1)相关概念

(2)特点

二、串的表示和实现

2.1 串的定长顺序存储

(1)结构体定义

(2)串的连接

(3)串的子串

2.2 串的堆分配存储

(1)结构体定义

 (2)初始化串、串的清空与销毁

(3)串的长度

(4)串的赋值

(5)串的插入

(6)串的删除

(7)串的比较

(8)串的连接

2.3 串的链式存储

三、经典实例

3.1 串的模式匹配算法

3.2 KMP算法(重点)

(1)经典KMP

(2)KMP改进

(3)KMP优化

四、小结


一、串的定义

1.1相关概念与特点

(1)相关概念

概念引入:

        字符串简称串,计算机上非数值处理的对象基本都是字符串数据。我们常见的信息检索系统 (如搜索引擎)、文本编辑程序(如Word)、问答系统、自然语言翻译系统等,都是以字符串数据 作为处理对象的。本章详细介绍字符串的存储结构及相应的操作。

基本概念:

串( string)是由零个或多个字符组成的有限序列,又名叫字符串。
一般记为: S=′a1​a2​...an′​(n>=0)。
其中, S 是串名,单引号括起来的字符序列是串的值; an​ 可以是字母、数字或其他字符;串中字符的个数 n称为串的长度。

其他概念:
空串:
n=0 时的串称为空串。
空格串:是只包含空格的串。注意它与空串的区别,空格串是有内容有长度的,而且可以不止一个空格。
子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。子串在主串中的位置就是子串的第一个字符在主串中的序号。
 

(2)特点

串的逻辑结构和线性表极为相似,区别仅在于串的数据对象限定为字符集。

在基本操作上,串和线性表有很大差别。线性表的基本操作主要以单个元素作为操作对象,如查找、插入或删除某个元素等;而串的基本操作通常以子串作为操作对象,如查找、插入或删除一个子串等。

二、串的表示和实现

2.1 串的定长顺序存储

(1)结构体定义

        类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区,则可用定长数组如下描述

#define MAXLEN 255	//预定义最大串长为255
typedef struct{
	char ch[MAXLEN];	//每个分量存储一个字符
	int length;	//串的实际长度
}SString;

        其中,MAXLEN表示串的最大长度,ch是存储字符串的一维数组,每个分量存储一个字符。length 表示字符串的当前长度。为了便于说明问题,后面算法描述当中所用到的顺序存储的字符串都是从下标为1的数组分量开始存储的,下标为0的分量闲置不用。

(2)串的连接

        相对而言比较简单,可参考c语言中<string.h>下有关字符串的一系列相关源码,此处不再做整体思路介绍,只做简单的注释

int Concat(SString *T,SString S1,SString S2)
	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
{
	int i = 1,j,uncut = 0;
	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
	{
		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
			(*T)[i] = S1[i];
		for (j = 1; j <= S2[0]; j++)
			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
		(*T)[0] = S1[0] + S2[0];
		uncut = 1;
	}
	else if(S1[0] < MAXSTRLEN)		//截断
	{
		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
			(*T)[i] = S1[i];
 
		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
		{
			(*T)[j] = S2[j - S1[0] ];
			(*T)[0] = MAXSTRLEN;
			uncut = 0;
		}
	}
	else
	{
		for (i = 0; i <= MAXSTRLEN; i++)
			(*T)[i] = S1[i];
		/*或者分开赋值,先赋值内容,再赋值长度
		for (i = 1; i <= MAXSTRLEN; i++)
			(*T)[i] = S1[i];
		(*T)[0] = MAXSTRLEN;
		*/
		uncut = 0;
	}
 
	return uncut;
}

(3)串的子串

返回串S的第pos个字符起长度为len的子串

int SubString(SString *Sub,SString S,int pos,int len)
	//用Sub返回串S的第pos个字符起长度为len的子串
	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
	//第1个字符的下标为1,因为第0个字符存放字符长度
{
	int i;
	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
		return 0;
	for (i = 1; i <= len; i++)
	{
		//S中的[pos,len]的元素 -> *Sub中的[1,len]
		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
	}
	(*Sub)[0] = len;
	return 1;
}

2.2 串的堆分配存储

(1)结构体定义

        多数情况下,串的操作是 以串的整体形式参与的,串变量之间的长度相差较大,在操作中串值长度的变化也较大,为串变量设定固定大小的空间不够合理。

        在C语言中,存在一个称之为“堆”(Heap)的自由存储区,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的地址,同时为了以后处理方便,约定串长也作为存储结构的一部分。这种字符串的存储方式也称为串的堆式顺序存储结构,定义如下:

typedef struct{
	char *ch;	//按串长分配存储区,ch指向串的基地址
	int length;	//串的长度
}HString;

 (2)初始化串、串的清空与销毁

//初始化串
InitString(HeapString *S)
{
    S->length=0;
    S->str='\0';
}


// 清空串
void StrClear(HeapString *S)
/*清空串,只需要将串的长度置为0即可*/
{
 
    S->str='\0';
    S->length=0;
}

//销毁串
void StrDestroy(HeapString *S)
{
    if(S->str)
        free(S->str);
}

(3)串的长度

int StrEmpty(HeapString S)
/*判断串是否为空,串为空返回1,否则返回0*/
{
    if(S.length==0)         /*判断串的长度是否等于0*/
        return 1;           /*当串为空时,返回1;否则返回0*/
    else
        return 0;
}
int StrLength(HeapString S)
/*求串的长度操作*/
{
    return S.length;
}

(4)串的赋值

void StrAssign(HeapString *S,char cstr[])
/*串的赋值操作*/
{
    int i=0,len;
    if(S->str)
        free(S->str);
    for(i=0;cstr[i]!='\0';i++); /*求cstr字符串的长度*/
        len=i;
    if(!i)
    {
        S->str=NULL;
        S->length=0;
    }
    else
    {
        S->str=(char*)malloc((len+1)*sizeof(char));
        if(!S->str)
            exit(-1);
        for(i=0;i<len;i++)
            S->str[i]=cstr[i];
 
        S->length=len;
    }
}

(5)串的插入

int StrInsert(HeapString *S,int pos,HeapString T)
/*串的插入操作。在S中第pos个位置插入T分为三种情况*/
{
    int i;
    if(pos<0||pos-1>S->length)      /*插入位置不正确,返回0*/
    {
        printf("插入位置不正确");
        return 0;
    }
    S->str=(char*)realloc(S->str,(S->length+T.length)*sizeof(char));
    if(!S->str)
    {
        printf("内存分配失败");
        exit(-1);
    }
 
    for(i=S->length-1;i>=pos-1;i--)
        S->str[i+T.length]=S->str[i];
    for(i=0;i<T.length;i++)
        S->str[pos+i-1]=T.str[i];
 
    S->length=S->length+T.length;
    return 1;
}

(6)串的删除

int StrDelete(HeapString *S,int pos,int len)
/*在串S中删除pos开始的len个字符*/
{
    int i;
    char *p;
    if(pos<0||len<0||pos+len-1>S->length)
    {
        printf("删除位置不正确,参数len不合法");
        return 0;
    }
    p=(char*)malloc(S->length-len);             /*p指向动态分配的内存单元*/
    if(!p)
        exit(-1);
    for(i=0;i<pos-1;i++)                        /*将串第pos位置之前的字符复制到p中*/
        p[i]=S->str[i];
    for(i=pos-1;i<S->length-len;i++)                /*将串第pos+len位置以后的字符复制到p中*/
        p[i]=S->str[i+len];
    S->length=S->length-len;                    /*修改串的长度*/
    free(S->str);                           /*释放原来的串S的内存空间*/
    S->str=p;                               /*将串的str指向p字符串*/
    return 1;
}

(7)串的比较

int StrCompare(HeapString S,HeapString T)
/*串的比较操作*/
{
int i;
for(i=0;i<S.length&&i<T.length;i++) /*比较两个串中的字符*/
    if(S.str[i]!=T.str[i])          /*如果出现字符不同,则返回两个字符的差值*/
        return (S.str[i]-T.str[i]);
return (S.length-T.length);             /*如果比较完毕,返回两个串的长度的差值*/
}

(8)串的连接

int StrCat(HeapString *T,HeapString S)
/*将串S连接在串T的后面*/
{
    int i;
    T->str=(char*)realloc(T->str,(T->length+S.length)*sizeof(char));
    if(!T->str)
    {
        printf("分配空间失败");
        exit(-1);
    }
    else
    {
        for(i=T->length;i<T->length+S.length;i++)   /*串S直接连接在T的末尾*/
            T->str[i]=S.str[i-T->length];
        T->length=T->length+S.length;           /*修改串T的长度*/
    }
    return 1;
}

2.3 串的链式存储

        类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符, 也可以存放多个字符。每个结点称为块,整个链表称为块链结构。图(a)是结点大小为4 (即每个结点存放4个字符)的链表,最后一个结点占不满时通常用“#”补上;图(b)是结点大小为1的链表。

图2.3-1 串的链式存储方式

三、经典实例

3.1 串的模式匹配算法

        子串的定位操作通常称为串的模式匹配,它求的是子串(常称模式串)在主串中的位置。这里采用定长顺序存储结构,给出一种不依赖于其他串操作的暴力匹配算法。

        加入给定字符串ababcabcacbab,查找abcac在其主串中的位置,其相关流程如下:

 图3.1-1 模式匹配流程

        模式匹配算法本质上是一种暴力遍历算法。在上述算法中,分别用计数指针 i j 指示主串S 和模式串T中当前正待比较的字符位置。

算法思想为:

从主串S的第一个字符起,与模式T的第一个字符比较, 若相等,则继续逐个比较后续字符;否则从主串的下一个字符起,重新和模式的字符比较;

以此类推,直至模式T 中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数值为与模式T中第一个字符相 等的字符在主串S中的序号,否则称匹配不成功,函数值为零。

代码实现入下:

int Index(SString S, SString T)
{
    int i = 1, j = 1;
	while(i <= S.length && j <= T.length)
    {
		if(S.ch[i] == T.ch[j])
        {
			++i; ++j;	//继续比较后继字符
		}

        else
        {
			//指针后退重新开始匹配
			i = i-j+2;
			j = 1;
		}

	}

	if(j > T.length)
    {
		return i - T.length;
	}


    else
    {
		return 0;
	}
}

         简单的模式匹配算法的最坏时间复杂度为O(nm),其中 n 和 m 分别为主串和模式串的长度。

3.2 KMP算法(重点)

(1)经典KMP

        KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度为O(m+n)。

        next数组概念:一个字符串中最长的相同前后缀的长度加一。

 图3.2-1 next数组求解图

下面先直接给出KMP的算法流程:

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
        如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
        如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
        换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。

 

要了解子串的结构,首先要弄清楚几个概念:前缀、后缀和部分匹配值。

前缀指除最后一个字符以外,字符串的所有头部子串;

后缀指除第一个字符外,字符串的所有尾部子串;        

部分匹配值则为字符串的前缀和后缀的最大公共前后缀长度。

举如下简单例子:
字符串 abcdab
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
那么最长相等前后缀即为ab,最长部分匹配值就为2
字符串:abcabfabcab中最长相等前后缀是abcab,最长部分匹配值就为5

再比如 ′ababa′
′a′ 的前缀和后缀都为空集最大公共前后缀长度长度为0。
′ab′ 的前缀为 { a }  ,后缀为 { b } , { a } ∩ { b } = NULL,最大公共前后缀长度长度为0。
′aba′ 的前缀为{a,ab}, 后缀为 { a , ba } , { a , a b } ∩ { a , b a } = { a }, 最大公共前后缀长度长度为1
′abab′ ,前缀∩后缀,{a,ab,aba}∩{b,ab,bab}={ab},最大公共前后缀长度长度为2。
′ababa′ ,前缀∩后缀,{a,ab,aba,abab}∩{a,ba,aba,baba}={a,aba}, 公共元素有两个,最大公共前后缀长度长度为3。


回到最初的问题,主串为′abacabcacbab′,子串为′abcac′。
利用上述方法容易写出子串′abcac′的最大公共前后缀长度为00010,将最大公共前后缀长度值写成数组形式,就得到了最大公共前后缀长度(Partial match,PM)的表。

 图3.2-2 最大公共前后缀长度


在这里插入图片描述
下面用PM表来进行字符串匹配:


第一趟匹配过程:
发现 c与a不匹配,前面的2个字符′ab′是匹配的,查表可知,最后一个匹配字符b对应的部分匹配值为0,因此按照下面的公式算出子串需要向后移动的位数:
移动位数=已匹配的字符数−对应最大公共前后缀长度
因为2−0=2,所以将子串向后移动2位。

如下进行第二趟匹配:

第二趟匹配过程:
发现 c与 b不匹配,前面4个字符′abca′是匹配的,最后一个匹配字符a对应的部分匹配值为4−1=3,将子串向后移动3位。

如下进行第三趟匹配:


第三趟匹配过程:
子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,故KMP算法可以在O(n+m)的时间数量级上完成串的模式匹配操作,大大提高了匹配效率。

        某趟发生失配时,如果对应的部分匹配值为0那么表示已经匹配相等序列屮没有相等的前后 缀,此时移动的位数最大,直接将了串首字符后移到主串i位置进行下一趟比较;如果已匹配相 等序列中存在最大相等前后缀(可理解为首尾重合),那么将子串向右滑动到和该相等前后缀对 齐(这部分字符下一趟显然不需要比较),然后从主串的i位置进行下一趟比较。

(2)KMP改进

 使用部分匹配值时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以将PM表右移一位,这样哪个元素匹配失败,直接看它自己的部分匹配值即可。将上例中字符串′abac′的PM表右移一位,就得到了next数组

我们注意到:

  1. 第一个元素右移以后空缺的用-1来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,而不需要计算子串移动的位数。
  2. 最后一个元素在右移的过程中溢出,因为原来的子串中,最后一个元素的部分匹配值是其下一个元素使用的,但显然已没有下一个元素,故可以舍去。

有时为了使公式更加简洁、计算简单,将next数组整体+1

因此,上述子串的next数组也可以写成


最终得到子串指针变化公式 j =next[j]。
next[ j ]的含义是:在子串的第j个字符与主串发生失配时,则跳到子串的next[ j ]位置重新与主串当前位置进行比较。

通过分析,可以知道,除第一个字符外,模式串中其余的字符对应的next数组的值等于其最大公共前后缀长度加上1

//求解next
void get_next(String T, int *next){
	int i = 1, j = 0;
	next[1] = 0;
	while (i < T.length){
		if(j==0 || T.ch[i]==T.ch[j]){	//ch[i]表示后缀的单个字符,ch[j]表示前缀的单个字符
			++i; ++j;
			next[i] = j;	//若pi = pj, 则next[j+1] = next[j] + 1
		}else{
			j = next[j];	//否则令j = next[j],j值回溯,循环继续
		}
	}
}
//kmp
int Index_KMP(String S, String T){
	int i=1, j=1;
	int next[255];	//定义next数组
	get_next(T, next);	//得到next数组
	while(i<=S.length && j<=T.length){
		if(j==0 || S.ch[i] == T.ch[j]){	//字符相等则继续
			++i; ++j;
		}else{
			j = next[j];	//模式串向右移动,i不变
		}
	}
	if(j>T.length){
		return i-T.length;	//匹配成功
	}else{
		return 0;
	}
}

(3)KMP优化

前面定义的next数组在某些情况下尚有缺陷,还可以进一步优化。子串 ′aaaab′在和主串′aaabaaaaab′进行匹配时:



显然后面3次用一个和p4相同的字符跟S4比较毫无意义,必然失配。
比较毫无意义。那么如果出现了这种类型的应该如何处理呢?
如果出现了,则需要再次递归,将next[j]修正为 next[next[j]],直至两者不相等为止,更新后的数组命名为nextval。计算next数组修正值的算法如下,此时匹配算法不变。

 简单来说。它是在计算出next值的同时,如果a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next的值。

void get_nextval(String T, int *nextval){
	int i = 1, j = 0;
	nextval[1] = 0;
	while (i < T.length){
		if(j==0 || T.ch[i]==T.ch[j]){	//ch[i]表示后缀的单个字符,ch[j]表示前缀的单个字符
			++i; ++j;

			if(T.ch[i] != T.ch[j]){	//若当前字符与前缀字符不同
				nextval[i] = j;	//则当前的j为nextval在i位置的值
			}else{
				//如果与前缀字符相同
				//则将前缀字符的nextval值给nextval在i位置上的值
				nextval[i] = nextval[j];
			}
		}else{
			j = nextval[j];	//否则令j = next[j],j值回溯,循环继续
		}
	}
}

四、小结

  1. 串是内容受限的线性表,它限定了表中的元素为字符。串有两种基本存储结构:顺序存储和链式存储,但多采用顺序存储结构。串的常用算法是模式匹配算法,主要有BF算法和KMP 算法。BF算法实现简单,但存在回溯,效率低,时间复杂度为O(m*n)KMP算法对BF算法 进行改进,消除回溯,提高了效率,时间复杂度为O(m+n)
  2. 多维数组可以看成是线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。一个n维数组实质上是n个线性表的组合,其每一维都是一个线性表。数组一般采用顺序存储结构,故存储多维数组时,应先将其确定转换为一 维结构,有按“行”转换和按“列”转换两种。科学与工程计算中的矩阵通常用二维数组 来表示,为了节省存储空间,对于几种常见形式的特殊矩阵,比如对称矩阵、三角矩阵和 对角矩阵,在存储时可进行压缩存储,即为多个值相同的元只分配一个存储空间,对零元 不分配空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

D了一天bug忘了编译

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值