串、数组、广义表大体了解
我们知道前面学过的__顺序表、链表、栈、队列__,这些都属于线性表。
其中__栈、队列__是操作受限的线性表。
比如:
- 栈,先进后出,只能在栈顶插入和删除数据。
- 队列:先进先出,只能在队尾插入数据,在对头出数据。
而今天要说到的__串、数组、广义表__。
其中__串__是__内容受限的线性表__,也就是说串中的数据元素只能存储字符。
而__数组和广义表__属于线性结构的推广。
1、串
定义:串(String)——零个或多个任意字符组成的有限序列。
1.1、串的相关术语
(1)__字串:__一个串中任意个连续字符组成的子序列(含空串)称为该串的字串。
例如,“abcde"的字串有:”"、“a”、“ab”、“abc”、“abcd”、"abcde"等
(2)__真子串:__是指不包含自身的所有字串。
(3)__字符位置:__字符在序列中的序号为该字符在传中的位置。
(4)__字串位置:__字串第一个字符在主串中的位置。
(5)__空格穿:__由一个或多个空格组成的串。【注意】:与空串不同。
例:字符串a、b、c、d
a = "BEI"
b = "JING"
c = "BEIJING"
d = "BEI JING"
它们的长度是:3 4 7 8
c的字串是:a,b
d的字串是:a,b
a在c中的位置是:1
a在d中的位置是:1
b在c中的位置是:4
b在d中的位置是:5
(6)__串相等:__当且仅当两个串的__长度相等__并且各个对应位置上的字符都相同时,这两个串才相等。
所有的空串都是相等的。
2、串的类型定义、存储结构及其运算
2.1、串的定义
- 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) 串替换
- Strlnsert(&S,pos,T) 字串插入
- StrDelete(&S,pos,len) 字串删除
- DestroyString(&S) 串销毁
2.2、串的存储结构
串中元素逻辑关系与线性表的相同,串可以采用与线性表相同的存储结构
串属于线性表,所以实现方式也有两种:
- 顺序串
- 链串。
实际上还是顺序串用的最多。
2.2.1、串的顺序存储结构
#define MAXLEN 25
typedef struct
{
//在后面研究串的算法时,串的下标从可以1开始,下标为0的可以闲置不用,这样再某些算法便于理解。
char ch[MAXLEN+1]; //存储串的一维数组
int length //串的当前长度
}SString;
2.2.2、串的链式存储结构
这样的方式,优点:操作方便。缺点:存储密度低。
存储密度 = 串值所占的存储/实际分配的存储 = 1/5=0.2
其实我们可以这样:将多个字符存放在一个结点中,以克服其缺点:
结构如下定义:
#define CHUNKSIZE 80 //块的大小由用户自定义
typedef struct Chunk
{
char ch[CHUNKSIZE];
struct Chunk* Next;
}Chunk;
typedef struct
{
Chunk* head; //串的头指针
chunk* tail; //尾指针
int curlen; //字符串的块链结构
}LString;
3、串的模式匹配算法
__算法目的:__确定主串中所含子串(模式串)第一次出现的位置(定位)。
算法应用:
- 搜索引擎
- 拼写检查
- 语言翻译
- 数据压缩
算法种类:
- BF算法(Brute-Force),又称古典的、经典的、朴素的、穷举的。
- KMP算法(特点:速度快)。
3.1、BF算法
3.1.1、串的匹配算法—BF算法核心思想分析
Brute-Force简称为__BF算法__,亦称简单匹配算法。采用穷举法的思路。
BF算法核心思想:从S(主串)的每个字符开始依次与T(子串)的字符进行匹配。
BF算法的核心思路:
- 将主串的第一个字符和模式串的第一个字符进行比较
- 若相等,继续逐个比较后序的字符。
- 若不相等,从主串的下一个字符起,重新与模式串的第一个字符进行比较。
- 直到主串的一个连续子串字符序列与模式串相等,然后返回值为S中与T匹配的子序列的第一个字符序号,即匹配成功。
- 否则,匹配失败,返回值0。
下面我们来具体讲解实现步骤:
假如,现在有两个字符串,一个主串S:a a a a a b,一个子串T: a a a b。
(1)创建两个变量:i、j,用来分别表示主串和子串的下标。需要注意的是这里的i,j是从1开始的。前面提到过,存储数据时,将下标为0的位置闲置,不存放数据,这有利于算法在某些过程的实现。
(2)进行比较:首先将i指针指向主串的第一个元素,将j指针指向子串的第一个元素。然后进行比较,发现i[1] == j[1],那就i++,j++继续逐个匹配。如下图:
(3)继上面i++,j++后,在继续进行比较,发现i[2] == j[2],发现主串和模式串依然匹配,那就继续i++,j++。如下图:
(4)继上面i++,j++后,在继续进行比较,发现i[3] == j[3],发现主串和模式串依然匹配,那就继续i++,j++。如下图:
(5)继上面i++,j++后,在继续进行比较,__重点来了:发现i[4] != j[4],__那此时主串与模式串不匹配了,就像i
和j
进行回溯。具体操作:
i = i-j+2;
j = 1;
这里重点解释变量i的回溯,
i-j+2
可以拆分为(i-j+1)+1
,i-j+1
代表什么意思呢?其实就是比较结束后i向前移动了几格,那就又回退了几格。相当于i回到了原位置。那i-j+1+1
就是表示i
在原位置的基础上又向后移动了一格。那么这样就满足了模式串继续和主串从下一个字符开始比较的条件。通过图来演示下:
回溯算法解释完毕后,我们继续回到原先的话题。
(6)下一轮的比较,这里简化过程,一张图比较完。
会发现,主串和模式串完全匹配。
(7)讨论循环结束条件
现在在进行一轮的比较后,发现模式串和主串比匹配。那回溯,接着让模式串从第一个字符和主串的下一个字符依次逐个向后匹配。如果一个匹配就i++,j++,不匹配,就在回溯。就这样一致循环往复的进行匹配。
那什么时候循环结束呢?
这里有两个条件:i > S.length
和j > T.length
其中j > T.length
是主串与模式串匹配成功时的情况。
而i > S.length
是主串与模式串匹配不成功时的情况。
【总结】:在while循环里面的条件可以写成:i <= S.length
和j <= T.length
。
(8)返回模式串的位置。
上面说过,只需要返回模式串的首字符的下标即可。那怎么表示呢?一行代码搞定:
i - t.length;
//i就是匹配完成后下标的值,此时为6。
//t.length就是模式串的长度,此时是4.
//所以i - t.length = 6 - 4 = 2。
//所里这里模式串匹配成功,且模式串的位置为2。
3.1.2、代码展示
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define MAXLEN 100
typedef struct SString
{
char ch[MAXLEN+1];
int length;
}SString;
int index_BF(SString S, SString T)
{
int i = 1;
int 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;
}
}
int main()
{
int i = 1;
int j = 1;
SString s;
SString t;
while (i<6)
{
scanf("%s", s.ch + i);
i++;
}
while (j<5)
{
scanf("%s", t.ch + j);
j++;
}
s.length = 5;
t.length = 4;
int ret = index_BF(s, t);
printf("%d\n", ret);
return 0;
}
3.1.3、BF算法的扩展:
也许我们会看到上述函数index_bf(SString S,SString T)
多个参数情况:index_bf(SString S,SString T,int pos)
这个函数多了个pos参数,它代表的含义是模式串直接从主串中第pos下标个字符开始匹配。而上面不带pos参数的情况是模式串直接从主串的第一个字符开始比较。那代码如何改变呢?很简单,之需要修改i变量的值即可。
如下:
int i = pos; //只需要修改i的值即可。
int j = 1;
3.1.4、BF算法的时间复杂度:O(n*m)
假设主串长度n,模式串长度为m。
最好的情况:从主串的第一个字符就开匹配,而且完全匹配成功。这个时候只需要比较模式串的长度即可。这时的时间复杂度为:O(M)。
最快的情况:主串前面n-m个位置都部分匹配成功到子串的最后意味,即着n-m位各比较了m次。
并且最后m位也各比较了一次。
所以总次数:(n-m)*m + m = (n-m+1)*m,若m<<n,则此时的时间复杂度位:O(n*m)。
可以看到BF的算法效率还是不太好的,那下面学习KMP算法的时间复杂度位O(n+m)。就能大大优化算法效率。
3.2、KMP算法
KMP算法是D.E.__K__nuth、J.H.__M__orris和V.R.__P__ratt共同提出的,简称KMP算法。
该算法较BF算法有较大改进,从而使算法效率有了某种程度的提高。
为什么说KMP算法效率比BF算法效率高呢?这是因为KMP的算法设计不同于BF。
3.2.1、KMP算法设计思想
利用已经部分匹配的结果而加快模式串的滑动速度。
同时有两个很重要的点:
- 主串S的指针
i
不必回溯!可提速到O(n+m)。- 并且模式串的指针
j
也不是必须回溯到模式串的首字符处,具体的位置需要因情况而讨论,但是有一点就是指针j
回溯的越少(也可以说成j
滑动的越远)是越好的。
【重点】:其实KMP算法的核心就是讨论模式串中每个字符与主串进行匹配时,变量j
回溯的位置。(个人理解)。
3.2.2、前缀与后缀
比如现在有各模式串:aabaaf
。
__前缀:__包含首字母,不包含尾字母的所有子串。
所以该模式串前缀有:
- a
- aa
- aab
- aaba
- aabaa
【注意】:aabaaf不是前缀,因为不能包含f尾字母。
__后缀:__包含尾字母,不包含首字母的所有子串。
所以该模式串前缀有:
- f
- af
- aaf
- baaf
- abaaf
【注意】:aabaaf不是后缀,因为不能包含a首字母。
3.2.3、最长公共前后缀
这里还以模式串:aabaaf
为例。
- a,a即是首字母又是为字母,所以a既没有前缀,也没有后缀。所以a的最长公共前后缀为0。
- aa,前缀为a,后缀为a。所以aa的最长公共前后缀为1。
- aab,前缀为a,后缀为b,前后缀不相等,所以aab的最长公共前后缀为0。
- aaba,前缀为a,后缀为a。所以aaba的最长公共前后缀为1。
- aabaa,前缀为aa,后缀为aa。所以aabaa的最长公共前后缀为2。
- aabaaf,前缀为a,后缀为f,前后缀不相等。所以aabaaf的最长公共前后缀为0。
【重点】:这里强调是__最长__公共前后缀,也就是说如果模式串中有多对前后缀,那一定要取最长得那一对公共前后缀。
比如有一个子串:ababa
最长公共前后缀是:aba
,所以最长公共前后缀的长度为3。
3.2.4、KMP算法分析
(1)从不匹配处开始找最长公共前后缀
现有主串和模式串,如下图:
注意看:现在主串和模式串在箭头处开始不匹配了。
并且可以看到i
和j
的位置如上图。
要是按照BF算法,i
会回溯到某个位置,j
会回溯到模式串的首字符处。
但是现在使用KMP算法。i
不改变,就还在此位置。而最最最重要的就是需要确定j
到底回溯到模式串的那个位置。
那下面就需要来找最长公共前后缀。
那怎么找呢?从模式串首字符到模式串和主串不匹配的前一个字符。
如下图:需要在子串A B B A B
中确定最长公共前后缀。
那可以发现:前缀是AB,后缀是AB。
(2)根据最长公共前后缀,确定指针j
回溯到模式串的位置
最长公共前后缀找到后,据可以确定j
回溯的位置了。那如何确定的指针j
回溯的位置呢?
这里直接记着结论就完事了,(一共两个结论,两个结论都是确定指针j
回溯的位置。记着任意一个结论即可。)
结论一:直接移动模式串,使原来的前缀移动到后缀的位置。所以最终j
指针指向了A B B A B
子串中第三个字符B处,如下图:
结论二:首先找到后缀,然后再找到与后缀相等的前缀,最后将j
指向前缀后面的字符。也就是A B B A B
子串中第三个字符B处,如下图:
由上图可以看出,指针j
回溯到模式串的黄色箭头位置,并且j
没有回溯到模式串得首字符位置。
(3)分析模式串中每个字符都不匹配的时候,指针j
分别需要回溯的位置
在经过上面找最长公共前后缀
和根据最长公共前后缀确定指针j回溯的位置
这两个步骤之后,我们就可以知道模式串再和主串进行某一个字符匹配时的具体操作。
其实我们在找最长公共前后缀
和根据最长公共前后缀确定指针j回溯的位置
时,发现没有关于主串的操作。所以我们只需要关注模式串进行操作即可。
那下面我们就只研究模式串即可,把模式串的相关信息挖掘出来之后,用此信息就可以和任何主串进行匹配了。
那下面我们以下面的数组为例:
开始分析模式串中各个字符不匹配的时候,下一步要做什么操作:
-
第一号字符不匹配下一步需要做的操作:1号位与主串下一位比较。
-
第二号字符不匹配下一步需要做的操作:1号位与主串当前位比较。
发现不匹配,就开始找指针左边的子串的最长前后缀,发现最长前后缀长度为0,那根据一个__结论:移动后指针左边子串长度就是最长公共前后缀的长度。__由于现在最长公共前后缀长度为0,所以对应的移动后指针
j
左边的自创长度也要为0,所以最终指针j
回溯到了,如下图位置:
-
第三号字符不匹配下一步需要做的操作:1号位与主串当前位比较。
具体原因和第二号字符不匹配的一样。
-
第四号字符不匹配下一步需要做的操作:2号位与主串当前位比较。
此时指针
j
指向4号位字符,不匹配。那先找最长公共前后缀,发现最长公共前后缀高度为1。如下图:将__前缀移动到后缀位置__,就是指针
j
回溯的位置,如下图:
那下面的第5、6、7、8、9号都可以这样分析。
下面我们直接总结结论:
【重点】:最长公共前后缀的长度为n,那就是n+1号位与主串当前位比较。
【注意】:如果数组的起始位置是从下标位0开始的,最长公共前后缀的长度为n,那就是n号位与主串当前位比较。
当前这里还需要个特殊情况进行处理:模式串中的第一个字符需要单独处理,那我们把第一个字符匹配时处理结构标记为0,当前出现0时,就代表需要做__1号位与主串下一位比较__操作了。
那这样就解决了模式串中每个字符不匹配时的情况。如下图:
3.2.5、next[j]
在上面我们得出了每个字符不匹配时,所要执行的操作。那可以转为相应的前缀表,存放在next[j]数组里面如下:
next[j]数组里面存放的就是模式串中每个字符不匹配时下一步需要做的操作。也就是指针j
需要回溯的位置。
再练习一组:
3.2.6、代码展示
KMP算法代码:
int index_KMP(SString S, SString T)
{
int i = 1;
int j = 1;
while (i <= S.length && j <= T.length)
{
if (S.ch[i] == T.ch[j])
{
i++;
j++;
}
else
{
//i不变,j后退
j = next[j];
}
}
if (j >= T.length)
{
return i - T.length;
}
else
{
return 0;
}
}
next[j]算法代码:
4、数组
数组:按一定格式排列起来的,具有相同类型的数据元素的集合。
一维数组:若线性表中的数据元素为非结构的简单元素。
一维数组的逻辑结构:线性结构,定长的线性表。
__二维数组:__若一维数组中的数据元素又是一维数组结构,则称为二维数组。
二维数组的逻辑结构:
-
可以看作是线性结构:每个数据元素即在一个行表中,又在一个列表中。
说二维数组不是线性表的原因时:一个元素在行中有一个前驱和一个后继,同时一个元素在它所在的列中也有一个前驱和一个后继,如下图:
-
线性结构定长的线性表:该线性表的每个数据元素也是一个定长的线性表。这个就是把二维数组中的每一行都看作是二维数组的元素。这样就可以看作为线性表。扩展的线性表。
在C语言中,一个二维数组类型也可以定义为一维数组:
typedef elemtype array2[m][n];
//等价于
typedef elemtype array1[n];
typedef array1 arrar2[m]; //这个意思就是:array2中有m个元素,每个元素时array1类型的元素,而array1又是一个有n个元素的数组。
三维数组:若二维数组中的元素又是一个一维数组,则称作为三维数组。
…
n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组。
结论:线性表结构时数组结构的一个特例,而数组结构又是线性表结构的扩展。
数组特点:结构固定—定义后,维数和维界不再改变。
数组的基本操作:
- 初始化数组
- 销毁外数组
- 取数组元素值
- 给数组元素赋值
4.1、数组的顺序存储
数组的基本操作:初始化,销毁,取元素,修改元素。
所以一般采用顺序结构来表示数组。
注意:数组可以是多维的,但存储结构元素的内存单元地址是唯一的,因此在存储数组结构之前,需要解决将多维关系映射到一维关系的问题。
一维数组:
例,有数组定义:inta[5];
每个元素占用4字节,假设a[0]存储在2000单元,a[3]地址是多少?
答案:a+i*L—>2000+4*3。
二维数组:
二维数组有两种存储方式:
- 以行序维主序(如下图:)
- 以列序为主序(如下图:)
二维数组的元素地址存放如下图:
存储单元是一维结构,而数组是个多维结构,则用一组连续存储单元存放数组元素就有个次序约定问题。
那如何计算二维数组中某个元素的地址呢?如下。
__以行序为主序:__设数组开始存储位置LOC(0,0),存储每个元素需要L个存储单元(占用几个字节)数组元素a[i][j]的存储位置是:LOC(i,j) = LOC(0,0) + (n*i+j)*L,其中(n*i+j)表示在a[i][j]前面所有元素个数。
那以列序为主序的计算过程也同理。
三维数组:
按页/行/列存放,页优先的顺序存储
当需要计算某个元素的地址时,我们先统计有多少页,然后页中有多少行,行中有多少了元素。
下标i1,i2,i3的数组元素的存储位置,计算公式:
n维数组:
下标为i1,i2,i3,…in的数组元素的存储位置:
4.2、特殊矩阵的压缩存储
矩阵:一个有m*n个元素排成的m行n列的表。
矩阵的__常规__存储:将矩阵描述为一个二维数组。
矩阵的__压缩__存储:为多个相同的非零元素只分配一个存储空间;对零元素不分配空间。
__矩阵的常规存储的特点:__可以对其元素进行随机存取;矩阵运算非常简单;存储的密度为1。
不适宜常规存储的矩阵:值相同的元素很多且呈某种规律分布;零元素多。
1、什么是压缩存储?
若多个数据元素的__值都相同__,则只分配一个元素值得存储空间,且零元素不占存储空间。
2、什么样的矩阵能够压缩?
一些特殊的矩阵,如:对称矩阵,对角矩阵,三角矩阵,稀疏矩阵等。
3、什么叫稀疏矩阵?
矩阵中非零元素的个数比较少(一般小于5%)。
4.2.1、对称矩阵压缩存储
【特点】:在n*n的矩阵a中,满足如下性质:
aij = aji(1<=i,j<=n)这说明矩阵中元素沿主对角线时对称的,如下图:
【注意】:这里有个非常重要的细节,我们这里的二维数组首元素的下标是从a[1][1]开始的,且一维数组下标从0开始。 这个一定要切记。否则答案会不一样的。
【存储方法】:只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1) / 2个元素空间。
下面存储下三角的数据元素,不在存储到二维数组中了,而是存储在一维数组中,如果存储在一维数组中,那我们怎么找到aij这个元素呢?
确定aij这个元素的下标,只需要统计aij前面元素个数即可。
那如何统计aij前面的元素个数呢?如下算法:
先统计i行前面有多少元素:1+2+3+…+i-1 = i*(i-1) / 2。
然后再统计j前面有多少元素:j-1个元素。
那相加这两部分:i*(i-1) / 2 + j-1。
所以aij的下标为:i*(i-1) / 2 + j-1。
先测试,求an1的下标。
直接套公式得:n*(n-1) / 2 。
所以an1得下标为:n*(n-1) / 2 。
4.2.2、三角矩阵压缩存储
1、对角矩阵
【特点】:对角线以下(或者以上)的数据元素(不包含对角线)全部为常数c。
【切记】:这里的二位在数组首元素下标是从a[1][1]开始的,不是从a[0][0]开始的。否则答案不同。
1.1、下三角矩阵存储(二维数组首元素下标是从a[1][1]开始的,且一维数组下标从0开始):
- i*(i-1) / 2 + j-1 i>=j时
- n * (n+1) / 2 i<j时 【注意】:这里i>j的情况其实就在看上三角的情况。
1.2、上三角矩阵存储(二维数组首元素下标是从a[1][1]开始的,且一维数组下标从0开始)
- (2n-i+2)(i-1) / 2 + (j-i) i<=j
- n * (n+1) / 2 i>j时 【注意】:这里i>j的情况其实就在看下三角的情况。
这里推导一下,i<=j的情况。
如果想要确定a[i][j]的元素位置,首先1~i-1行有多少了元素。
第一行有n个元素,第二行有n-1个元素,第三行有n-2个元素…,那第i-1有n-(i-1-1)个元素。
那n+(n-1)+(n-2)+(n-3)+…+(n-i+2) = (2n-i+2)(i-1) / 2。
然后再算i行中a[i][j]前有多少个元素,找规律发现有:(j-i)个。
综上,两个结果就是:(2n-i+2)(i-1) / 2 + (j-i)。
所以a[i][j]再一维数组中的下标就是(2n-i+2)(i-1) / 2 + (j-i)。
4.2.3、对角矩阵(带状矩阵)压缩存储
【特点】:在n*n的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值全为0,则称为__对角矩阵__。常见的有三对角矩阵、五对角矩阵、七对角矩阵等。
【存储方法】:这里可以转为二维数组的方式进行存储,如下图:
但是这里也可以使用一维数组进行存储,我们依下图的对焦矩阵为例,注意:对角矩阵的首元素下标从a[1][1]开始,并且一维数组的下标从0开始。如下图:
分析:第一行有2个元素,第n行有2个元素。其余都有3个元素。
如果求a[i][j]元素的在一维数组中的下标。
首先需要记录第i-1行的元素个数,那就是第一行+第2~第i-1行的元素个数,那就是2+(i-1-1)*3 = 2+(i-2)*3,
然后我们在算第i行中a[i][j]前面有多少个元素。直接写结论:-(i-j)+1。
那么两个结果相加就是:2+(i-2)*3 - (i-j)+1 = 2i+j-3。
所以对角矩阵中元素在一维数组中存储的位置为: 2i+j-3
4.3、稀疏矩阵
__稀疏矩阵:__设在m*n的矩阵中有t个非零元素。令q=t/(m*n),当q<=0.05时为稀疏矩阵。
如下有个6*7的矩阵:
我们可以算算它属于不属于稀疏矩阵,一共8个非零元素,所以t=8,m*n = 6*7 = 42,8/42大概为20%。
所以此矩阵不属于稀疏矩阵。
4.3.1、数组存储法
那为了节省空间可以怎么存储呢?可以使用三元组的方法进行存储。三元组(i,j,a[i][j]),i是此元素的行坐标,j是此元素的列坐标,a[i][j]是此元素的值。
那么以上8个非零元素就可以使用三元组表示为:
{(1,2,12),(1,3,9),(3,1,-1),(3,6,14),(4,3,24),(5,2,18),(6,1,15),(6,4,-7)}
并且还需要一个三元组来存储此矩阵维数和总非零元素个数:(6,7,8)。
以下面表示存储:
i | j | value |
---|---|---|
6 | 7 | 8 |
1 | 2 | 12 |
1 | 3 | 9 |
3 | 1 | -1 |
3 | 6 | 14 |
4 | 3 | 24 |
5 | 2 | 18 |
6 | 1 | 15 |
6 | 4 | -7 |
那这样我们只使用了3*9=27个空间,比起前面使用了42个空间,还是节约不少呢。
那同样的道理,我们看到上面的表格,也可以反推出矩阵。
三元组顺序表又称__有序的双下标法__。
三元组顺序表的优点:非零点在表中按行序有序存储,因此便于__进行以行顺序处理的矩阵运算。__
4.3.2、十字链表存储法
三元组顺序表的缺点:不能随机存取。若按行号存取某一行中的非零元素,则需从头开始进行查找。
因此我们使用十字链表法进行存储。
十字链表法的优点:它能够灵活地插入和删除因运算而产生的新的非零元素,实现矩阵的各种运算。
在十字链表中,矩阵的每一个非零元素用一个结点表示。
该结点一共有5个域:
- row:用来存储该元素的行。
- col:用来存储该元素的列。
- value:用来存储该元素的值。
- right:用于链接同一行中的下一个非零元素。
- down:用于链接同一列中的下一个非零元素。
十字链表中结点的结构示意图:
小试牛刀:
元素3的链表结构:
那如果只是上面的链式结构,我们依然不好访问元素。还需要每一行的头指针和每一列的头指针,如下:
我们再来测试一下:
5、广义表
广义表是线性表的推广。
__广义表(又称列表Lists):__是n>=0个元素a0,a1,a2,…,an-1的有限序列,其中每一个ai或者是原子,或者是一个广义表。
广义表前半句定义和线性表几乎一样,线性表中也是包含n个元素。但二者不同的是线性表中存放的n个元素都是同样类型的单一元素,而广义表中的n个元素既可以是同样类型且单一的元素,也可以又是一个广义表。(这个表是原来广义表的子表)
5.1、广义表的概念
广义表通常记作:LS={a1,a2,a3,…,an}
其中:LS为表名,n为表的长度,每一个ai为表的元素。
习惯上,一般用大写字母表示广义表,小写字母表示原子。
__表头:__若LS非空(n>=1),则其第一个元素a1就是表头。记作head(LS) = a1。
【注】:表头可以是原子,也可以是子表。
__表尾:__除表头之外的__其它元素__组成的表。记作:tail(LS) = (a2,…,an)。
【注】:表尾不是最后一个元素,而是一个子表。
光看概念,难理解,下面来看几个例子:
- A=() 空表,长度为0。
- B=(()) 广义表中有个空表,所以广义表长度为1,表头、表尾为空。
- C=(a,(b,c)) 长度为2,由原子a和子表(b,c)构成。所以表头为a;表尾为((b,c))。
- D=(x,y,z) 长度为3,每一项都是原子。表头为x;表尾为(y,z)。
- E=(C,D) 长度为2,每一项都是子表。表头为C,表尾为(D)。
- F=(a,F) 长度为2,第一项为原子,第二项为它本身。表头为a;表尾为(F)。
5.2、广义表性质
(1)广义表中的数据元素有相对次序,一个直接前驱和一个直接后继。
(2)广义表的长度定义为最外层所包含元素的个数。
如:C=(a,(b,c))是长度为2的广义表。
(3)广义表的深度定义为该广义表展开所包含括号的重数;
如:A=(b,c)的深度为1,B=((b,c),d)的深度为2,C=(f,((b,c),d),h)的深度为3。
【注】:原子的深度为0,空表的深度为1。
(4)广义表可以为其它广义表共享;如:广义表B就共享表A。在B中不比列出A的值,而是通过名称来引用,
如:B=(A)。
(5)广义表可以是一个递归的表,如:F=(a,F=(a,(a,(a,…))))。
【注】:递归表的深度是无穷值,长度是有限值。
(6)广义表是多层次结构,广义表的元素可以是单元素,也可以是子表,而子表的元素还可以是子表。
可以用图形象的表示。
例:D=(E,F),其中E=(a,(b,c)),F=(d,(e))。
可以用图形象的表示为:
5.3、广义表和线性表的区别
广义表可以看成是线性表的推广,线性表是广义表的特列。
广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、数和有向图等各种常用的数据结构。
当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。
另外,树和有向图也可以广义表来表示。
由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因 此在计算机地许多领域都有成功使用广义表的示例。
5.4、广义表的基本运算
(1)求表头GetHead(L):非空广义表的第一个元素,可以是一个原子也可以是一个子表。
(2)求表尾GetTail(L):非空广义表除去表头元素外其它元素所构成的表。表尾一定是一个表。
例:
D=(E,F)=((a,(b,c)),F):
GetHead(D)=E;GetTail(D)=(F)。
GetHead(E)=a;GetTail(E)=((b,c))。
GetHead(((b,c)))=(b,c);GetTail(((b,c)))=()。
GetHead((b,c))=b;GetTail((b,c))=(c)。
GetHead((c))=c;GetTail((c))=()。
5.5、广义表如何存储?
广义表一般用链表来存储。
为什么不用数组来存储广义表呢?我们知道数组的特点是:每一个元素占用连续的空间且存储相同类型的元素(占用空间大小也相同)。而广义表中每个元素有可能是原子,有可能是子表,显然这不符合数组的特性。所以广义表适合用链表来存储。