Transcript 第4章串、数组和广义表
项目四 串、数组、矩阵、广义表
项目导读
串是字符串的简称,它的每个数据元素由一个字符组成。串是
一种特殊的线性表。随着非数值处理的广泛应用,字符串已成为某
些程序系统的处理对象。本章主要介绍串的存储结构及基本运算。
数组可视为线性表的推广,其特点是数据元素仍然是一个表。本章
主要讨论数组的逻辑结构、存储结构、稀疏矩阵及其压缩存储等内
容。
广义表是线性表的一种推广。本章我们主要介绍广义表的定义
及其存储结构。
教学目标
通过本章学习,要求掌握以下内容:
1.串的存储结构及其基本运算。
2.数组的存储结构及稀疏矩阵的压缩存储。
3.广义表的定义及其存储结构。
4.1 串
串是一种特殊的线性表,它的数据对象是字符集合,它
的每个元素都是一个字符,一系列相连的字符就组成了一个
字符串,字符串简称串。
计算机中的非数值处理的对象基本上是字符串数据。在
程序设计语言中,字符串通常是作为输入和输出的常量出现
的。随着计算机程序设计语言的发展,产生了字符串处理,
字符串也作为一种变量类型出现在程序设计语言中。在汇编
语言的编译程序中,源程序和目标程序都是字符串数据。
在日常事务处理程序中,也有许多字符串应用的例子,
如客户的名称和地址信息、产品的名称和规格等信息都是作
为字符串来处理的。在文字处理软件中,在计算机翻译系统
中,都使用了字符串处理的方法。
1.1 串的定义和特性
串是由n个字符组成的有限序列,一般记为:
s="a1 a2…an ″ (n≥0)
其中,s是串的名字,用双引号括起来的字符序列是串
的值。双引号本身不属于串,它是定界符,用来标志字符串
的起始和终止。ai (1≤i≤n)可以是字母、数字或其他字符。
n为串中字符的个数称为串的长度。
空串:不含任何字符的串称为空串,它的长度n=0,记为
s=″″。]]
空格串:仅由空格组成的串称为空格串,它的长度为空
格符的个数。为了清楚起见,在书写时把空格写成' ',如
s=″″,则s串长度为n=1。
由于空格也是一个字符,因此它可以出现在其他串之间。
计算串的长度时,这些空格符也要计算在内。如串s="I am
a student.″,该串的长度是15,而不是12。
子串:串中任意个连续字符构成的序列称为该串的子串,
而包含该子串的串称为母串。例如,串s1=″abcdefghijk″,
s2=″def″,则称s2是s1的子串,s1是s2的母串。
两串相等:只有当两个串的长度相等,并且各对应位置
上的字符都相同时,两个串才相等。
4.1.2 串的顺序存储及其基本操作实现
串的顺序存储结构,简称为顺序串。顺序串中的字符被
顺序地存放在内存中一片连续的存储单元中。在计算机中,
一个字符只占一个字节,所以串中的字符是顺序存放在相邻
字节中的。
在C语言中,串的顺序存储可用一个字符型数组和一个
整型变量来表示,其中字符型数组存放串值,整型变量存放
串的长度。用C语言描述如下:
typedef struct string
{
char ch[MAX]; /*MAX是事先定义好的常量,用以确
定*/
int len;
/*字符串数组可能的最大长度*/
}STRING;
当计算机按字节(byte)单位编址时,一个机器字,即一个存储单
元刚好存放一个字符,串中相邻的字符顺序地存放在地址相邻的字节中。
若给定串s=″data structure″,则顺序串的存储如图4-1所示。
… d
a
t
a
s
t
r
u
c
t
u
r
e …
图4-1 顺序串存储示意图
当计算机按字(word)单位编址时,一个存储单元由若
干个字节组成。这时串的顺序存储结构有非紧缩存储和紧缩
存储两种方式。
1.串的非紧缩存储
假设计算机的字长为32位,即4个字节(bytes),则一
个存储单元仅存放一个字符时,就要浪费3个字节。若给定
串s=″data structure″,则非紧缩存储方式如图4-2所示。这
种存储方式的优点是对串中的字符处理效率高,但对存储空
间的利用率低。
图4-3 紧缩格式存储
图 4-2 非紧缩格式存储
2.串的紧缩存储
根据计算机中的字的长度,尽可能将多个字符存放在一
个字中。若给定串s=″data structure″,则紧缩存储方式如图
4-3所示。
与非紧缩存储的优缺点相反,紧缩存储方式对存储空间
的利用率高,但对串的单个字符操作很不方便,需要花较多
的处理时间。
在C语言中,若采用非紧缩的顺序存储方式存放长度不
超过MAX的串,可使用字符数组来实现,顺序串的类型定义
如下:
#define MAX 100
char ch[MAX];
/*用字符型数组ch存放串值,
MAX是已经定义过的常量*/
下面讨论顺序串的运算方法,主要包括求串长、串连接、
两串相等判断、取子串、插入子串、删除子串、子串定位和
子串置换。
根据C语言数组下标从0开始的特点,串中的第1个元素
存放在ch[0]中,其中字符的位置顺序为第0,1,…,
MAX-1。
1.求串长
求串长就是求字符串的长度,将求得的长度值返回。
#define MAX 50
int length(char *s)
{/*求串s中所含字符个数*/
int i;
for(i=0;s[i]!=’\0’;i++);
return(i);
}
main()
{ char ch[MAX]= ″china”;
int len;
len=length(ch);
printf(“%d”,len);
}
2.串连接
串连接就是把两个串连接在一起,其中一个串接在另一
个串的末尾,生成一个新串。如给出两个串s1和s2,把s2连
接到s1之后,生成一个新串s。其算法描述如下:
#define MAX 50
/*定义一常量MAX为50*/
char * connect(char * s1,char * s2)
{char s[MAX]; int i;
if(length(s1)+length(s2)<=MAX) /*当s1和s2的长度之和
小于或等于MAX时*/
{
for(i=0;i<length(s1);i++) /*将s1存放到s中*/
s[i]=s1[i];
for(i=0;i<length(s2);i++) /*将s2存放到s中*/
s[length(s1)+i]=s2[i];
s[length(s1)+i]=′\0′; /*设置串结尾标志*/
}
else
s[0]=’\0’;
return(s);
串*/
/*当s1和s2的长度之和大于MAX时*/
/*不能连接,置s空串*/
/*连接成功,返回s,不成功时返回空
}
main()
/*主程序*/
{
char a1[MAX]=″Beijing″, a2[MAX]=″China″,*s; /*字符数组a1
和a2并给这两个数组赋初值*/
s=connect(a1,a2); /*调用connect函数*/
printf(″\n%s\n″,s); /*输出结果*/
}
输出结果为:
Beijing China
在该例中,有两个串分别为a1=″Beijing″,a2=″China″,
调用函数connect(a1,a2)后,函数的串值为s=″Beijing
China″。
3.两串相等判断
只有当两个串的长度相等,并且各对应位置上的字符都
相等时,两个串才相等。如给定两个串s1和s2,判断这两个
串是否相等。当s1与s2相等时,返回函数值1,否则返回函
数值0。算法如下:
#define MAX 50
/*定义一常量MAX为50*/
int equal(char *s1,char *s2)
{
int i,m,n;
m=length(s1); /*求s1字符串的长度*/
n=length(s2); /*求s2字符串的长度*/
if(m!=n)
/*如果s1与s2长度不等*/
return(0);
/*返回函数值0*/
else
/*如果s1与s2长度相等*/
{
for(i=0;i<m;i++) /*判断s1和s2中各对应位置上字符是否相
等*/
if(s1[i]!=s2[i]) /*如果某一对应位置上字符不相等*/
return(0);
/*返回函数值0*/
}
/*两个串完全相等时*/
return(1);
/*返回函数值1*/
}
main()
/*主程序*/
{
char a1[MAX]=″Beijing″,a2[MAX]=″China″; /*定义两个字
符数组并给它们赋初值*/
int r;
r=equal(a1,a2);
/*调用equal函数*/
if(r)
printf(“equal\n”);
/*输出结果*/
else
printf(“ not equal\n”);
}
输出结果为:
not equal
在该例中,有两个串分别为a1=″Beijing″,a2=″China″,
调用函数equal(a1,a2)后,函数的返回值为0,所以才输
出not equal。
4.取子串
取子串就是在给定串中,从某一位置开始连续取出若干
字符,作为子串的值。例如,给定串s,从s中的第n1个字符
开始,连续取出n2个字符,放在t串中。其算法描述如下:
#define MAX 50
/*定义一常量MAX为50*/
int substring(char *s, int n1,int n2, char *t)
{
int i,k;
if((n1<1)||(n1>length(s)) /*如果位置不对则返回0值*/
return(0);
for(k=0;(k<n2)&&(s[n1-1+k]!=’\0’);k++) /*从串s的第n1个
位置取出n2长度的子串放在t串中*/
t[k]=s[n1-1+k];
t[k]=’\0’;
return(1);
}
main()
/*主程序*/
{
char a1[MAX]=″Beijing China″ ,s[MAX]; /*定义字符数组并给
该数组赋初值*/
if(substring(a1, 4,4,s)) /*如果取子串成功,则函数返回
值为1,输出该子串*/
printf(“%s”,s);
else /*函数返回值为0时,输出不成功信息*/
printf(“unsuccess”);
}
输出的结果为:
jing
5.插入子串
插入子串就是在给定串s的第p个字符之前插入一个串t。
插入成功函数返回值为1,否则函数返回值为0。其算法描述
如下:
#define MAX 50
int insert(char *s,int p, char *t)
{ int i,j,k;
i=length(s);j=length(t); /*求S串和t串的长度分别赋给变量
i,j*/
if((p<1)||(p>i)||(i+j)>=MAX) return(0); /*如果位置不合适
或者两个字符串合在一起的长度比MAX大时函数返回0值。
*/
for(k=i-1;k>p-2;k--) s[k+j]=s[k]; /*将s 串中从第p个字符开
始一直到串尾分别向后挪动j个位置以便将t串插入*/
for(k=0;k<j;k++) s[p-1+k]=t[k];
s[i+j]=’\0’;
return(1);
}
main()
/*主程序*/
{
char s[MAX]=″Beijing China″,t[MAX]=″Shanghai″; /*定义两个
字符数组并分别给它们赋初值*/
int i=8;
if(insert(s,i,t))
printf(“%s”,s);
else
printf(“insert is unsuccessful”);
}
输出结果为:
Beijing Shanghai China
6.删除子串
在串中删除从某一位置开始连续的字符。如在s串中,从
第p个字符开始连续删除j个字符。可能出现三种情况:
(1)如果p值不在s串范围内,不能删除。
(2)从第p个字符开始到最后的字符数不足j个,删除时,
不用移动元素。
(3)p和j都可以满足要求。删除后,要把后面其余的元素
向前移动j位。
删除成功后函数返回值为1,否则函数返回值为0。
删除子串的C语言算法描述如下:
#define MAX 50
/*定义一常量MAX为50*/
int delete(char *s,int p,int j)
{
int i,k;
i=length(s); /*求串s的长度*/
if((p<1)||(p>i)) return(0); /*如果位置不合适函数返回0值*/
if((p+j-1)>i) s[p-1]=’\0’; /*从第p个字符开始到最后的字符数不足
j个,删除时,不用移动元素,只需给s[p-1]时赋值为’\0’即
可*/
else
{for(k=0;k<=(i-p-j+1);k++) /*删除j个字符后要把后面的其余的元
素向前移动j位。*/
s[p-1+k]=s[p-1+j+k];
s[i-j]=’\0’;}
return(1);
}
main()
/*主程序*/
{char s[MAX]= ″Beiing Shanghai China″;
int i=8,j=9;
if(delete(s,i,j))
printf(“%s”,s);
else
printf(“delete is unsuccessful”);
}
输出结果为:
Beijing China
13
在该例中,串s=″Beijing Shanghai China″,在调用了delete
(s,i,j)后,得到的结果为s=″Beijing China″。
7.子串定位
子串定位运算也称串的模式匹配。这是一种很常用的串
运算,在文本编辑中经常用到这种运算。
所谓模式匹配,就是判断某个串是否是另一个已知串的
子串。如果是其子串,则给出该子串在这个串中的起始位置,
即子串第一个字符的位置。如果不是,则给出不是的信息。
下面介绍一个简单的模式匹配算法。
设有一母串s和一子串s1,判断母串s中是否包含子串s1。
其判断的基本方法是:从母串s中的第一个字符开始,按s1
子串的长度与s1子串中的字符依次对应比较。
若不匹配,则再从s串中的第二个字符开始,仍按s1子串的
长度与s1子串中的字符依次对应比较。
如此反复进行比较。直到匹配成功或者母串s中剩余的
字符少于s1的长度为止。若匹配成功,则返回s1串在s串中
的位置。若匹配不成功,则返回函数值0。
子串定位的算法如下:
#define MAX 50
/*定义一常量MAX为50*/
int match(char *s,char *s1)
{
int i,j,m,n;
i=j=0;
m=length(s); /*将s串的长度赋给m变量*/
n=length(s1);/*将s1串的长度赋给n变量*/
while((i<m)&&(j<n)) /*从s串的第一个字符开始与s1 串相比较*/
if(s[i]==s1[j]){i++;j++;}/*两个字符串对应的字符相等就将i和j分
别加1*/
else {i=i-j+1;j=0;} /*否则,修改i的值,比较从i-j+1的位置重新
开始*/
if(j>=n) return(i-n+1); /*如果j>=n那么s1 就是s的子串并返回其
在s串中的位置*/
else return(0);
}
main()
/*主程序*/
{
char a[MAX]=″Beijing Shanghai China″,
a1[MAX]=″Shanghai″; /*定义两个字符数组,给字符数组赋
初值*/
int r;
r=match(a,a1); /*调用match函数*/
printf(″\n%d″,r); /*输出结果*/
}
输出结果为:
9
8.串置换
串置换就是把母串中的某个子串用另一个子串
来替换。字符串替换算法可以用删除子串的算法和
插入子串的算法来实现。在这里不在详述,大家可
以依照上面的程序稍加改动即可。
4.1.3 串的链式存储及其基本操作实现
串的链式存储结构与单链表类似。由于串结构
的特殊性----结构中的每个数据元素是一个字符,用
链表存储串值时,就存在一个“结点大小”的问题,
即每个结点可以存放一个字符,也可以存放多个字
符,如图4-3所示的链表,其结点大小分别为4和1。
图4-3 串的链式存储结构
对于结点大小超过1的结点,在存储串值时,最后一个
结点的data域不一定正好填满,这时就要以一个非串值字符
(例如@)补足。
在链式存储方式中,结点大小的选择和顺序存储方式的
格式选择一样重要,直接影响对串的处理的效率。
总的来说,由于串的特殊性,使得采用采用链式存储结
构存储串不太实用,所以并不常用链式存储结构方式存储串。
4.1.4 串的应用举例
文本编辑是串应用的一个典型例子。有很多种文本编辑
的软件,用于不同的应用领域,如一般的办公室文书编辑、
专业用的报刊和书籍编辑。不同的软件公司也开发出了不同
的文本编辑软件。这些文本编辑软件的大小、功能都不一样,
但其基本操作原理都是一致的,通常都是串的插入、删除、
查找替换以及字符格式的设置等。
这些编辑软件进行文本处理时,对整个文本进行了不同
的拆分方法,如页、段、行、句、词和字等。在编辑过程中,
可以把整个文本看成是一个字符串,也可以把它叫做文本串,
那么页就是文本串的子串,行又是页的子串,等等。这样,
在编辑过程中,就能够以不同的单位对文本进行各种不同功
能的操作。
计算机在执行文本编辑程序时,要对文本建立起各种功
能所需要的存储信息表,如页表、行表。在编辑过程中,要
设立页指针、行指针和字符指针。在进行插入、删除等操作
时都是按照这些指针进行相应的修改工作。如插入字符后,
后面的文本要相应向后移动,删除字符后,后面的文本要相
应向前移动等。
4.2 数组
4.2.1 数组的定义和运算
数组是最常用的一种数据结构,在大多数程序设计语言
中都把数组作为固有的数据类型。
数组类似于线性表,是由同种类型的数据元素构造而成,
数组的每个元素由一个值和一组下标所决定,数组中各元素
之间的关系是由各元素的下标体现出来。也就是,在有下标
指定数组的相关元素中都存在一个与它相对应的值。
一维数组记为A[n]或A=(a0,a1, … ,an-1),每个数组元
素由一个值和一个下标来确定,数组元素的下标的顺序可作
为一个线性表中的序号。
二维数组,又称矩阵,图4-1为矩阵的表示形式。
图4-1 矩阵的表示
图4-1 矩阵的表示
二维数组中的每一个元素由矩阵元素aij值及一组下标(i,j)
(i=0,1,2, … ,m-1;j=0,1,2, … ,n-1)来确定。每组下标(i,j)都
唯一地对应一个值aij。二维数组中的每一个元素都要受到两个关
系即行关系第i行的行表ai0,ai1,ai2, … ,ain-1,同行元素aij是aij-1
的直接后继;另一个是表达列关系第j列的列表a0j,a1j,a2j, …
am-1j,同列元素aij是ai-1j的直接后继。
可以把二维数组看成是这样一个线性表,它的每个元素是一
个线性表。例如,图4-1所示是一个二维数组,以m行n列的矩阵
形式表示:
A=[(a00,a01, ,a0n-1),(a10,a11, ,a1n-1), ,(am-10,am11, ,am-1n-1)]
它的每个数组元素是一个以行序为主的线性表。而
A=[(a00,a10, ,am-10),(a01,a11, ,am-11), ,(a0n-1,a1n1, ,am-1n-1)]
它的每个数组元素是一个以列序为主的线性表。
对于数组,通常只有两种运算。
1.给定一个下标,存取相应的数据元素。
2.给定一个下标,修改相应的数据元素的某个数据项的值。
4.2.2 数组的顺序存储结构
由于数组一般不作删除或插入运算,所以一旦数据被定
义后,数组中的元素个数和元素间的关系就无需变动。一般
采用顺序存储结构,而随机存取是顺序存储结构的主要特性。
在这里我们只讨论二维数组的顺序存储结构,大部分高级语
言对二维数组的顺序存储均采用以行序为主的存储方式,如
图4-2(b)所示,如C语言。但在有的语言(如Fortran)中
采用的是以列序为主的存储方式,如图4-2(c)所示。
图4-2 二维数组的两种存储结构
在C语言中,数组中任一元素aij的地址计算公式为:
LOC(A[i][j])=LOC(A[0][0])+(i×n+j)×s 0<i≤m-1,
0<j≤n-1
其中,LOC(A[0][0])为数组的起始位置,s 为每个数据元
素所占存储单元个数。由于在定义数组时,LOC(A[0][0])、s
和n是已知的,因此根据公式可以计算出任一元素的存储地
址,实现随机存取。
4.3 矩阵的压缩存储
在许多的科学技术和工程计算中,矩阵是数值分析问题
研究的数学对象。对于矩阵数据结构主要研究其中计算机中
的存储,从而使矩阵得到更为有效地存储和使用。
一般情况下,高级语言对矩阵采用二维数组进行存储,
然而,实际应用中会遇到一些特殊矩阵,所谓特殊矩阵是指
矩阵中值相同的元素或者零元素的分布有一定的规律。例如,
对称矩阵、三角矩阵、带状矩阵等都是特殊矩阵。对于这种
特殊矩阵,在运算时为了节省存储空间,需要对这类矩阵进
行压缩存储。下面我们讨论如何对这些特殊矩阵实现压缩存
储。
4.3.1 对称矩阵的压缩存储
对称矩阵是个n阶方阵。若一个n阶矩阵A的元素满足于
aij=aji(0 ≤ i,j ≤ n-1)的性质,则称为n阶对称矩阵。即在对
称矩阵中,以对角线a00,a11,… ,an-1n-1为轴线的对称
位置上的矩阵元素值相等。由此,可以对每一对对称的矩阵
元素分配一个存储空间,那么n阶矩阵中的n × n个元素就可
以被压缩到n(n+1)/2个元素的存储空间中去。
若以行序为主序存储的对称矩阵为例,包括对角线元素
的下三角矩阵。假设以一维数组S[n(n+1)/2]作为n阶对称矩
阵A的存储结构,一维数组S[k]与矩阵元素aij之间存在一一
对应的关系的公式如下:
图4-3 对称矩阵的压缩存储
对于任意一组下标(i,j),均可在S中找到矩阵元素 aij ,
反之,对所有的k=0,1,2,…,n(n+1)/2-1,都能确定在S[k]中
的元素在对称矩阵中的下标位置(i,j)。其存储结构如图4-3
所示。
4.3.2 三角矩阵的压缩存储
三角矩阵也是量个n阶方阵,有上三角和下三角矩阵,
下(上)三角矩阵是主对角线以上(下)元素为零的n 阶矩
阵。图4-4是一个下三角矩阵。
如果不存储主对角线另一方的零元素,三角矩阵的压缩
存储方式可与对称矩阵相同。
图4-4 下三角矩阵
4.4 稀疏矩阵
4.4.1 稀疏矩阵的定义
在一个m×n矩阵中,如果零元素比非零元素个数多得多,
且非零元素在矩阵中的分布无规律,则称此矩阵为稀疏矩阵。
如图4-5所示的矩阵M和矩阵N都是稀疏矩阵。
图4-5 稀疏矩阵M和N
对于稀疏矩阵的压缩存储,仍然以存储矩阵的非零元素
为原则。但是稀疏矩阵中的非零元素是无规律地出现的,所
以不能简单的进行存储,对于稀疏矩阵的存储也有顺序存储
和链式存储两种方式。
4.4.2 稀疏矩阵的顺序存储及其基本操作实现
稀疏矩阵的顺序存储方式可以用三元组表示法来表示。
这个方法用一个线性表来表示稀疏矩阵,线性表的每个结点
对应稀疏矩阵的一个非零元素。其中包括三个域,分别为矩
阵非零元素行号、列号和值。记做(row,col,val),结点
仍按矩阵行优先顺序排列(跳过零元素),称该线性表为三
元组表。图4-6所示的三元组表分别对应图4-5的两个稀疏矩
阵。
图4-6 稀疏矩阵的三元组表示图例
若用一维数组ma存储矩阵M,用一维数组mb 存储矩阵
N。则用C语言描述算法的具体结构定义如下:
#define MAX 50
struct node{ int row;
int col;
int val;
}NODE;
NODE ma [MAX],mb[MAX];
当稀疏矩阵用三元组表示后,可对它进行某些运算,以
矩阵转置为例说明三元组表示的稀疏矩阵是如何进行运算的。
矩阵的转置运算是矩阵中一种最简单的基本运算。对于
m× n的矩阵M,它的转置矩阵N是一个n× m的矩阵,且
N[i][j]=M[i][j],其中,1≤i≤n,1 ≤j≤m。例如,图4-5中M是N
的转置矩阵,反之,N也是M的转置矩阵。相互转换的结果
应满足下列两个条件:
(row,col,val) (col,row,val)
使转置的三元组数组仍是按行排列的。进行矩阵的转置
操作,首先要将矩阵的行列值相互交换。使转置的三元组数
组仍然按行号的递增次序存储。具体实现转置运算时有以下
两种算法。
1.矩阵的列序转置
矩阵M是按行序为主序存储的,若按列序为主序进行转
置可以得到按行序为主序存储的转置矩阵N。假设,矩阵M
的三元组存入一维数组ma ,而转置矩阵N的三元组将存入
另一个一维数组mb中。由此,只要在数组ma中按三元组的
列域col的值开始扫描,从第1列至第n-1列,依序将三元组列
域与行域之值对换,并顺次存入数组mb中。转换成功后函
数返回值为1,否则,函数返回值为0。其具体算法描述如下:
#define MAX 50
typedef struct node{ int row;
int col;
int val;
}NODE;
NODE ma [MAX],mb[MAX];
int trans(NODE ma[],NODE mb[])
{int i,j,k=1,m,n,t;
if(ma[0].val==0) return (0); /*矩阵中无非零元素 */
m=ma[0].row; /*m为矩阵M的行数*/
n=ma[0].col; /*n 为矩阵M的列数*/
t=ma[0].val; /* t 为矩阵M的非零元素的个数 */
mb[0].row=n; /*转置矩阵N的行数 */
mb[0].col=m; /*转置矩阵N的列数 */
mb[0].val=t; /*转置矩阵N中的非零元素个数*/
for(j=1;j<=n;j++)
for(i=1;i<=t;i++)
if(ma[i].col==j)
{mb[k].row=ma[i].col;
mb[k].col=ma[i].row;
mb[k].val=ma[i].val;
k++;
}
return(1);
}
main()
{int i,j,val,m,n,t;
scanf(“%d,%d,%d”,&m,&n,&t);/* 输入矩阵M的行数m、列数n、
非零元素的个数t */
ma[0].row=m;
ma[0].col=n;
ma[0].val=t;
for (i=1;i<=t;i++)
{scanf(“%d,%d,%d”,&m,&n,&val);
ma[i].row=m;
ma[i].col=n;
ma[i].val=val;
}
j=trans(ma,mb);
if(j)
{t=mb[0].val;
for(i=1;i<=t;i++)
printf(“%5d%5d%5d\n”,mb[i].row,mb[i].col,mb[i].val);
}}
若设n为转置矩阵的列数,t矩阵中非零元素个数,则上
述算法的时间主要花费在两个循环上,所以时间复杂度为O
(n×t)。也就是说,时间的花费和矩阵M的列数和非零元
素个数的乘积成正比。若换一种方法用m×n二维数组表示
矩阵,则相应的矩阵转置算法的循环为:for(i=1;i<=n;i++)
for(j=1;j<=m;j++) b[i][j]=a[j][i];。此时,时间复杂度为O
(m×n)。比较两种算法,若矩阵中无非零元素,即t=
m×n时,则上述算法的时间复杂度将达到O(m×n2)。由
此可见,上述算法仅适用存在大量的非零元素的稀疏矩阵。
若要节省时间可以用快速转置方法进行转置。
2.矩阵的快速转置
矩阵的快速转置算法是在对按转置前矩阵M的列序进行
转置时,将转置后的三元组按矩阵N的行序直接置入mb中适
当的位置上。首先,要确定M中每列非零元素的个数,这就
确定了N中每一行非零元素的个数,也就确定了数组ma中每
一列第一个非零元素在mb中的存放位置。这样就能够容易
地把ma中的元素依次移到它们在mb中正确的位置上。为此,
需要设两个一维数组num[MAX]和pot[MAX]。num[j]( 1≤j≤n)
表示数组ma中第j列非零元素个数;而ma中第一列第一个非
零元素转置后必须放在mb[1]中,这样就可以推算出数组ma
中每一列第一个非零元素在数组mb中的位置。设数组pot用
以记录此位置。pot[j]为数组ma中第j列第一个非零元素在转
置后数组mb中的位置。显然有:
例如,矩阵M的num和pot的数组值,如图4-7所示。
j
1
2
3
4
5
num[j]
1
2
1
1
1
pot[j]
1
2
4
5
6
图4-7 矩阵M的num和pot数组的值
快速转置的C语言算法描述如下:
#define MAX 50
typedef struct node{ int rowi;
int col;
int val;
}NODE;
NODE ma [MAX],mb[MAX];
int tranquick( NODE ma[],NODE mb[])
{ int i,j,m,n,t,num[MAX],pot[MAX];
if( ma[0].val==0) return(0); /*矩阵无非零元素 */
m=ma[0].row; /*m为矩阵M的行数*/
n=ma[0].col; /*n 为矩阵M的列数*/
t=ma[0].val; /* t 为矩阵M的非零元素的个数 */
mb[0].row=n; /*转置矩阵N的行数 */
mb[0].col=m; /*转置矩阵N的列数 */
mb[0].val=t; /*转置矩阵N中的非零元素个数*/
for(i=1;i<=n;i++) num[i]=0; /* 对数组num初始化*/
for(j=1;j<=t;j++) /*统计第j列中非零元素的个数*/
{ m=ma[j].col;num[m]++;}
pot[1]=1;
for(i=2;i<=n;i++) pot[i]=pot[i-1]+num[i-1];/*pot数组记录每列非
零元素在mb数组中的位置*/
for(i=1;i<=t;i++)
{j=ma[i].col;
mb[pot[j]].row=ma[i].col;
mb[pot[j]].col=ma[i].row;
mb[pot[j]].val=ma[i].val;
pot[j]++;
}
return(1);
}
main()
{int i,j,val,m,n,t;
scanf(“%d,%d,%d”,&m,&n,&t);/* 输入矩阵M的行数m、列数n、
非零元素的个数t */
ma[0].row=m;
ma[0].col=n;
ma[0].val=t;
for (i=1;i<=t;i++)
{scanf(“%d,%d,%d”,&m,&n,&t);
ma[i].row=m;
ma[i].col=n;
ma[i].val=t;
}
j=tranquick(ma,mb);
if(j)
{t=mb[0].val;
for(i=1;i<=t;i++)
printf(“%5d%5d%5d\n”,mb[i].row,mb[i].col,mb[i].val);
}}
上述算法有四个并列的循环,第一个循环对数组num置
零进行初始化操作;第二个循环对转置前矩阵非零元素的三
元组数组ma进行扫描,且将统计出的ma中的每列非零元素
的个数放入数组num中;第三个循环按公式生成数组pot;
第四个循环是矩阵的转置操作。上述算法的时间主要花费在
这四个并列的循环上,其时间复杂度为O(n+t)。当矩阵M
无非零元素(即t=m×n)时,其时间复杂度就变成O
(m×n),此时和用二维数组表示矩阵转置的时间复杂度
相同。
4.4.3 稀疏矩阵的链式存储及其基本操作实现
用三元数组的结构来表示稀疏矩阵,在某些情况下,它
可以节省存储空间并加快运算速度,但在运算过程中,若稀
疏矩阵的非零元素位置发生变化,必将会引起数组中元素的
频繁移动。这时,采用链式存储结构会更好些。
十字链表是稀疏矩阵的另一种存储结构。十字链表适用
于在矩阵中非零元素的个数的运算及元素位置变动频繁的稀
疏矩阵。在十字链表中,每一个非零元素可用一个结点表示。
每个结点由五个域组成,其中行域(row)、列域(val)分
别表示非零元素的行号、列号和值。向下域(down)用以
链接含同一列中下一个非零元素的结点,向右域(right)用
以链接含同一行中下一个非零元素的结点。结点结构如图48(b)所示。
稀疏矩阵中同一行非零元素通过向右链接成一个行链表,
同一列的非零元素也通过向下域链接成一个列链表。因此,
对于表示每个非零元素的结点来说,它既是第i行行链表中的
一个结点,又是第j列列链表中的一个结点。整个稀疏矩阵是
用一个十字交叉的链表结构表示的。所以称作十字链表。另
外还设行指针数组rh和列指针数组ch 。设稀疏矩阵有m 行、
n列。行指针数组有m个元素,分别指向各行的含第一个非
零元素的结点;列指针数组有n个元素,分别指向各列的含
第一个非零元素的结点。这样对矩阵元素的查找可顺着所在
行的行链表进行,也可以顺着所在列的列链表进行。稀疏矩
阵M及十字链表表示如图4-8所示。
图4-8 稀疏矩阵的十字链表表示示例
采用十字链表表示的稀疏矩阵时,由于需要额外的存储
链域空间,且还要有行、列指针数组,所以在十字链表中,
只有当非零元素不超过总元素个数的20%时才可能比一般的
数组表示方法节省存储空间。
4.5 广义表
广义表是线性表的一种推广,广义表双称列表。
4.5.1 广义表的定义和特性
广义表是n个元素的有限序列,记为
L=(d1,d2, …,dn)
其中,L是广义表的名称,n是广义表的长度,di(1≤i≤n)
是广义表的数据元素,这这些数据元素也可为数据对象或广
义表。若di是数据元素,则称di是广义表L的原子;若di是广
义表,则称di为广义表的子表。显然,广义表的定义是一个
递归的定义,广义表中可以包含广义表。按照惯例,用英文
大写字母表示广义表的名称,小写字母表示数据元素。
当广义表L非空(n>0)时,第一个数据元素(d1)称为
广义表的表头(head),其余数据元素组成的表
(d2,d3, …,dn)为广义表L的表尾(tail),分别记为 head(L)=
d1,tail(L)= (d2,d3, …,dn)。下面是几个广义表的例子。
A=(a),广义表A的长度为1,唯一的数据元素是原子a。
B=(a,(x,y)),广义表B由数据元素a和子表(x,y)组成,
其长度为2。
C=(A,B,()),广义表C的长度为3,第一个数据元
素为广义表A,第二个数据元素为广义表B,最后一个数据
元素是空表,可以写成C=((a),(a,(x,y)),())。
D=(a,D),广义表长度为2,其中第二项仍为D,所以D
是一个递归表,相当于一个无限表,可写成D=
(a,(a,(a,…)))
E=(),E为长度为零的空表。
F=(E),F为长度为1的空表,可写成F=(())。
从上述定义和例子可推出如下结论。
一个广义表可以与其子表共享数据。在上述广义表C中,
子表A,B与C共享数据。
广义表可以是一个递归的表,即广义表也可以是其本身的
一个子表。上述广义表D就是一个递归的表。
另外,广义表的数据元素之间除了存在次序关系外,还
存在层次关系,这种关系可以图形表示。例如,图4-9所示
的广义表C,图中的圆形图符表示广义表,方形图符表示数
据元素。
图4-9 广义表C的图形表示
广义表中数据元素的最大层次为表的深度。数据元素的
层次就是包括该元素的括号对的数目。例如广义表G
(a,b,(c,(d)))中,数据元素a,b在第一层,数据元素c在第二
层,数据元素d在第三层,广义表G的深度为3。
4.5.2 广义表的存储结构及其基本操作实现
通常广义表采用链表存储结构。每个数据元素可用一个
结点表示,这些元素可能是原子或子表。由此采用两种结构
结点,一种是表结点,另一种为原子结点,如图4-10所示。
广义表的表结点由tag,hp,tp三个域组成。tag为标识域
(tag=1标识表结点);hp为表头域存放指向该子表的指针
值。tp域为链域,用以存放指向广义表中下一个元素的指针
值。图4-10(a)为表结点的结构;广义表的原子结点有三
个域,tag为标识域(tag=0标识原子结点),value为值域,
link为下一个数据元素的指针域,图4-10(b)为数据元素结点
的结构。
图4-10 广义表的链表结点
在链表中,广义表各元素之间的次序关系被表示得更为
清晰。一般用横向箭头表示元素之间的次序,用竖向的箭头
表示元素之间的层次关系。图4-11给出了广义表A~F的链表
存储结构。
图4-11 广义表的链表存储结构
与链表类似,可对广义表进行的操作有查找、插入和删
除等。由于广义表在结构上较线性表复杂的多,广义表操作
的实现要比线性表困难得多。下面介绍广义表的两种基本操
作,取广义表的表头head(L)和取表尾tail(L)。对前述例子有
以下操作。
head(A)=a ,tail(A)=();
head(B)=a ,tail(B)=((x,y));
head(C)=A ,tail(C)=(B,());
head(D)=a ,tail(D)=(D);
head(F)=E ,tail(F)=();
4.6 项目小结
1.串是一种受限制的线性表,串的存储方式有两种:
静态存储结构和动态存储结构。静态存储结构分为紧缩格式
存储和非紧缩格式存储。两种存储方式各有其优缺点,非紧
缩格式存储,不能节省内存单元,但操作起来比较方便。相
反,紧缩格式存储可以节省内存单元,但操作起来不方便。
2.数组是一种最常见的存储结构,数组一般采用顺序
存储结构进行存储,在内存中是以行序为主序进行存储的。
二维数组的特例就是矩阵,对于一些特殊矩阵一般会采用压
缩的存储方式,如,对称矩阵、三解矩阵等等,除此之外,
对于稀疏矩阵我们也采用压缩存贮方式:线性存储采用三元
组表示法;链式存储采用十字链表表示法。
3.广义表也是一种线性表,对于广义表的操作主要有
取表头和表尾的操作。
习题4
一、选择题
1.下面关于串的的叙述中,哪一个是不正确的?( )
A.串是字符的有限序列
B.空串是由空格构成的串
C.模式匹配是串的一种重要运算 D.串既可以采用顺序存储,
也可以采用链式存储
2. 若串S1=‘ABCDEFG’, S2=‘9898’ ,S3=‘###’,S4=‘012345’,
执行
concat(replace(S1,substr(S1,length(S2),length(S3)),S3),subst
r(S4,index(S2,‘8’),length(S2))),其结果为( )
A.ABC###G0123 B.ABCD###2345 C.ABC###G2345
D.ABC###2345
E.ABC###G1234 F.ABCD###1234 G.ABC###01234
3.设有两个串p和q,其中q是p的子串,求q在p中首次出现的
位置的算法称为( )
A.求子串
B.联接
C.匹配
D.求串长
4. 设有一个10阶的对称矩阵A,采用压缩存储方式,以行序为
主存储,a11为第一元素,其存储地址为1,每个元素占一个
地址空间,则a85的地址为( )。
A. 13
B. 33
C. 18
D. 40
5. 对稀疏矩阵进行压缩存储目的是( )。
A.便于进行矩阵运算 B.便于输入和输出 C.节省存储空
间 D.降低运算的时间复杂度
6. 广义表A=(a,b,(c,d),(e,(f,g))),则下面式子的值为( )。
Head(Tail(Head(Tail(Tail(A)))))
A. (g)
B. (d)
C. c
D. d
7. 设广义表L=((a,b,c)),则L的长度和深度分别为( )。
A. 1和1
B. 1和3
C. 1和2
D. 2和3
二、判断题
1.串是一种数据对象和操作都特殊的线性表。( )
2. 稀疏矩阵压缩存储后,必会失去随机存取功能。( )
3. 数组是同类型值的集合。( )
4. 数组可看成线性结构的一种推广,因此与线性表一样,可以
对它进行插入,删除等操作。( )
5. 一个稀疏矩阵Am*n采用三元组形式表示, 若把三元组中有
关行下标与列下标的值互换,并把m和n的值互换,则就完
成了Am*n的转置运算。( )
6. 二维以上的数组其实是一种特殊的广义表。( )
7. 广义表中的元素或者是一个不可分割的原子,或者是一个非
空的广义表。( )
三、填空题
1.空格串是指__(1)__,其长度等于___(2)__。
2.组成串的数据元素只能是________。
3.一个字符串中________称为该串的子串 。
4. 数组的存储结构采用_______存储方式。
5. 所谓稀疏矩阵指的是_______。
6. 广义表A=(((a,b),(c,d,e))),取出A中的原
子e的操作是: _______。
四、应用题
1.名词解释:串
2.描述以下概念的区别:空格串与空串。
3. 特殊矩阵和稀疏矩阵哪一种压缩存储后失去随机存取的功能?
为什么?
4. 试叙述一维数组与有序表的异同。
5. 一个n╳n的对称矩阵,如果以行或列为主序存入内存,则其
容量为多少?
五、算法设计题
1.设s、t为两个字符串,分别放在两个一维数组中,m、
n分别为其长度,判断t是否为s的子串。如果是,输出子串
所在位置(第一个字符),否则输出0。(注:用程序实现)
2.编写算法打印出由指针Hm指向总表头的以十字链表
形式存储的稀疏矩阵中每一行的非零元的个数。注意:行、
列及总表头结点的形式为:
它们已用val域链接成循环链表。非零元的结点形式也同
上,每一行(列)的非零元由right(down)域把它们链接
成循环链表,该行(列)的表头结点即为该行(列)循环链
表的表头。
项目实训 3
实训目的要求:
1.进一步理解稀疏矩阵的三元组存贮方法
2.学会转置矩阵的算法设计。
实训内容:
编写一个完整的程序,实现稀疏矩阵三元组的存贮及其转置
矩阵的算法设计。在主函数中程序中调用转置函数、输出原
矩阵函数和转置后的矩阵函数。
1.编写一个主函数实现稀疏矩阵的三元组存贮。
2.编写一个转置函数实现矩阵的转置。
3.编写一个输出矩阵函数。
4.在主函数中调用转置函数和输出函数。
实训参考程序:
#define MAX 40
#include <stdio.h>
#include<stdlib.h>
typedef stuct
{int i,j,v;/*一个三元组单元中放入非零元素的行号、列号及非零
元素的值*/
}NODE ;
typedef struct
{int m,n,t;/*稀疏矩阵的行号、列号、非零元素个数*/
NODE data[MAX]];
}MT;
MT tran( MT a)
{/*稀疏矩阵(三元组存储结构)转置算法*/
int p,q,col;
MT b;
b.m=a.n;b.n=a.m;b.t=a.t)
if(a.t!=0)
{q=1;
for(col=1;col<=a.n;col++)/*a稀疏矩阵的列数即为转置后稀疏
矩阵的行数*/
for(p=1;p<=a.t;p++)
if(a.data[p].j==col)
{b.data[q].j= a.data[p].i}
b.data[q].i= a.data[p].j;
b.data[q].v= a.data[p].v;
q++;}}
return (b);
}
void print(MT c)
{/*稀疏矩阵(三元组存储结构)输出*/
int n,i;
n=c.t;
for(i=1;i<=n;i++)
printf(“[%d]行号=%d列号=%d元素值=%d\n”,i,c.data[i].i,
c.data[i].j,c.data[i].v);
}
main()
{MT a,b;
int i,j,r,c,t,n;
printf(“\n\n输入矩阵行号数:”);
scanf(“%d”,&r);
printf(“\n\n输入矩阵列号数:”);
scanf(“%d”,&c);
printf(“\n\n输入非零元素个数:”);
scanf(“%d”,&t);
a.m=r;a.n=c;a.t=t;
printf(“\n\n”);
for(i=1;i<=t;i++)
{printf(“输入非零元素所在的行号、列号及非零元素值:\n”);
sanf(“%d,%d,%d”,&r,&c,&n);
a.data[i].i=r;a.data[i].j=c;a.data[i].t=n;}
printf(“\n\n稀疏矩阵三元组表:\n\n”);
print(a);
b=tran(a);
printf(“\n\n转置后稀疏矩阵三元组表:\n\n”);
print(b);
printf(“\n\n”);
}
依据图4-5执行程序后的结果(加下划线的为用户自己输入):
输入矩阵行号数:4
输入矩阵列号数:5
输入非零元素个数:6
输入非零元素所在的行号、列号及非零元素值:1,2,30
输入非零元素所在的行号、列号及非零元素值:2,1,10
输入非零元素所在的行号、列号及非零元素值:3,2,-5
输入非零元素所在的行号、列号及非零元素值:3,5,50
输入非零元素所在的行号、列号及非零元素值:4,3,20
输入非零元素所在的行号、列号及非零元素值:4,4,30
稀疏矩阵三元组表:
[1]行号=1列号=2元素值=30
[2]行号=2列号=1元素值=10
[3]行号=3列号=2元素值=-5
[4]行号=3列号=5元素值=50
[5]行号=4列号=3元素值=20
[6]行号=4列号=4元素值=30
转置后稀疏矩阵三元组表:
[1]行号=1列号=2元素值=10
[2]行号=2列号=1元素值=30
[3]行号=2列号=3元素值=-5
[4]行号=3列号=4元素值=20
[5]行号=4列号=4元素值=30
[6]行号=5列号=3元素值=50