文章目录

一.串

串(String)----零个或多个字符组成的有限序列,是一种特殊的线性表,其数据元素为一个字符,即内容受限的线性表。

- 子串:串中任意个连续字符组成的子序列(含空串)称为该串的子串;
- 真子串是指不包含自身的所有子串。
- 主串:包含子串的串相应地称为主串;
- 字符位置:字符在序列中的序号为该字符在串中的位置(!是位置不是下标);
- 子串位置:子串第一个字符在主串中的位置;
- 空格串:由一个或多个空格组成的串,与空串不同。
- 串相等:两个串是相等的,当且仅当这两个串的值相等。也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等。例如,下例中的串a、b、c和d彼此都不相等。
串与线性表的区别:
- 📌串是限定了元素为字符的线性表
- 📌线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串(整体)
- 📌在串里面,一个数据元素是由一个字符组成
实例

1.2串的存储结构
串中元素逻辑关系与线性表的相同,串可以采用与线性表相同的存储结构:顺序存储和链式存储。但考虑到存储效率和算法的方便性,串多采用顺序存储结构。
1.2.1顺序串
类似于线性表的顺序存储结构, 用一组地址连续的存储单元存储串值的字符序列。
//串的定长顺序存储结构
#define MAXLEN 255 //串的最大长度
typedef struct{
char ch[MAXLEN+1]; //存储串的一维数组
int length; //串的当前长度
}SString;
串的实际长度可在这预定义长度的范围内随意,超过预定义长度的串值则被舍去,称之为“截断”
。对串长有两种方法表示:一种是如上定义描述的那样,以下标为0的数组分量存放串的实际长度,另一种是在串值后面加一个不计入串长的结束标记字符,此时的串长为隐含值,显然不便于进行某些串的操作。为了便于说明问题采用第一种方法,串从下标为1的数组分量开始存储,下标为0的分量闲置不用或用于存储串长,从而可以简化为:
//----- 串的定长顺序存储结构- - ---
#define MAXLEN 255 //串的最大长度
typedef struct {
char ch[MAXLEN+1]; //存储串的一维数组
} SString;
🌈串联接Concat(&T, S1, S2)
假设S 1 , S 2和T 都是String型的串变量,且串T是由串S1联杰结串S2得到的,即串T的值的前一段和串S1的值相等,串T的值的后一段和串S2的值相等,则只要进行相应的“串值复制”操作即可,只是需前述约定,对超长部分实施“截断”操作
🌈求子串SubString(& Sub, S, pos, len)
求子串的过程即为复制字符序列的过程,将串S中从第pos个字符开始长度为len的字符序列复制到串Sub中。显然,本操作不会有需截断的情况,但有可能产生用户给出的参数不符合操作的初始条件,返回false
bool SubString(SString &Sub, SString S, int pos, int len){
//子串范围越界
if(pos+len-1 > S.length){
return false;
}
for(int i = pos; i<pos+len; i++){
Sub.ch[i-pos+1] = S.ch[i];
}
Sub.length = len;
return true;
}
在顺序存储结构中,实现串操作的原操作为“字符序列的复制”,操作的时间复杂度基于复制的字符序列的长度,另一操作的特点是:如果在操作中出现串值序列的长度超过上界MAXSTRLEN时,约定用截尾法处理。这种情况不仅在求联串时可能发生,在串的其他操作中,如插入、置换等也可能发生。客服这个弊病唯有不限定串长的最大长度,即动态分配串值的存储空间。
🌈比较操作
若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
//比较操作,若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
int StrCompare(SString S, SString T){
for(int i=1; i<S.length && i<T.length; i++){
if(S.ch[i] != T.ch[i]){
return S.ch[i] - T.ch[i];
}
}
//扫描过的错有字符都相同,则长度长的串更大
return S.length - T.length;
}
🌈定位操作
若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0。
int Index(SString S, SString T){
int i = 1, n = StrLength(s), m = StrLength(T);
SString sub; //用于暂存子串
while(i<=n-m+1){
SubStriing(sub, S, i, m); //将串S从第i个字符开始长度为len的字符序列赋值到串sub中
if(StrCompare(sub, T) != 0){ //两个串的比较
i++;
}else{
return i; //返回子串在主串中的位置
}
}
return 0; //S中不存在与T相等的子串
}
*堆分配存储表示
上述定义方式是静态的,不利于串的内存空间变化,因此最好是根据实际需要, 在程序执行过程中动态地分配和释放串空间。
在C语言中, 存在一个称之为 " 堆 " (Heap)的自由存储区,可以为每个新产生的串动态分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时为了以后处理方便,约定串长也作为存储结构的一部分。 这种字符串的存储方式也称为串的堆式顺序存储结构, 定义如下:
//----- 串的堆式顺序存储结构---- -
typedef struct{
char *ch; //若是非空串,则按串长分配存储区,否则 ch 为 NULL,ch指向串的基地址
int length; //串的当前长度
}HString;
HString S;
S.ch= (char*)malloc(MAXLEN* sizeof(char));
S.length=0;
这种存储结构表示时的串操作仍是基于“字符序列的复制”进行的。
由于堆分配存储结构的串既有顺序存储结构的特点,处理方便,操作中对串长又没有任何限制,更显灵活,因此在串处理的应用程序中也常被选用
1.2.3块链
顺序串的插人和删除操作不方便,需要移动大量的字符。因此,可采用单链表方式存储串。由于串结构的特殊性——结构中的每个数据是一个字符,则用链表存储串值时,存在一个“结点大小”的问题,即每个结点可以存放一个字符,也可以存放多个字符。当结点大小大于1时,由于串长不一定是结点大小的整数倍,则链表中的最后一个结点不一定全被串值占满,此时通常补上“#”或者其他非串值字符(通常“#”不属于串的字符集,是一个特殊的符号).
第一种表示:
typedef struct StringNode{
char ch; //每个结点存放一个字符
struct StringNode* next;
}StringNode,*String;
为了便于进行串的操作,当以链表存储串值时,除头指针外还可附设一个尾指针表示链表中的最后一个结点,并给出当前串的长度。称如此定义的串存储结构为块链结构.
第二种表示
typedef struct StringNode{
char ch[4]; //每个节点存多个字符
struct StringNode * next;
}StringNode, * String;
typedef struct{
StringNode *head,*tail;//串的头指针和尾指针
int curlen; //串的当前长度
}LString;
由于在一般情况下,对串进行操作时,只需要从头向尾扫描即可,则对串值不必建立双向链表。设尾指针的目的是为了便于进行联结操作,但应注意联结时需要处理第一个串尾的无效字符。
在链式存储方式中,结点大小的选择和顺序存储方式的格式选择一样都很重要,它直接影响着串处理的效率。
📌存储密度可定义为:串值所占的存储位/实际分配的存储位
存储密度小(如结点大小为1时),运算处理方便,然而,存储占用最大,如果在串处理过程中需要进行内、外存交换的话,则会因为内外存交换操作过多而影响处理的总效率。一般地,字符集小,则字符的机内编码就短,这也影响串值的存储方式的选取。
串值的链式存储结构对某些串操作,如联接操作等有一定方便之处,但总的来说不如另两种存储结构灵活,它占用存储量大且操作复杂。
1.3串的模式匹配算法
算法目的:确定主串中所含子串(模式串)第一次出现的位置(定位),找到返回所在位置,没有找到返回0.
算法应用:搜索引擎、拼写检查、语言翻译、数据压缩
算法种类:
- BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)
- KMP算法(特点:速度快)
1.3.1BF(Brute-Force)算法
模式匹配不一定是从主串的第一个位置开始,可以指定主串中查找的起始位置pos。如果采用字符串顺序存储结构,可以写出不依赖于其他串操作的匹配算法。
【算法思路】:
-
分别利用计数指针 i 和 j 指示
主串S
和模式T
中当前正待比较的字符位置,i 初值为pos, j 初值为1。 -
如果两个串均未比较到串尾,即 i 和 j 均分别小于等于S和T的长度时,则循环执行以下操作:
S[i].ch
和T[j].ch
比较,若相等,则 i 和 j 分别指示串中下个位置,继续比较后续字符若不等,指针回溯后退重新开始匹配,从主串的下一个字符(
i=i-j+2
)起再重新和模式的第一个字符(j=1)
比较。 -
如果 j>T.length,说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则匹配成功,返回和模式T中第一个字符相等的字符在主串S中的序号
( i-T.length )
;否则称匹配不成功,返回0。
? i=i-j+2
i=i-j+2=i-(j-1)+1
j-1是j移动的距离(j看作从1开始,而不是从0开始);i-(j-1)是i回到与子串比较的起始位置(不是一直回到i=1,i在多次匹配中不断的变大)。然后[i-(j-1)] +1 就是回到起始位置之后再往后进一位。

