第四章 串、数组和广义表
波兰表达式
串的定义
串或字符串(string) 是由零个或多个字符组成的有限序列,一般记为 s=“a1 a2 … an” (n≥0)
其中,s是串的名,用双引号标识的字符序列是串的值;可以是字母、数字或其他字符;串中字符的数目n称为串的长度。零个字符的串称为空串(null string),其长度为0。
串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应地称为主串。通常称字符在序列中的序号为该字符在串中的位置。子串在主串中的位置则以子串的第一个字符在主串中的位置来表示。
当且仅当两个串的值相等,称这两个串是相等的。也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时两个串才相等。
在各种应用中,空格常常是串的字符集合中的一个元素,因而可以出现在其他字符中间。由
一个或多个空格组成的串“ ”称为空格串(blank string,请注意:此处不是空串),其长度为串
中空格字符的个数。为清楚起见,以后我们用符号“Ø”来表示“空串”。
串的类型定义、存储结构及运算
串的抽象类型定义
串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集。
串的抽象数据类型的定义如下:
串的存储结构
- 串的顺序存储
//- - - - - 串的定长顺序存储结构- - - - -
#define MAXLEN 255 //串的最大长度
typedef struct{
char ch[MAXLEN+1]; //存储串的一维数组
int length; //串的当前长度
}SString;
在C语言中,存在一个称之为“堆”(Heap)的自由存储区,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时为了以后处理方便,约定串长也作为存储结构的一部分。这种字符串的存储方式也称为串的堆式顺序存储结构,定义如下:
//- - - - - 串的堆式顺序存储结构- - - - -
typedef struct{
char *ch; //若是非空串,则按串长分配存储区,否则ch为NULL
int length; //串的当前长度
}HString;
- 串的链式存储
图4.3(a)所示为节点大小为4(每个节点存放4个字符)的链表,
图4.3(b)所示为节点大小为1的链表。
当节点大小大于1时,由于串长不一定是节点大小的整倍数,则链表中的最后一个节点不一定全被串值占满,此时通常补上“#”或其他的非串值字符(通常“#”不属于串的字符集,是一个特殊的符号)。
//- - - - - 串的链式存储结构- - - - -
#define CHUNKSIZE 80 //可由用户定义的块大小
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头和尾指针
int length; //串的当前长度
}LString;
串的模式匹配算法
子串的定位运算通常称为串的模式匹配或串匹配。
设有两个字符串S和T,设S为主串,也称正文串;设T为子串,也称为模式。在主串S中查找与模式T相匹配的子串,如果匹配成功,确定相匹配的子串中的第一个字符在主串S中出现的位置。
著名的模式匹配算法有BF算法和KMP算法。
- BF 算法
最简单直观的模式匹配算法是BF(Brute-Force)算法。
int Index_BF(SString S,SString T,int pos)
{//返回模式T在主串S中第pos个字符开始第一次出现的位置。若不存在,则返回0
//其中,T非空,1≤pos≤S.length
i=pos; 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(n + m)。
- 最坏情况下的平均时间复杂度是O(n × m)。
- KMP 算法
这种改进算法是由克努特(Knuth)、莫里斯(Morris)和普拉特(Pratt)共同设计实现的,因此简称KMP算法。此算法可以在O(n + m)的时间数量级上完成串的模式匹配操作。其改进在于:每当一趟匹配过程中出现字符不等时,无须回溯主串的指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。
【KMP算法】
int Index_KMP(SString S,SString T,int pos)
{//利用模式串T的next函数求T在主串S中第pos个字符之后的位置
//其中,T非空,1≤pos≤S.length
i=pos;j=1;
while(i<=S.length && 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; //匹配失败
}
【计算next函数值】
void get_next(SString T,int next[])
{//求模式串T的next函数值并将其存入数组next
i=1;next[1]=0;j=0 ;
while(i<T.length)
{
if(j==0‖T.ch[i]==T.ch[j]){++i;++j;next[i]=j;}
else j=next[j];
}
}
【计算next函数修正值】
void get_nextval(SString T,int nextval[])
{//求模式串T的next函数修正值并将之存入数组nextval
i=1;nextval[1]=0;j=0;
while(i<T.length)
{
if(j==0‖T.ch[i]==T.ch[j])
{
++i;++j;
if(T.ch[i]!=T.ch[j]) nextval[i]=j;
else nextval[i]=nextval[j];
}
else j=nextval[j];
}
}
KMP算法中next数组和nextval数组是如何快速求解的
求出next数组和nextval数组后如何跟主串进行匹配
数组
数组的类型定义
数组是由类型相同的数据元素构成的有序集合,每个元素称为数组元素,每个元素受n(n≥1)个线性关系的约束,每个元素在n个线性关系中的序号i1,i2,…,in称为该元素的下标,可以通过下标访问该数据元素。因为数组中每个元素处于n(n≥1)个关系中,故称该数组为n维数组。数组可以看成线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。
例如,一维数组可以看成一个线性表,二维数组可以看成数据元素是线性表的线性表。图4.10(a)所示的二维数组可以看成一个线性表:
抽象数据类型数组可定义为:
数组的顺序存储
假设每个数据元素占L个存储单元,则二维数组A[0… m−1, 0… n−1](下标从0开始,共有m行n列)中任一元素aij的存储位置可由下式确定:
LOC(i, j) = LOC(0, 0) + (n×i + j)L
其中,LOC(i, j)是aij的存储位置;LOC(0, 0) 是a00的存储位置,即二维数组A的起始存储位置,也称
为基地址或基址。
求一维、二维数组的存储位置
特殊矩阵的压缩存储
假若值相同的元素或者零元素在矩阵中的分布有一定规律,则称此类矩阵为特殊矩阵。 特殊矩阵主要包括对称矩阵、三角矩阵和对角矩阵等,下面我们重点讨论这3种特殊矩阵的压缩存储。
-
对称矩阵
-
三角矩阵
-
对角矩阵
广义表
广义表的定义
顾名思义,广义表是线性表的推广,也称为列表。
从上述定义和例子可推出广义表的如下3个重要结论。
表头、表尾均为空表。
广义表的存储结构
由于广义表中的数据元素可以有不同的结构(或是原子,或是列表),因此难以用顺序存储结构表示,通常采用链式存储结构。常用的链式存储结构有两种,头尾链表的存储结构和扩展线性链表的存储结构。
- 头尾链表的存储结构
由于广义表中的数据元素可能为原子或广义表,因此需要两种结构的节点:一种是表节点,用以表示广义表;一种是原子节点,用以表示原子。
//- - - - -广义表的头尾链表存储表示- - - - -
typedef enum{ATOM,LIST} ElemTag; //ATOM==0 :原子 ;LIST==1 :子表
typedef struct GLNode
{
ElemTag tag; //公共部分,用于区分原子节点和表节点
union //原子节点和表节点的联合部分
{
AtomType atom; //atom是原子节点的值域,AtomType由用户定义
struct{struct GLNode*hp,*tp;}ptr;
//ptr是表节点的指针域,ptr.hp 和ptr.tp 分别指向
表头和表尾
};
}*GList; //广义表类型
(1)除空表的表头指针为空外,对任何非空广义表,其表头指针均指向一个表节点,且该节点中的hp域指向广义表表头(或为原子节点,或为表节点),tp域指向广义表表尾(除非表尾为空,则指针为空,否则必为表节点)。
(2)容易分清列表中原子和子表所在层次。如在广义表D中,原子a和e在同一层次上,而b、c和d在同一层次且比a和e低一层,B和C是同一层的子表。
(3)最高层的表节点个数即广义表的长度。
以上3个特点在某种程度上可给广义表的操作带来方便。
- 扩展线性链表的存储结构