第四章串、数组和广义表
1.串(字符串)
-
串——零个或多个任意字符组成的有限序列(内容受限的线性表)。
-
子串——串中任意个连续字符组成的子序列(包含空串)称为该穿的子串。
例如,“abcde"的子串有:”"、“a”、“ab”、“abc”、“abcd”、"abcde"等。
注:真子串指不包含自身的所有子串。
- 字符位置——字符在序列中的需要为字符在串中的位置。
- 子串位置——子串第一个字符在主串中的位置。
- 空格串——由一个或者多个空格字符组成的串,不同于空串。
- 空串——不包含任何字符。
- 串的长度——字符个数
- 串相等——当且仅当两个串长度相等并各个对应位置上的字符都相同。所有空串都相等
案例1:病毒感染检测
研究者将人的DNA和病毒DNA均表示成由字母组成的字符串序列。然后检测某种病毒DNA序列是否在患者的DNA序列出现过,如果出现则已经感染,否则没有感染。(注:人的DNA序列是线性的,病毒的DNA序列是环状的)。
例如病毒DNA序列为baa,由于病毒DNA序列是环状的,因此融入人的DNA中情况有baa、aab、aba三种。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znfgwAa5-1689241591145)(F:\桌面文件\编程学习\img\image-20230711161502109.png)]
*患者1的DNA序列为aaabbba,此时患者1感染了。
*患者2的DNA序列为babbba,此时患者2没有感染。
2.串的定义
2.1.类型定义
ADT String{
数据对象:D={ai | ai∈Char, i=1,2,3……}
数据关系:R={<ai-1 , ai> | ai-1,ai ∈ D, i=1,2,3……}
基本操作:
StrAssign(&T,chars) //串赋值
StrCompare(S,T) //串比较
StrLength(S) //求串长
Concat(&T,S1,S2) //串连结
SubString(&Sub,S,pos,len) //求子串
StrCopy(&T,S) //串拷贝
StrEmpty(S) //串判空
ClearString(&S) //清空串
Index(S,T,pos) //子串位置
Replace(&S,T,V) //串替换
StrInsert(&S,pos,T) //子串插入
StrDelete(&S,pos,len) //子串删除
DestroyString(&S) //串销毁
}ADT String
2.2串的顺序存储—顺序串
1.顺序存储实现(用的比较多)
#define MAXLEN 255
typedef struct{
char ch[MAXLEN+1]; //存储串的一堆数组 这里表示数组下标为[0] ~ [255]。为了某些算法的渐变,一般0下标不用
int length; //串当前长度
}SString;
2.3串的链式存储—链串
串的链式存储示意图:一个结点存储一个字符
一个结点存储一个字符会导致存储密度低。存储密度=串值所占存储/实际分配的存储。
为了解决存储密度低,可以将多个字符放在一个结点。
1.链式存储实现—块链结构
#define CHUNKSIZE 80 //定义块的大小
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct
Chunk *head,*tail; //定义串的头指针和尾指针
int curlen; //串的当前长度
}LString; //字符串的块链结构
3.串的模式匹配算法
算法目的:确定主串中所含子串第一次出现的位置。
算法应用:搜索引擎、拼写检查、语言翻译、数据压缩
算法种类:
—BF算法(Brute-Force):暴力破解法
—KMP算法:特点速度快
3.1BF算法(顺序存储)
简单匹配算法(Brute-Force)简称BF算法。采用穷举法的思路。即一个一个比较。
算法思路:从正文串的每一个字符开始,依次与T的字符进行匹配比较。
例如有正文串:aaaabcd
模式串(匹配的子串):abc
【案例1】:设目标串S=“aaaaab”,模式串T=“aaab”
S的长度为n(n=6),T的长度为m(m=4)。
BF算法匹配过程:用“ i ”记录正文串匹配字符,用“ j ”记录模式串匹配字符。两个都从1开始。
当遇到不匹配字符时,“ i ”回退到刚匹配的第一个字符的后一位(i-j+2)。" j "回退到1。
当匹配成功时。返回" i - T.length"为子串出现的位置。
【算法设计思想】:Index(S,T,pos)
*将主串的第pos个字符和模式串的第一个字符比较
*若相等,继续逐个比较后续字符。(i - j + 2)
*若不等,从主串的下一个字符起,重新与模式串的第一个字符比较。(j = 1)
*直到主串的一个连续子串字符序列与模式串相等,返回值为S中的T匹配的子序列第一个字符的序号,即匹配成功。否则匹配失败返回值0。
【算法实现】
int Index_BF(SString S,SString T){
int i=1,j=1;
while(i<=S.length && j<=T.length){ //i不超过主串长度,j超过模式串的长度
if(S.ch[i] == T.ch[j]){ //主串和模式串字符匹配,匹配下一个字符
i++;
j++;
}else{ //主串和模式串字符不匹配,则回溯
i = i-j+2;
j = 1;
}
}
if(j>= T.length){ //如果是j超过模式串长度,即匹配成功
return i-T.length; //返回主串匹配模式串的第一个字符
}
else{ //如果是i超过主串的长度,即匹配失败
return 0; //返回值0
}
}
倘若主串不从第一个位置开始,而是从某个值pos开始。只需要把" i "刚开始值赋为pos。其余相同
int Index_BF(SString S,SString T,int pos){
int i=pos,j=1;
while(i<=S.length && j<=T.length){ //i不超过主串长度,j超过模式串的长度
if(S.ch[i] == T.ch[j]){ //主串和模式串字符匹配,匹配下一个字符
i++;
j++;
}else{ //主串和模式串字符不匹配,则回溯
i = i-j+2;
j = 1;
}
}
if(j>= T.length){ //如果是j超过模式串长度,即匹配成功
return i-T.length; //返回主串匹配模式串的第一个字符
}
else{ //如果是i超过主串的长度,即匹配失败
return 0; //返回值0
}
}
BF算法的时间复杂度:若n为主串的长度,m为子串的长度。
最坏情况:主串前面 n - m 个位置的部分都与子串匹配到最后一位,即总次数为(n - m +1) *m。
若m<<n(m远远小于n),则算法复杂度为O(n*m)。
平均情况:O(n/2 * m)。
3.2KMP算法(顺序存储)
- KMP较BF算法有较大改进,使算法效率有了某种程度的提高。利用部分已经匹配的结果,主串指针" i “不必回溯,而从当前位置继续比较。模式串” j "回溯到相应位置。
【算法思想】
利用已经部分匹配的结果加快模式串的滑动速度,主串的指针" i "不比回溯,可提速到O(n+m)。
利用一个数组定义为next[j](注:这里" j "从1开始),来存储第“ j ”个字符与主串中相应字符“ 失配 ”时,在模式串中需要回溯的位置。
注:一般的,第一个是0,第二个是1。
例如:
简单说:就是看这个字符前面的n个字符,与开头的n个字符一样。则k-1=n。所以next[j] = k。
【算法实现】
int Index_KMP(SString S,SString T,int pos){
int i = pos,j=1;
while(i<S.length && j<T.length){
if(j == 0 || S.ch[i]==T.ch[j]){
i++;
j++;
}//if
else{
//i不变,j退到next[j]记录的数组。
j = next[j];
}
}
if(j>T.length){ //如果是j已经超出长度,即匹配成功,返回这一组第一个字符位置
return i-T.length;
}
else //如果是i超出长度,即匹配失败,返回值0。
return 0;
}
//如何求next[j]
void get_next(SString T,int &next[]){
i=1; //用来标记目前模式串中next数组的位置
next[1]=0; //第一个一定是0
j=0; //用来标记需要回溯的值
while(i<T.length){
if(j==0 || T.ch[i] == T.ch[j]){ //模式串中,如果i标记的字符与j标记的字符相等,则同时自增,并且next[i]=j。
++i;
++j;
next[i]=j;
}//if
else{
j=next[j]; //如果两个位置不相等,则j=next[1]。即j=0,也可以使得上面的if继续循环。
}
}
}
当模式串" aaaab “与主串” aaabaaaab "匹配时,当i=4,j=4时候,S.ch[4]≠T.ch[4]还需要继续进行i=4,j=3……等比较。所以这个next数组是有缺陷的。
所以对next进行修正。用nextval[]数组存储。nextval是根据next值来求。
对比nextval数组与next数组来说,nextval数组在j回溯到0,也就是j
【nextval数组实现】
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数组与next数组不同的地方在这里。
nextval[i]=nextval[j];
}
}else{
j=nextval[j];
}
}
}
4.数组
-
数组:按照一定格式排列起来的具有相同类型的数据元素的集合。
-
一维数组:像线性表中数据元素为非结构的简单元素,则为一位数组。逻辑结构为线性结构。也就是定长的线性表。
-
一维数组的声明格式: 数组类型 变量名称[长度];
例:int num[5] = {0, 1, 2, 3, 4}; (这里的下标为 0~4)
-
二维数组:若一维数组中,数据元素又是一堆一维数组结构,则称为二维数组。
如示例图:
-
二维数组的声明格式:数据类型 变量名称 [行数] [列数];
例:int num[5] [8];
在C语言中,二维数组也可以定义为一维数组类型。
typedef elemtype array2[m][n];
//等价于
typedef elemtype array1[n];
typedef array1 array2[m];
- 以此类推,三维数组……n维数组,即n-1为数组中的元素又是一个一维数组结构,成为n维数组。
注:线性表结构是数组结构的一个特例,而数组结构又是线性表结构的拓展。
数组特点:结构定义后,维数和维界不再改变。除了初始化和销毁外,只有取(get)或修改(set)元素值的操作。
1.数组的存储
假设一个二维数组 a[m] [n]。即m行n列
1.以行为主序:
先按照一维数组存储a[0] [0]到a[0] [n]。然后a[1] [0] 在a[0] [n]后。以此类推。
即按照每一行为一个一维数组,存储第一行后,第二行存储在第一行后面。
2.以列为主序
先按照一维数组存储a[0] [0]到a[n] [0]。然后a[0] [1] 在a[n] [0]后。以此类推。
即按照每一列为一个一维数组,存储第一列后,第二列存储在第一列后面。
2.矩阵的存储
矩阵:由一个m×n个元素排成的m行n的表。一般将矩阵存储为一个二维数组。
当一个矩阵中,值相同的元素很多呈现某种规律分布;零元素很多,则很浪费存储空间。则通过“ 压缩存储 ”来存储一些比较特殊的矩阵。
- 一些比较特殊的矩阵:对称矩阵,对角矩阵,三角矩阵,稀疏矩阵等。(注:矩阵中非零元素较少一般少于5%时,成为稀疏矩阵)
2.1对称矩阵
根据对称矩阵上或下三角中元素数均为n(n+1)/2。可以以行序为主序将元素存放在一个一维数组s[n(n+1)/2]中。
例:以行序为主序存储下三角
通过一维数组来存储下三角矩阵的数据。
如果需要求a[i] [j] 这个数据在一维数组中的位置,i行前面的 i-1行即是1+2+……+(i-1)个元素,并且数组下标从0开始。
所以a[i] [j]在一维数组中的位置为 [ i (i -1) / 2] + j -1。
2.2三角矩阵
2.3对角矩阵
2.4稀疏矩阵(顺序存储)
压缩存储原则:把每个非零元素通过三元组来表示,将其行、列、值存储到三元组中。
注:一般在三元组的小标为0存储稀疏矩阵的总行数、总列数、非零元素总个数。
2.5稀疏矩阵(链式存储)——十字链表
该结点除了行、列、值(row,col,value)以外还有两个域:
-right:用于指向同一行的下一个非零元素。
-down:用来指向同一列的下一个非零元素。
通过行头指针和列头指针来记录某一行或者某一列第一个非零元素。
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