int Index(SString S, SString T, int pos)
{
i = pos; //字符串开始查找的起始位置pos
j = 1; //模式串从头开始
while (i <= S[0] && j <= T[0]) //两个串均未比较到串尾
{
if (S[i] = T[j]) //当对应字符相等时,比较后续字符
{
++i;
++j;
} //继续比较后继字符
else //当对应位置不等时
{
i = i - j + 2; //主串指针回溯重新开始下一次匹配
j = 1; //模式串从头开始重新比较
} //指针后退重新开始匹配
}
if (j > T[0]) //匹配成功,返回匹配的第一个字符的下标
return i-T[0];
else
return 0; //匹配失败,返回0
}
若模式串长度为m,主串长度为n,
则匹配成功的最好时间复杂度为:O(m),最坏的时间复杂度(n-m)*m+m =( n − m + 1 ) ∗ m
次比较。
匹配失败的最好时间复杂度为:O(n),最坏时间复杂度就可表示为:O(nm)
【算法分析】BF算法思路直观简明。但匹配失败时,主串的指针i总是回溯到i-j+2的位置,模式串的指针总是恢复到首字符位置j=1,因此算法时间复杂度高。
1.3.2KMP算法
-
KMP算法思想
+是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数
实现,函数本身包含了模式串的局部匹配信息。KMP算法的出现就是为了解决主串指针回溯问题。 -
next函数
-
在实现算法之前,我们需要引入next函数:若令
next[j]=k
,则next[j]
表明当模式中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的当前字符的位置。由此引出next函数的定义:
求next函数:
next[ j ]=第 j 位字符前面 j-1位字符组成的子串的前后缀重合字符数 +1
实例:
- nextval值
我们在前面介绍的next函数虽然好用,但还不够完美,在某种情况下是有缺陷的,例如如下匹配过程
主串为"aaabaaaab" 模式串为"aaaab"
在求得模式串的next值之后,接着求nextval值对KMP算法进行改进:
nextval[ j ]=将X与和Y的next值相同的逻辑索引的字母进行下一轮比较,直到找到不相同为止
实例:
【算法思路】:
在求得模式串的next函数之后,匹配可按如下进行:
- 假设指针i和j分别指示主串S和模式串T中当前比较的字符,令i的初始值为pos,j的初值为1。
- 若在匹配过程中Si=Tj,则i和j分别增1;否则,i不变,而 j 退到next[ j ]位置再比较,即Si和Tnext[ j ]进行比较(** j 后移**),若相等,则指针各自增1,否则 j 再退到下一个next值得位置,以此类推,直至下列两种可能:
- 第一种: j 退到某个next值(
next[…next[j]]
)时字符比较相等,则指针各自增1继续进行匹配; - 第二种是 j 退到next值为0(即与模式串得第一个字符失配),则此时需将主串和模式串同时向右滑动一个位置( 此时j=0,当向右滑动一个位置时,即模式串得第一个字符),即从主串得下一个字符
Si+1
和模式串T1
重新开始比较。
- 根据next值求nextval值
1.第一位的nextval值必定为0,第二位如果于第一位相同则为0,如果不同则为1。
2.第三位的next值为1,那么将第三位和第一位进行比较,均为a,相同,则第三位的nextval值为第一位的next值,为0。
3.第四位的next值为2,那么将第四位和第二位进行比较,不同,则第四位的nextval值为其next值,为2。
4.第五位的next值为2,那么将第五位和第二位进行比较,相同,第二位的next值为1,则继续将第二位与第一位进行比较,不同,则第五位的nextval值为第二位的next值,为1。
5.第六位的next值为3,那么将第六位和第三位进行比较,不同,则第六位的nextval值为其next值,为3。
6.第七位的next值为1,那么将第七位和第一位进行比较,相同,则第七位的nextval值为0。
7.第八位的next值为2,那么将第八位和第二位进行比较,不同,则第八位的nextval值为其next值,为2。
int Index_KMP(SString T, int next[]){
int i=1, 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;
}
}
//求模式串T的next函数值并存入数组next
void get_next(SString T,int 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[]){
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];
}
}
时间复杂度:O(n+m)
二.数组
2.1定义
数组是由类型相同的数据元素构成的有序集合,每个元素称为数组元素,每个元素受n(n≥1 )个线性关系的约束,每个元素在n个线性关系中的序号i1,i2,…,in称为该元素的下标,可以通过下标访问该数据元素。因为数组中每个元素处于n (n≥1)个关系中,故称该数组为n维数组。数组可以看成是线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。
-
声明格式:
数据类型 变量名称[长度];
数据类型 变量名称[行数][列数]
-
一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组。逻辑结构式线性结构,定长的线性表。
-
二维数组:在C语言中,若一维数组中的数据元素又是一维数组结构,则称为二维数组。即
typedef elemtype array2[m][n];
等价于typedef elemtype array1[n]; typedef array1 array2[m];
逻辑结构分为非线性结构和线性结构,线性结构指该线性表的每个数据元素也是一个定长的线性表,非线性结构指每一个数据元素既在一个行表中,又在一个列表中。 -
同理,一个n维数组类型可以定义为其元素为n-1维数组类型的一维数组。
线性表结构是数组结构的一个特例,而数组结构又是线性表结构的扩展。
2.2数组的顺序结构
由于数组一般不做插人或删除操作,也就是说;一旦建立了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动。因此,采用顺序存储结构表示数组比较合适。
注意:数组可以是多维的,但存储数据元素的内存单元地址是一维的,因此,在存储数组结构之前,需要解决将多维关系映射到一维关系的问题,即用一组连续存储单元存放数组的数据元素有个次序约定的问题。
//二维数组结构
typedef int elem;
#define n 10
#define m 10
typedef elem array1[n];
typedef array1 array2[m];
对于数组,一旦规定了其维数和各维的长度,便可为它分配存储空间。反之,只要给出一组下标便可求得相应数组元素的存储位置。
- 一维数组
L指每个元素所占字节数,i 是数组元素下标,a是基地址

