510 likes | 652 Views
第四章 数组、串和广义表. 学习要点:. 数组(二维数组)基本概念与存储方式,基于某些特殊矩阵的典型算法。 串的基本概念和基本操作,串的存储方式和典型算法。 广义表基本概念和相关概念,广义表的存储与基本操作实现。. § 4.1 数组. 数组( array ) 表示 n ( n>1 )个具有相同数据类型的数据元素的有序集合。. 4.1.1 二维数组. 数组中数据元素由 一个数据值 和 一个(组)下标 确定。下标是数组中元素相互区分的标识。使用一个下标区分其中元素的数组称为一维数组。使用两个以上下标的数组称为多维数组。常用的多维数组是二维数组,通常称为矩阵。
E N D
学习要点: • 数组(二维数组)基本概念与存储方式,基于某些特殊矩阵的典型算法。 • 串的基本概念和基本操作,串的存储方式和典型算法。 • 广义表基本概念和相关概念,广义表的存储与基本操作实现。
§4.1 数组 数组(array)表示n(n>1)个具有相同数据类型的数据元素的有序集合。
4.1.1二维数组 • 数组中数据元素由一个数据值和一个(组)下标确定。下标是数组中元素相互区分的标识。使用一个下标区分其中元素的数组称为一维数组。使用两个以上下标的数组称为多维数组。常用的多维数组是二维数组,通常称为矩阵。 • A3*4的阵列形式表示:
4.1.2 矩阵的顺序表示与实现 • 行序优先存储: Loc(aij) = Loc(a00) +(i*n+j)* length • 列序优先存储: Loc(aij) = Loc(a00) +(j*m+i)* length
4.1.3 特殊矩阵压缩存储 1.对称矩阵 • n阶方阵,沿主对角线对称位置上的元素相等,即aij=aji。 • 主对角线:由元素aii组成的数列 如右图的对称矩阵按行序存储下三角的元素,映射到一维数组中去,其对应的一维数组下标如下图所示:
4.1.3 特殊矩阵压缩存储 1.对称矩阵2 • 对称矩阵中任意元素与其对应的一维数组下标k的计算公式:
4.1.3 特殊矩阵压缩存储2 2.三角矩阵 • n阶方阵,沿主对角线以上的元素全为零。 • 上三角矩阵、下三角矩阵 如右图为下三角矩阵。 按行序压缩存储下三角矩阵,映射到一维数组中去,其对应的一维数组下标如下图所示:
4.1.3 特殊矩阵压缩存储2 2.三角矩阵2 • 按行序压缩存储下三角矩阵,元素下标(i,j)与对应的一维数组下标k映射公式为: • k=(i+1)*i/2+j (i>=j)
4.1.3 特殊矩阵压缩存储3 3.对角矩阵 • n阶方阵,沿主对角线上下若干对角线上有非零元素,其他全为零。 • 带宽:主对角线上下各d条对角线上有非零元,则共有2d+1条对角线有非零元,称为带宽。 如右图为3对角矩阵。 按行序压缩存储3对角矩阵,映射到一维数组中去,其对应的一维数组下标如下图所示:
4.1.3 特殊矩阵压缩存储3 3.对角矩阵2 • 按行序压缩存储三对角矩阵,元素下标(i,j)与对应的一维数组下标k映射公式为: • k=3*i-1+(j-i+1)=2*i+j
4.1.4 稀疏矩阵压缩存储 • 稀疏矩阵是指矩阵中的非零元很少,大部分是零元素,且非零元的分布没有规律。如下矩阵:
4.1.4 稀疏矩阵压缩存储 1.三元组表表示法 • 在存储稀疏矩阵中的非零元时,为了描述一个非零元素,需要元素本身的值和行标、列标三方面的信息,这些信息的组合就称为三元组。
1.三元组表表示法2 • 用C语言定义三元组表的结构类型如下: 00 typedef int DataType; /* 假设稀疏矩阵的元素类型为整型 */ 01 #define MAXSIZE 100 /* 非零元个数的最大值 */ 02 typedef struct /* 定义三元组结点类型 */ 03 { 04 int row,col; /* 非零元所在的行下标,列下标 */ 05 DataType value; /* 非零元素值 */ 06 }Triple; 07 typedef struct /* 定义三元组表结构类型SMatrix */ 08 { 09 Triple data[MAXSIZE+1]; /* 非零元组成的三元组表 */ 10 int mu,nu,tu; /* 稀疏矩阵的行数、列数和非零元个数 */ 11 }SMatrix;
1.三元组表表示法3 (1)建立稀疏矩阵的三元组表1 算法4-1 建立并输出稀疏矩阵的三元组表 00 SMatrix CreateSMatrix() /* 创建稀疏矩阵M */ 01 { 02 SMatrix M; 03 int i; 04 printf("\n请输入矩阵的行数:"); 05 scanf("%d",&M.mu); 06 printf("\n请输入矩阵的列数:"); 07 scanf("%d",&M.nu); 08 printf("\n请输入矩阵的非零元素个数:"); 09 scanf("%d",&M.tu); 10 for(i = 0; i < M.tu; i++) 11 { 12 printf("请按行序顺序输入第%d个非零元素所在的行(0~%d)," "列(0~%d),元素值:(逗号分隔)\n", i,M.mu-1,M.nu-1); 13 scanf("%d,%d,%d",&M.data[i].row,&M.data[i].col,&M.data[i].value); 14 } 15 return M; 16 }
1.三元组表表示法3 (1)建立稀疏矩阵的三元组表2 算法分析: • 建立并输出稀疏矩阵的三元组表,首先要给出三元组表的类型定义,然后编写建立函数和输出函数,最后在主程序中调用,见算法4-1。程序第00-16行为建立函数,程序先定义了三元组变量M,再通过键盘输入稀疏矩阵的信息:行数、列数、非零元个数,由程序第04-09行完成。程序第10行的for语句循环M.tu次,输入每个非零元的信息,填入到相应的三元组的对应域中,即完成三元组表的建立。 • 输出函数(程序第17-24行)则按每行输出一个三元组的形式输出三元组的信息。 • 主程序(程序第25-30行)通过调用建立函数和输出函数完成三元组表的建立于输出。
1.三元组表表示法4 (2)稀疏矩阵的转置 第一种方法: 假设稀疏矩阵M有n列,转置后的稀疏矩阵为T。需要对M的三元组表进行n趟扫描,第i(0≤i≤n-1)趟扫描将M中所有col值为i的三元组存放到T的三元组表中,具体方法是:取出该三元组,交换row值和col值,连同value值,作为新的三元组存放到T的三元组表中(程序第13-15行)。这样就将M中的第i列的非零元转置到了T中的第i行,并且能保证T的三元组表是按行序存放。
算法4-2 稀疏矩阵的转置算法 00 SMatrix TransMatrix(SMatrix M) /* 稀疏矩阵M的转置算法 */ 01 { 02 SMatrix T; 03 int i,j,k; 04 T.mu=M.nu; /* 稀疏矩阵M的列数作为T的行数 */ 05 T.nu=M.mu; /* 稀疏矩阵M的行数作为T的列数 */ 06 T.tu=M.tu; /* 稀疏矩阵M的非零元个数作为T的非零元个数 */ 07 if(T.tu>0) 08 { 09 k=0; 10 for(i=0;i<M.nu;i++) /* M有nu列,要扫描nu趟 */ 11 for(j=0;j<M.tu;j++) /* 对M中的每个三元组扫描 */ 12 if(M.data[j].col==i) 13 { 14 T.data[k].row=M.data[j].col; 15 T.data[k].col=M.data[j].row; 16 T.data[k].value=M.data[j].value; 17 k++; 18 } 19 } 20 return T; 21 }
1.三元组表表示法4 (2)稀疏矩阵的转置2 第二种方法:快速转置 引入两变量:一个变量存放转换后的稀疏矩阵T每行的非零元素个数,可定义为rowsize[ ],变量长度为T的行数。 另外一个变量存放T中每行的第一个非零元在三元组表中的位置,可定义为rowstart[ ],长度也为T的行数。具体公式为: 在两个辅助变量计算出来后,可对三元组表进行快速转置,进行一趟扫描即可。具体做法是:逐个扫描M的三元组表中三元组的列值j(程序第31行),以此列值为下标读取rowstart[j]中的元素值(程序第32行),即为该三元组转置后在T的三元组表中的位置,将此三元组存放到该位置后,rowstart[j]值加1(程序第36行),表示同一行的下一个非零元素三元组的位置。
算法4-3 稀疏矩阵的快速转置算法1 00 SMatrix FastTransMatrix(SMatrix M) /* 稀疏矩阵M的快速转置算法 */ 01 { 02 SMatrix T; 03 int rowsize[MAXSIZE]; 04 int rowstart[MAXSIZE]; 05 int i,j,k; 06 T.mu=M.nu; /* 稀疏矩阵M的列数作为T的行数 */ 07 T.nu=M.mu; /* 稀疏矩阵M的行数作为T的列数 */ 08 T.tu=M.tu; /* 稀疏矩阵M的非零元个数作为T的非零元个数 */ 09 if(T.tu>0) 10 { 11 for(j=0;j<M.nu;j++) /* 设rowsize的初值全为0 */ 12 rowsize[j]=0; 13 for(i=0;i<M.tu;i++) /* 求M中每一列非零元素个数 */ 14 { 15 j=M.data[i].col; 16 rowsize[j]=rowsize[j]+1; 17 } 18 printf("\nrowsize[]:"); 19 for(i=0;i<M.nu;i++) /* 打印输出rowsize变量 */ 20 printf("%d ",rowsize[i]); 21 printf("\n");
算法4-3 稀疏矩阵的快速转置算法2 22 rowstart[0]=0; 23 for(j=1;j<M.nu;j++) /* 求第j列中第一个非零元在T中的位置 */ 24 rowstart[j]=rowstart[j-1]+rowsize[j-1]; 25 printf("\nrowstart[]:"); 26 for(i=0;i<M.nu;i++) /* 打印输出rowstart变量 */ 27 printf("%d ",rowstart[i]); 28 printf("\n"); 29 for(i=0;i<M.tu;i++) 30 { 31 j=M.data[i].col; /* 取得三元组的列值 */ 32 k=rowstart[j]; /* 取得三元组在T中的位置 */ 33 T.data[k].row=M.data[i].col; 34 T.data[k].col=M.data[i].row; 35 T.data[k].value=M.data[i].value; 36 rowstart[j]=rowstart[j]+1; 37 } 38 } 39 return T; 40 }
4.1.4 稀疏矩阵压缩存储2 2.十字链表法 • 当矩阵的非零元个数和位置在操作过程中变化较大时,就不宜采用三元组表来表示,对此种类型矩阵,采用链式存储表示三元组的线性表更为合适。 • 为了使用链表存储稀疏矩阵,需要分别建立非零元素结点、行/列链表头结点和整个链表头结点。
2.十字链表法2 非零元素结点C语言定义如下: 00 typedef int DataType; /* 假设稀疏矩阵的元素类型为整型 */ 01 struct node /* 定义十字链表结点结构 */ 02 { 03 int row,col; /* 非零元所在的行下标,列下标 */ 04 DataType value; /* 非零元素值 */ 05 struct node *down; /* 定义down域指向同一列中下一个非零元地址 */ 06 struct node *right; /* 定义right域指向同一行中下一个非零元地址 */ 07 }; 08 typedef struct node TripleNode; /* 定义十字链表结点类型 */
2.十字链表法3 十字链表头结点C语言定义如下: 00 struct node1 /* 定义十字链表结点结构 */ 01 { 02 int mu,nu,tu; /* 稀疏矩阵的行数、列数和非零元个数 */ 03 TripleNode *head; /* 定义head域指向行列表头结点向量 */ 04 }; 05 typedef struct node1 Triple; /* 定义十字链表类型 */ 建立稀疏矩阵的十字链表程序见4.1.3节算法4-4
2.十字链表法4 一个十字链表实例:
§4.2 串 4.2.1 串及相关概念 串(string):是由零个或多个字符组成的有限序列。 S=‘a0a1a2…an-1’ (n≥ 0) 串名:大写字母S为串名 串值:引号引起来的字符序列 长度:n表示串值中的字符个数,称为长度 空串:当n=0时,称为空串 空格串:由一个或多个空格组成的串 子串与主串:串中任意连续的字符组成的子序列称为该串的子串。包含子串的串相应地称为主串。 位置:字符在字符串中首次出现的位置 相等:两个串的长度相等且对应位置上的字符都一一相等
4.2.2 串的基本操作 ADT String is {数据对象:D={ai|ai∈CharacterSet,i=0,1,2,…,n-1,n≥0} 数据关系:R={<ai-1,ai>|ai-1,ai∈D,i=1,2,…,n-1} 基本操作: (1)StrAssign(String T, String chars) 把chars赋为T的值,chars是字符串常量。 (2)StrCopy(String T, String S) 将串S复制到串T中。 (3)StrEmpty(String S) 判断串S是否为空串,并返回TRUE或FALSE的标志。 (4)StrCompare(String S, String T) 判断两个字符串是否相等,若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。 (5)int StrLength(String S) 返回字符串S的元素个数,称为串的长度。 …… } ADT String • 串的抽象数据类型定义如下:
4.2.3 串的存储结构 1.串的定长顺序存储结构 • 串的定长顺序存储表示,也称为静态存储分配的顺序串。它是用一组地址连续的存储单元存储串值的字符序列。所谓定长顺序存储结构,是直接使用定长的字符数组来定义。 • 用C语言定义串的结构类型如下: typedef struct {char data[MAXSTRLEN]; /* 定义data域为定长的顺序存储结构 */ int len; /* 定义len域为串的长度 */ } String; /* 定义串的结构类型为String */ • 串的定长顺序存储结构,优点是可以实现直接存取,定位方便,可以很方便地求取子串;缺点是插入、删除、置换等操作困难,需要移动大量的字符。
4.2.3 串的存储结构2 2.串的链式存储结构 • 串的单链表存储表示 • 结点大小为4的块链存储表示 最后结点不满可以用“#”号补上; 也可设置尾指针,方便联接操作,注意“#”号的处理。
串值所占存储空间 存储密度 = 实际分配的存储空间 2、串的链式存储结构2 重点:设定结点的大小,小则操作方便,但系统 开销大;大节省空间,操作不方便。
4.2.4 串的模式匹配 串的模式匹配是一种重要的串运算,即子串定位操作,具体含义是:若主串S中存在和串T值相同的子串, 则返回它在主串S中第一次出现首字母的位置; 否则函数值为-1。 1. BF算法 • 基本思想: 假设主串S=’s0s1s2…sn-1’,模式串T=’t0t1t2…tm-1’,n为S的长度,m为T的长度。从主串S的第一个字符开始和模式串T的第一个字符进行比较,若相等,则继续比较两者的后续字符;否则,从主串S的第二个字符开始和模式T的第一个字符进行匹配。重复上述过程,若T中的字符全部匹配完毕,则说明本趟匹配成功,返回本趟匹配S的起始位置;否则,匹配失败,返回-1。
设主串S=’abacababaabcacadcaa’,模式串T=’abaabcac’,BF算法匹配过程如下图所示:设主串S=’abacababaabcacadcaa’,模式串T=’abaabcac’,BF算法匹配过程如下图所示:
1. BF算法2 BF算法虽然简单但是效率较低,时间复杂度为O(n×m)。造成BF算法效率低的原因是回溯,即在某趟匹配i个字符失败后,对于主串S要回溯到本趟匹配开始字符的下一个字符,模式串T要回溯到第一个字符,没有利用前i-1个字符时的部分匹配结果。 BF算法详见教材4.2.4节的算法4-5。
4.2.4 串的模式匹配2 2. KMP算法 • 基本思想: 该算法主要消除了主串指针(i指针)的回溯,利用已经得到的部分匹配结果将模式串右滑尽可能远的一段距离再继续比较,从而使算法效率有某种程度的提高,可使效率提高到O(n+m)的水平。 KMP算法当某趟匹配不成功时,即Si≠Tj时,主串S下标i不回溯,模式串T下标j也不回到0位置,而是回到一个恰当的位置,所以在主串下标i不回溯的前提下,下标j回到哪个位置是问题的关键。 假设回到位置j,代表的意思是在主串S位置i之前(j-1)位必定与模式串T的前(j-1)位一一匹配。
设主串S=’abacababaabcacadcaa’,模式串T=’abaabcac’,T中next函数值如下表所示:设主串S=’abacababaabcacadcaa’,模式串T=’abaabcac’,T中next函数值如下表所示:
2. KMP算法2 • next函数的值完全只与模式串有关而与主串的值没有任何关系,因此对于每个模式串来说都有一个唯一的next数组值。 • next数组的计算公式为: • Index_KMP函数中,求next数组的GetNext函数时间复杂度为O(m)接下来不回溯的匹配过程时间复杂度为O(n),所以,KMP算法总的时间复杂度为O(m+n)。 KMP算法详见教材4.2.4节的算法4-6。
§4.3 广义表 4.3.1 广义表基本概念 1.广义表及相关概念 广义表(Generalized Lists)是由n(n≥0)个数据元素组成的有序序列,一般记为: Lists=(a0,a1,a2,……,an-1) 长度:广义表中包含数据元素(直接数据元素)个数称为该广义表的长度 深度:包含括弧的最大嵌套层数称为该广义表的深度 子表:广义表的递归性使得广义表中数据元素为子广义表 表头:广义表中第一个元素a0为广义表的表头 表尾:广义表中除了表头之外的其它元素组成的广义表称为表尾 再入表:广义表L中存在某个原子或子表成员出现多次 纯表:广义表L中不存在相同数据元素出现两次以上
1.广义表及相关概念2 • 一些广义表的例子: ① A=()。A是一个广义表,而且是一个空表,长度为0。表头为空,表尾为()。 ② B=(())。注意B与A的区别,B的长度为1,子表是()。表头head(A)=(),表尾tail(B)=()。 ③ C=(a,b,c)。C是一个长度为3的广义表,它的数据元素都是原子元素。表头head(C)=a,表尾tail(C)=(b,c)。此时,C是一个纯表。 ④ D=(a,(a,b),(a,(a,b)))。D是一个长度为3,深度为3的广义表,它的数据元素有原子元素,也有广义表。表头head(D)=a,表尾tail(D)=((a,b),(a,(a,b)))。此时,D是一个再入表 ⑤ E=(A,C,d)=((),(a,b,c),d)长度为3,深度为2。此时,广义表中的子表可以用广义表的名称来代替。 ⑥ F=(f,F)。广义表F是一个递归表述的表,意义为F=(f,(f,(f,(…)))),F的长度为2。此时,F是一个递归表 广义表能够实现共享!
4.3.1 广义表基本概念2 ADT Generalized Lists is {数据对象:D={ai| ai∈AtomSet或者ai∈Generalized,i=0,1,2,…,n-1,n≥0,AtomSet为某种数据元素类型} 数据关系:R={<ai-1,ai>|ai-1,ai∈D,i=1,2,…,n-1} 基本操作: (1)InitGList(Generalized Ls) 创建一个空的广义表Ls。 (2)GetHead(Generalized Ls) 返回取广义表Ls的表头。 (3)GetTail(Generalized Ls) 返回广义表Ls的表尾。 (4)GListDepth(Generalized Ls) 返回广义表Ls的深度。 (5)GListEmpty(Generalized Ls) 判断广义表Ls是否为空广义表。 (6)DestroyGList(Generalized Ls) 销毁广义表Ls释放空间。 …… } ADT Generalized 2.广义表ADT描述
4.3.2 广义表存储结构 • 广义表中的元素有原子元素和广义表,在存储结构设计上也需要使用表结点和原子结点两种类型。
4.3.2 广义表存储结构2 • 用C语言定义广义表的结点结构类型如下: 00 typedef struct GLnode 01 { 02 int tag; /* 标识域,用于区分原子结点和表结点 */ 03 union 04 { 05 DataType atom; /* 原子结点的值域,元素类型为DataType */ 06 struct GLnode *sublist; /* 指向表头结点的指针域 */ 07 } val; /* 通过val.atom和val.sublist来引用值域或指针域 */ 08 struct GLnode *next; /* 指向下一元素结点的指针域 */ 09 } GLnode; /* 定义广义表结点类型为GLnode */
4.3.2 广义表存储结构3 • 广义表存储结构示例:
4.3.3 广义表基本操作 1.广义表创建 00 GLnode *creatglist(char *s) /* 创建广义表 */ 01 { 02 GLnode *h; 03 char ch; 04 ch=*s; 05 s++; 06 if(ch!='\0') 07 { 08 h=(GLnode *)malloc(sizeof(GLnode)); 09 if(!h) 10 printf("error!\n"); 11 if(ch=='(') 12 { 13 h->tag=1; 14 h->val.sublist=creatglist(s); /* 递归创建广义表 */ 15 } 16 else 17 if(ch==')') 18 h=NULL;
4.3.3 广义表基本操作 1.广义表创建2 19 else 20 { 21 h->tag=0; 22 h->val.atom=ch; 23 } 24 } 25 else 26 h=NULL; 27 ch=*s; 28 s++; 29 if(h!=NULL) 30 { 31 if(ch==',') 32 h->next=creatglist(s); 33 else 34 h->next=NULL; 35 } 36 return h; 37 }
4.3.3 广义表基本操作2 2.输出广义表 00 void output(GLnode *h) 01 { 02 if(h!=NULL) 03 { 04 if(h->tag==1) 05 { 06 printf("("); 07 if(h->val.sublist==NULL) 08 printf(" "); 09 else 10 output(h->val.sublist); /* 输出广义表表头结点 */ 11 } 12 else 13 printf("%c",h->val.atom); /* 如该元素为原子结点,输出元素值 */ 14 if(h->tag==1) 15 printf(")"); 16 if(h->next!=NULL) 17 { 18 printf(","); 19 output(h->next); /* 继续输出广义表下一个元素 */ 20 } } }
4.3.3 广义表基本操作3 3.插入元素到广义表表头 00 viod insert(GLnode *h,char e) 01 { 02 GLnode *p; 03 p=(GLnode *)malloc(sizeof(GLnode)); /* 申请新结点p并赋值 */ 04 p->tag=0; 05 p->val.atom=e; 06 p->next=h->val.sublist; /* 将新结点p插入到广义表中 */ 07 h->val.sublist=p; 08 }
4.3.3 广义表基本操作4 4.求广义表深度 00 int depth(GLnode *h) /* 求广义表的深度 */ 01 { 02 int max=0,dep; 03 while(h!=NULL) 04 { 05 if(h->tag==1) /* 如该元素为广义表,继续求其子表深度 */ 06 { 07 dep=depth(h->val.sublist); 08 if(dep>max) 09 max=dep; /* 各子表深度最大值为子表的深度 */ 10 } 11 h=h->next; 12 } 13 return max+1; /* 返回子表深度+1 */ 14 }
本章小结 • 本章基本内容: