500 likes | 672 Views
ç¬¬äº”ç« å¤šç»´æ•°ç»„ä¸Žå¹¿ä¹‰è¡¨. 2007.9. 5.1 多维数组 5.1.1 多维数组的定义 5.1.2 多维数组的å˜å‚¨ 5.2 矩阵的压缩å˜å‚¨ 5.2.1 特殊矩阵 5.2.2 稀ç–矩阵 5.3 广义表. 5.1 多维数组. 5.1.1 多维数组的定义. 一维数组 一维数组å¯ä»¥çœ‹æˆæ˜¯ä¸€ä¸ªçº¿æ€§è¡¨æˆ–一个å‘é‡ï¼Œå®ƒåœ¨è®¡ç®—æœºå†…æ˜¯å˜æ”¾åœ¨ä¸€å—连ç»çš„å˜å‚¨å•å…ƒä¸ï¼Œé€‚åˆäºŽéšæœºæŸ¥æ‰¾ã€‚ 有一个直接å‰é©±å’Œä¸€ä¸ªç›´æŽ¥åŽç»§ 二维数组 二维数组å¯ä»¥çœ‹æˆæ˜¯å‘é‡çš„æŽ¨å¹¿ã€‚ 有两个直接å‰é©±å’Œä¸¤ä¸ªç›´æŽ¥åŽç»§. 三维数组 æœ€å¤šå¯æœ‰ä¸‰ä¸ªç›´æŽ¥å‰é©±å’Œä¸‰ä¸ªç›´æŽ¥åŽç»§ 多维数组
E N D
第五章 多维数组与广义表 2007.9
5.1 多维数组 5.1.1多维数组的定义 5.1.2多维数组的存储 • 5.2 矩阵的压缩存储 5.2.1 特殊矩阵 5.2.2 稀疏矩阵 • 5.3 广义表
5.1.1多维数组的定义 • 一维数组 • 一维数组可以看成是一个线性表或一个向量,它在计算机内是存放在一块连续的存储单元中,适合于随机查找。 • 有一个直接前驱和一个直接后继 • 二维数组 • 二维数组可以看成是向量的推广。 • 有两个直接前驱和两个直接后继
三维数组 • 最多可有三个直接前驱和三个直接后继 • 多维数组 • 把三维以上的数组称为多维数组, • 可有多个直接前驱和多个直接后继 • 是一种非线性结构。
在C语言中的描述 typedef int datatype; datatype array1[N]; datatype array2[M][N]; datatype array3[X][Y][Z]; 数组一旦被定义,它的维数和维界就不再改变。因此,数组只有存取元素和修改元素值的操作。
5.2 多维数组的存储 • 考虑问题的基本出发点: • 计算机的内存结构是一维的。因此用一维内存来存多维数组,就必须按某种次序将数组元素排成线性序列。 • 数组一旦建立,结构中的元素个数和元素间的关系就不再发生变化。因此,一般都是采用顺序存储的方法来表示数组。
两种顺序存储方式 • 行优先顺序——将数组元素按行排列 • 在PASCAL、C语言中,数组就是按行优先顺序存储的。 • 列优先顺序——将数组元素按列向量排列 • 在FORTRAN语言中,数组就是按列优先顺序存储的。 • 推广到多维数组的情况: • 行优先顺序:先排最右下标,从右到左,最后排最左下标 • 列优先顺序:先排最左下标,从左向右,最后排最右下标。
计算机如何实现数组元素的随机存取? 如何计算数组元素的地址? 按上述两种方式顺序存储的序组,只要知道: • 开始结点的存放地址(即基地址), • 维数 • 每维的上、下界 • 每个数组元素所占用的单元数, 就可以将数组元素的存放地址表示为其下标的线性函数。因此,数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。
如何计算数组元素的地址? a a a a a a …… …… 00 00 01 01 0n 0n - - 1 1 a a a a a a …… …… 10 10 11 11 1n 1n - - 1 1 …………………………. …………………………. a a a a a a m m - - 1 0 1 0 m m - - 1 1 …… 1 1 …… m m - - 1 n 1 n - - 1 1 内存 内存 • 一维数组 • 二维数组 • 三维数组 0 ListSize -1
假设数组各维的下界是1,按“行优先顺序”存储,假设每个元素占用d个存储单元。假设数组各维的下界是1,按“行优先顺序”存储,假设每个元素占用d个存储单元。 • 二维数组Amn, aij的地址计算函数为: LOC(aij)=LOC(a11)+[(i-1)*n+j-1]*d • 三维数组Amnp,aijk的地址计算函数为: LOC(aijk)=LOC(a111)+[(i-1)*n*p+(j-1)*p +(k-1)]*d
更一般的二维数组是A[c1..d1,c2..d2],这里c1,c2不一定是1。更一般的二维数组是A[c1..d1,c2..d2],这里c1,c2不一定是1。 aij的地址计算函数为: LOC(aij)=LOC(ac1c2)+[(i-c1)*(d2-c2+1)+j-c2)]*d • 例如,在C语言中,数组各维下标的下界是0,因此在C语言中,二维数组的地址计算公式为: LOC(aij)=LOC(a00)+(i*(d2+1)+j)*d
最基本的原理 Ai1…in的起始地址 第一个元素的起始地址 该元素前面的元素个数 单位长度 = 〸 ╳
2006-1 • 对于二维数组a[0…4,1…5],设每个元素占1个存储单元,且以行为主序存储,则元素a[2,1]相对于数组空间起始地址的偏移量是___(40)___。(40)A.5B.10 C.15D.25 2003 • 设数组a[3..16,5..20]的元素以列为主序存放,每个元素占用两个存储单元,则数组元素a[i,j](3≤i≤16,5≤j≤20)的地址计算公式为___(11)___。 (11)A.a-118+2i+28j B.a-116+2i+28j C.a-144+2i+28j D.a-146+2i+28j
2001 • 二维数组X 的行下标范围是0~5,列下标范围是1~8,每个数组元素占六个字节,则该数组的体积为__(6)__个字节,若已知X 的最后一个元素的起始字节地址为382,则X 的首地址(即第一个元素的起始字节地址)为__(7)__,记为Xd。若按行存储,则X{1,5] 的起始地址是__(8)__, 结束字节地址是__(9)__。若按列存储,则X[4,8]的起始字节地址为__(10)__。 • (6): A.210 B.240 C.288 D.294 • (7): A.0 B.6 C.94 D.100 • (8): A.Xd+24 B.Xd+72 C.Xd+78 D.Xd+144 • (9): A.Xd+29 B.Xd+77 C.Xd+83 D.Xd+147 • (10):A.Xd+186 B.Xd+234 C.Xd+270 D.Xd+276
在编程时,简单而又自然的方法,是将矩阵描述为一个二维数组。矩阵在这种存储表示之下,可以对其元素进行随机存取。在编程时,简单而又自然的方法,是将矩阵描述为一个二维数组。矩阵在这种存储表示之下,可以对其元素进行随机存取。 • 但是在一些特殊矩阵中,非零元素呈某种规律分布或者矩阵中有大量的零元素,如果仍用二维数组存,会造成极大的浪费,尤其是处理高阶矩阵的时候。 • 为了节省存储空间, 我们可以对这类矩阵进行压缩存储。
5.2.1 几种常见的特殊矩阵 对称矩阵 在一个n阶方阵A中,若元素满足下述性质:aij=aji 0≦i,j≦n-1,则称A为对称矩阵。 特征:元素关于主对角线对称 压缩存储的办法: 只存矩阵中上三角或下三角中的元素。 所需空间:
三角矩阵 • 特征: • 上三角矩阵中,主对角线的下三角中的元素均为常数。在大多数情况下,常数为零。 • 下三角矩阵正好相反。 • 压缩方法: • 只存上(下)三角阵中上(下)三角中的元素 • 常数c可共享一个存储空间 • 所需空间:
对角矩阵 • 特征: • 所有的非零元素集中在以主对角线为中心的带状区域中, • 即除了主对角线和主对角线相邻两侧的若干条对角线上的元素之外,其余元素皆为零。 • 压缩存储的办法: 只存对角线上的元素。 • 存三对角矩阵所需的空间: 三对角矩阵
稀疏矩阵 • 特征:只有少量非零元素,且非零元素的分布没有规律。 • 压缩存储的办法: 只存非零元素。 • 所需空间:与非零元素的个数和存储方式有关。
5.2.2 特殊矩阵的压缩存储 • 矩阵类型 • 对称矩阵 • 三角矩阵 • 对角矩阵 • 压缩的基本思想: • 只存有用的元素 • 由用二维数组改为用一维数组来存放 • 说明: • 按C语言中规定,下标从0开始 • 不失一般性,按“行优先顺序”存储
= 所需空间 • 关键问题 • 如何确定一维数组的大小? • 如何确定矩阵元素在一维数组中的位置?从而保证对矩阵元素的随机存取 Aij的位置 = 该元素前的元素个数
1. 对称矩阵 • 如何确定一维数组的大小? • 设:存放下三角阵中的元素, • 则:如何确定元素Aij在一维数组中的位置?
2. 三角矩阵 • 如何确定一维数组的大小? • 设:在下三角阵中, • 则:如何确定元素Aij在一维数组中的位置?
3.三对角矩阵 • 如何确定一维数组的大小? • 如何确定元素Aij在一维数组中的位置? 在Aij之前有i行,共有3*i-1个非零元素, 在第i行, aij之前有j-i+1个非零元素, 3*i-1+(j-i+1) = 2*i+j
程序员试题 2004-1 对矩阵压缩存储的主要目的是__(5)__。 (5) A.方便运算 B.节省存储空间 C.降低计算复杂度 D.提高运算速度 2003 将一个三对角矩阵A[l..100,1..100]中的元素按行存储在一维数组B[l..298]中,矩阵A中的元素A[66,65]在数组B中的下标为___(4)___。 (4) A.195 B.196 C.197 D.198
5.2.3 稀疏矩阵的压缩存储 • 顺序存储:三元组表 • 链式存储:十字链表
1.三元组表存稀疏矩阵 • 考虑: • 只存非零元素 • 一个非零元素的必需信息有:(i,j,aij) • 记录一个稀疏矩阵的必需信息有:行数M,列数N,非零元素个数T M=5 N=5 T=7
三元组表的C语言描述 #define maxsize 10000 typedef int datatype; typedef struct{/*三元组结点*/ int i,j; datatype v; }TriTupleNode; typedef struct{ TriTupleNode data[maxsize]; /*三元组表*/ int m,n,t;/*稀疏矩阵的行数,列数,非零元素个数*/ }TriTupleTable;
2.带行指针的三元组表 • 把具有相同行号的非零元用一个单链表连接起来,稀疏矩阵中的若干行组成若干个单链表,合起来称为带行指针的链表。
5.2.4应用举例:稀疏矩阵的转置 1.矩阵转置的数学解释 一个m×n的矩阵A,它的转置B是一个n×m的矩阵,且a[i][j]=b[j][i],0≦i≦m,0≦j≦n。 Aij=Bji
2.利用三元组表实现转置 Aij=Bji M=4 N=2 T=5 M=2 N=4 T=5
思想一:直接交换a.data中i和j的内容 问题:b.data是一个按列优先顺序存储的稀疏矩阵B 解决方法:重新排列B中三元组的顺序。 Aij=Bji M=2 N=4 T=5 按i排序 M=4 N=2 T=5
M=2 N=4 T=5 b.m=a.n; b.n=a.m; b.t=a.t;/*基本信息的赋值*/ /*按交换i、j的方式给B的三元组赋值*/ for ( i=0; i<b.t; i++ ) { b.data[i].i=a.data[i].j; b.data[i].j=a.data[i].i; b.data[i].v=a.data[i].v;} /*扫描B,按i排序*/ M=4 N=2 T=5 按i排序
思想二:在A中按列序找三元组,写入B • B的行优先即A的列优先 • 对A的第col列 ,扫描三元组表a.data,找出所有列号等于col的三元组,将它们的行号和列号互换后依次放入b.data中,即可得到B的按行优先的压缩存储表示。 Aij=Bji col=0,没有匹配的三元组 col=1,找到2,3,4 col=2,找到5,6 M=4 N=2 T=5 M=2 N=4 T=5
Void transmatrix(tripletable a,tripletable b) { int pa, pb, col; b.m=a.n; b.n=a.m; b.t=a.t; /*基本信息的赋值*/ if(b.t<=0) { printf(“A=0\n”); return 0;} /*无非零元素*/ pb=0; /*pb指向三元组表B中的当前位置*/ for(col=0;col<a.n;col++)/*按列col扫描表A*/ for(pa=0;pa<=a.t;pa++) /*pb指向表A中的当前位置*/ /*找所有列号等于col的三元组,i,j互换写放入B*/ if(a.data[pa].j==col){ b.data[pb].i=a.data[pa].j; b.data[pb].j=a.data[pa].i; b.data[pb].v=a.data[pa].v; pb++; } }
算法分析 • 主要的工作是在pa和col的两个循环中完成的,故算法的时间复杂度为O(n*t),即矩阵的列数和非零元的个数的乘积成正比。 • 传统矩阵的转置算法的时间复杂度为O(n*m) • 当非零元素的个数t和m*n同数量级时,该算法的时间复杂度为O(m*n2)。(最坏情况) • 三元组顺序表虽然节省了存储空间,但时间复杂度比一般矩阵转置的算法还要复杂,同时还有可能增加算法的难度。因此,此算法仅适用于t<=m*n的情况。
思想三:快速转置 • 基本思想:在A中按行序找三元组,确定该三元组在B中的位置,写入B. • 关键问题:如何确定每个三元组在B中的位置 • A中三元组在B的中位置= 每列的第一个非零元素在数组B中应有的位置 + 每一列非零元素的个数
M=2 N=4 T=5 M=4 N=2 T=5 Aij=Bji 由递推关系得出cpos的值: cpos[0]=0 cpos[i]=cpos[i-1]+cnum[i-1]
void fasttranstri(tritupletable b,tritupletable a) { intcol;/*当前列号*/ int pa, pb; /*pa,pb:三元组a,b的下标*/ int cnum[0..a.n], cpos[0..a.n]; b基本信息m,n,t的赋值; 若a无非零元素,则返回; 初始化数组cnum; /*统计a中每列非零元素的个数;*/ for(pa=0; pa<a.t; pa++) { col=a.data[pa].j; cnum[col]++;} /*由递推关系计算cpos的值*/ cpos[0]=0; for(col=1;col<=a.n;col++) cpos[col] = cpos[col-1] + cnum[col-1];
/*扫描a,将元素交换i,j写入b*/ for( pa=0; pa<a.t; pa++ ){ col = a.data[p].j; pb = cpot[col]; b.data[pb].i = a.data[pa].j; b.data[pb].j = a.data[pa].i; b.data[pb].v = a.data[pa].v; cpos[col]++; } } }
算法分析 • 时间复杂度O(n+t)。 • 当非零元素的个数t和m*n同数量级时,该算法的时间复杂度为O(m*n),与不压缩存储的情况相同。
5.5.1 基本概念 广义表是第2章提到的线性表的推广。线性表中的元素仅限于原子项,即不可以再分,而广义表中的元素既可以是原子项,也可以是子表(另一个线性表)。 1.广义表的定义 广义表是n≥0个元素a1,a2,…,an的有限序列,其中每一个ai或者是原子,或者是一个子表。广义表通常记为LS=(a1,a2,…,an),其中LS为广义表的名字,n为广义表的长度, 每一个ai为广义表的元素。但在习惯中,一般用大写字母表示广义表,小写字母表示原子。 2.广义表举例 (1)A=( ),A为空表,长度为0。 (2)B=(a,(b,c)),B是长度为2的广义表,第一项为原子,第二项为子表。 (3)C=(x,y,z),C是长度为3的广义表,每一项都是原子。 (4)D=(B,C),D是长度为2的广义表,每一项都是上面提到的子表。 (5)E=(a,E),是长度为2的广义表,第一项为原子,第二项为它本身。
3.广义表的表示方法 1)用LS=(a1,a2,…,an)形式,其中每一个ai为原子或广义表 例如:A=(b,c), B=(a,A) , E=(a,E) 都是广义表。 2)将广义表中所有子表写到原子形式,并利用圆括号嵌套 例如,上面提到的广义表A、B、C可以描述为: A(b,c) B(a,A(b,c)) E(a,E(a,E(…))) 3)将广义表用树和图来描述
4.广义表的深度 一个广义表的深度是指该广义表展开后所含括号的层数。 . (1)A=(b,c) (2)B=(a,A) (3)C=(A,B) (4) E=(a,E) depth=1 depth=2 depth=3 depth=∞ E a 5.广义表的分类 (1)线性表:元素全部是原子的广义表。 (2)纯表:与树对应的广义表,见图 (1)和(2)。 (3)再入表:与图对应的广义表(允许结点共享),见图 (3)。 (4)递归表:允许有递归关系的广义表,例如(4)。 这四种表的关系满足: 递归表再入表纯表 线性表