- 二维数组
二维数组可有两种存储方式:一种是以列序为主序的存储方式;一种是以行序为主序的存储方式。

- 三维数组
按页/行/列存放,页优先的顺序存储

- n维数组
2.3特殊矩阵的压缩存储
矩阵的常规存储:将矩阵描述为一个二维数组,可以对其元素进行随机存取。但是对于值相同的元素很多且呈某种规律分布的特殊矩阵,不适宜进行常规存储。我们引入特殊矩阵的压缩存储。
- 特殊矩阵: 值相同的元素或0元素在矩阵中的分布有一定的规律。如:对称矩阵、三角矩阵、对角矩阵等。
- 压缩存储: 压缩存储是指为多个值相同的元只分配一个存储空间,且对零元不分配存储空间。目的是节省大量存储空间。
什么样的矩阵能够压缩?
特殊矩阵、稀疏矩阵等。
- 对称矩阵
根据位置特性,用等差数列计算,注意这里因为下标是从1开始的,最后要减1
- 三角矩阵
以主对角线划分,三角矩阵有上三角矩阵和下三角矩阵两种。上三角矩阵是指矩阵下三角(不包括对角线)中的元均为常数c或零的n阶矩阵,下三角矩阵与之相反。对三角矩阵进行压缩存储时,除了和对称矩阵一样,只存储其上(下)三角中的元素之外,再加一个存储常数c的存储空间即可。
[特点]对角线以下(或者以上)的数据元素(不包括对角线)全部为常数C。
[存储方法]重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素。
上三角矩阵
sa[k]和矩阵元aij之间的对应关系为:
下三角矩阵
sa[k]和矩阵元aij之间的对应关系为:
在下三角矩阵中,i<j 表示超过了下三角区域的范围,与对称矩阵不同的是,需要再加一个存储常数c的存储空间
- 对角矩阵
对角矩阵所有的非零元都集中在以主对角线为中心的带状区域中,即除了主对角线上和直接在对角线上、下方若干条对角线上的元之外,所有其他的元皆为零。
- 稀疏矩阵
设在m*n
的矩阵中有 t 个非零元素。令 δ = t / (m*n)
,当 δ<=0.05时,称为稀疏矩阵。压缩存储原则:存个非零元素的值、行列位置和矩阵的行列数。
- 存储方法1:三元组
三元组顺序表又称有序的双下标法。三元组顺序表的优点:非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。三元组顺序表的缺点:不能随机存取。若按行号存取某一行中的非零元,则需从头开始进行查找。
(PS:为了更准确的描述,通常再加一个“总体”信息:即总行数、总列数、非零元素总个数。)
- 存储方法2:十字链表
优点:它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的各种运算。在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row, col,value)以外,还要有两个域:
right:用于链接同一行中的下一个非零元素;
down:用以链接同一列中的下一个非零元素。
十字链表中结点的结构示意图:
每一行/列有一个指针,指向该行/列的第一个元素
三.广义表
3.1定义
广义表:n ( ≥0)个表元素组成的有限序列,记作LS=(a1,a2,…,an)
。LS是表名,ai是表元素,它可以是表(称为子表),可以是数据元素(称为原子)。n为表的长度,n=0的广义表称为空表。
- 求表头GetHead(L):非空广义表的第一个元素,可以是一个原子,也可以是一个子表。
- 求表尾GetTail(L):非空广义表除去表头元素以外其它所有元素所构成的表。表尾一定是一个表。
广义表的性质

3.2广义表的存储结构
由于广义表中的数据元素可以有不同的结构(或是原子,或是列表),因此难以用顺序存储结构表示,通常采用链式存储结构。
头尾链表的存储结构
由于广义表中的数据元素可能为原子或广义表,由此需要两种结构的结点:一种是表结点,用以表示广义表;一种是原子结点,用以表示原子。若广义表不空,则可分解成表头和表尾,因此,一对确定的表头和表尾可唯一确定广义表。
- 一个表结点可由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是表结点的指针域,prt.hp和ptr.tp分别指向表头和表尾
};
} * GList, GLNode; /* 广义表类型 */
*扩展线性链表的存储结构
在这种结构中,无论是原子结点还是表结点均由三个域组成
typedef struct glnode
{
int tag; // 0 原子结点;1 子表结点
union
{
atomtype atom; //原子结点的值域
struct glnode *hp; //子表表头指针
} struct glnode *tp; //下一元素指针
} * glist;
3.3、广义表与线性表的区别
广义表可以看成是线性表的推广,线性表是广义表的特例。
广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。
当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。
另外,树和有向图也可以用广义表来表示。
由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。
四.总结
1.串是内容受限的线性表,它限定了表中的元素为字符。串有两种基本存储结构:顺序存储和链式存储,但多采用顺序存储结构。串的常用算法是模式匹配算法,主要有BF算法和KMP算法。BF算法实现简单,但存在回溯,效率低,时间复杂度为O(m ×n)。KMP算法对BF算法进行改进,消除回溯,提高了效率,时间复杂度为O(m+n)。
2.多维数组可以看成是线性表的推广,其特点是结构中的元素本身可以是具有某种结构的数据,但属于同一数据类型。一个n维数组实质上是n个线性表的组合,其每一维都是一个线性表。数组一般采用顺序存储结构,故存储多维数组时,应先将其确定转换为一维结构,有按“行”转换和按“列”转换两种。科学与工程计算中的矩阵通常用二维数组来表示,为了节省存储空间,对于几种常见形式的特殊矩阵,比如对称矩阵、三角矩阵和对角矩阵,在存储时可进行压缩存储,即为多个值相同的元只分配一个存储空间,对零元不分配空间。
3.广义表是另外一种线性表的推广形式,表中的元素可以是称为原子的单个元素,也可以是一个子表,所以线性表可以看成广义表的特例。广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。广义表的常用操作有取表头和取表尾。广义表通常采用链式存储结构:头尾链表的存储结构和扩展线性链表的存储结构。