430 likes | 658 Views
第 5 章 数组和广义表. 2. 5.1 数组的逻辑结构. 数组 (array) 是最常用的数据结构之一。几乎所有的程序设计语言都把数组类型设定为固有类型。. 线性结构中的数据都是非结构的原子类型,元素的值是不再分解的。而数组可以看成是线性表在下述含义上的扩展:. 表中的数据元素本身也是一种数据结构。. 数组的定义. 数组的基本操作. 3. 5.1.1 数组的定义. 数组是由下标和值组成的序对集合。在数组中,一旦给定下标,都存在一个与其相对应的值,这个值就称为数组元素。.
E N D
2 5.1 数组的逻辑结构 数组 (array) 是最常用的数据结构之一。几乎所有的程序设计语言都把数组类型设定为固有类型。 线性结构中的数据都是非结构的原子类型,元素的值是不再分解的。而数组可以看成是线性表在下述含义上的扩展: 表中的数据元素本身也是一种数据结构。 数组的定义 数组的基本操作
3 5.1.1 数组的定义 数组是由下标和值组成的序对集合。在数组中,一旦给定下标,都存在一个与其相对应的值,这个值就称为数组元素。 也可以说,数组中的每个数据元素都对应于一组下标( j1, j2, … , jn),每个下标取值范围是 1≤ji≤bi , bi 称为第 i维的长度( i = 1, 2, …, n)。显然,当 n = 1 时,n 维数组就退化为定长的线性表。反之,n维数组也可以看成是线性表的推广。
a11 a21 … am1 a12 a22 … am2 a13 a23 … am3 … … … … a 1.n a2, n … am, n 4 可以把二维数组看成是这样一个定长线性表:它的每个数据元素也是一个定长线性表。 例如,下面是一个二维数组,且以 m行 n列的矩阵形式表示。 Am×n =
二维数组 A还可以看成是一个线性表: a11 a21 … am,1 a12 a22 … am, 2 a1, n a2, n … am, n a13 a23 … am, 3 5 A = (α1, α2, … , αn ) 每个数据元素aj是一个列向量形式的线性表 αj = ( a1j, a2j, … , am, j ) 1 ≤ j ≤ n Am×n = … 每个数据元素是一个行向量形式的线性表 B=(β1β2β3 … ,βm) βi = ( ai 1 , ai 2 , … , ai, n ) 1 ≤ i ≤ m ( a21a22… a2, n) , … , Am×n = ( ( a11a12… a1, n) , …, ( am, 1am, 2… am, n) )
基本操作: InitArray (A, n, bound1, … , boundn ); 操作结果:如果维数 n和各维长度合法,则构造 相应的数组 A,并且返回 TRUE。 5.1.2 数组的抽象类型定义 ADT Array { D={aj1j2j3…..jn|n>0,称为数组的维数, ji是数组的第i维下标,1≤ ji≤bi, bi为数组第i维的长度, aj1j2j3…..jn∈ElementSet} 数据关系:R={R1,R2, …….Rn} Ri=<aj1…ji….jn,aj1…ji+1…jn>| 1≤jk ≤bk, 1≤k≤n 且 k≠i, 1≤ji≤bi-1, aj1…j2….jn,aj1…ji+1…jn∈D,i=1,…n}
基本操作 DestroyArray(A):销毁数组A。 GetValue(A,e, index1, … , indexn ): 初始条件:A是 n维数组,e为元素变量,随后是n个下标值。 操作结果:若各下标合法,则用e返回数组A中由由index1,… indexn所指定的元素的值. SetValue ( A, e, index1, … , indexn ); 初始条件:A是 n维数组,e为元素变量,随后是 n个下标值。 操作结果:若各下标合法,则将数组A中由index1,… indexn所指定的元素的值置为e.
8 5.2 数组的顺序存储结构 由于内存储器的结构是一维的。一维数组可直接采用顺序存储。用一维的内存存储表示多维数组时,需按某种次序将数组中元素排成一线性序列,再将这个线性序列存放在一维的内存中,即数组的顺序存储结构表示。 顺序存储的定位公式 数组的顺序存储表示 基本操作的算法描述
9 5.2.1 顺序存储的定位公式 用顺序存储结构来存储数组中的元素,一定要按照某种次序将元素排成一个线性序列。对二维数组可以有两种存储方式: (1) 以列为主序 ( column major order ) 的存储方式,即按列优先,逐列顺序存储。 (2) 以行为主序 ( row major order ) 的存储方式,即按行优先,逐行顺序存储。
⑵二维数组的地址计算 假设每个数据元素占 C个存储单元,且以行序为主序的进行存储,则二维数组 A中任一元素 aij的存储位置可以由下面定位公式确定 LOC (A[i],[ j]) = LOC (A[1], [1]) +( n*(i-1)+(j-1))*C 10 其中: LOC (A[i[,[ j]) 是 aij的存储位置; LOC (A[1], [1]) 是 a11的存储位置,即二维数组 A的起始存储位置,也称为基地址或基址; n 是数组第二维的长度。 一般地: LOC (A[i],[ j]) = LOC (A[s], [t]) +( n*(i-s)+(j-t))*C
⑶三维数组的地址计算 三维数组A(1:r,1:m,1:n)。假设每个数据元素占size个存储单元,且以行序为主序的进行存储,首元素a111的地址为Loc(A[1][1][1]),求任意元素aijk的地址。 显然,ai11地址为 Loc(A[i][1][1])=Loc(A[1][1][1])+(i-1)*m*n,因为在该元素之前有i-1个m*n的二维数组。 11 不难得到三维数组任意元素aijk的地址: Loc(A[i][j][k])=Loc(A[1][1][1])+((i-1)*m*n+(j-1)*n+(k-1))*size,其中:1≤i≤r, 1≤j≤m, 1≤k≤n。
12 5.3 矩阵的压缩存储 矩阵 (matrix) 是很多科学与工程计算问题中研究的数学对象。在数据结构中,我们感兴趣的不是矩阵本身,而是如何存储矩阵的元素而使矩阵的各种运算能够有效地进行。 在数值分析中经常出现有些阶数很高的矩阵,同时在矩阵中有许多值相同的元素或者是零元素。有时为了节省存储空间,可以对这类矩阵进行压缩存储。
13 所谓压缩存储是指:为多个值相同的元只分配一个存储空间;对零元不分配空间。 特殊矩阵的压缩存储 稀疏矩阵的逻辑结构 稀疏矩阵的存储结构
14 5.3.1 特殊矩阵的压缩存储 假若相同的元素或者零元素在矩阵中的分布有一定规律,则称特殊矩阵。特殊矩阵主要有 3 种:对称矩阵、三角矩阵、带状矩阵。 在所有这些统称为 “特殊矩阵” 的矩阵中,非零元的分布都有一个明显的规律,从而都可以将其压缩存储到一维数组中,并且找到每个非零元在一维数组中的对应关系。
15 1. 对称矩阵 若一个 n阶矩阵 M中的元满足下述性质 aij = aji 1 ≤ i,j ≤ n 则称为 n阶对称矩阵。
0 0 16 3. 带状矩阵 一个 n阶方阵,若它的全部非零元素落在一个以对角线为中心的带状区域中,则称该矩阵为带状矩阵,或对角矩阵。这个带状区域若包含主对角线上下各 b条对角线道上元素,那么,b称为该带状矩阵的半带宽,或称该带状矩阵的带宽为 (2b+1)。 b条 b条
a11 a12 0 0 0 a21 a22 a23 0 0 0 a32 a33 a34 0 Ann= 0 0 a43 a44 a45 0 0 … … … 17 带状矩阵中最常见的是三对角带状矩阵。 特点: 当 i=1 j=1,2 1<i<n,j=i-1,i,i+1 i=n,j=n-1,n aij非零,其它元素均为零
18 三对角带状矩阵的压缩存储,以行序为主序进行存储,且只存储非零元素。其方法为 1.确定存储该矩阵所需的一维向量空间的大小 除第一行和最后一行只有两个元素外,其余各行均有3个非零元素,由此得到一维向量所需的空间大小为:3n-2 2.确定非零元素在一维数组空间中的位置 Loc(a[i][j])=Loc(a[1][1])+2(i-1)+j-1
19 5.3.2 稀疏矩阵的逻辑结构 1. 稀疏矩阵的定义 一般来说,当矩阵中非零元素的个数远远小于矩阵元素的总数时,称之为稀疏矩阵。假设在 m×n的矩阵中,若有 t个元素不为零,令 = t / (m×n),则称 为矩阵的稀疏因子。通常认为 ≤0.05时称为稀疏矩阵。
5.3.3 稀疏矩阵的存储结构 20 1. 三元组顺序表 按照压缩存储的概念,只存储稀疏矩阵的非零元素。因此,除了存储非零元素的值 aij之外,还必须同时记下它所在矩阵的行 i和列 j 的位置。反之,一个三元组 ( i, j, aij ) 唯一确定了矩阵的一个非零元素。因此,稀疏矩阵可以由表示非零元的三元组及其矩阵的总的行列数唯一确定。 假设以顺序存储结构表示三元组表,则可以得到稀疏矩阵的一种压缩存储方式,这种方式称之为三元组顺序表。
0 12 9 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 0 14 0 0 0 24 0 0 0 0 0 18 0 0 0 0 0 15 0 0 -7 0 0 0 21 M = 矩阵 M可以由三元组表 (1, 3, -3),(1, 6, 15),(2, 1, 12),(2, 5, 18), (3, 1, 9),(3, 4, 24),(4, 6, -7),(6, 3, 14) 再加上(7, 6)这一对总的行列值来描述。
22 (1) 三元组顺序存储表示 # define MAXSIZE 1000 // 假设非零元个数的最大值为 1000 typedef struct { // 三元组顺序表的元素结构定义 int row, ;col // 该非零元的行下标和列下标 ElementType e; // 该非零元的值 } // Triple; typedef struct { // 三元组顺序表存储结构定义 Triple data[ MAXSIZE+1]; // 非零元三元组表,data[0] 未用 int m, n, len; // 矩阵的行数、列数和非零个数 } // TSMatrix; // 三元组顺序表的类型名
23 (2) 利用三元组顺序表实现矩阵的转置运算 显然,一个稀疏矩阵的转置矩阵仍是稀疏矩阵。假设 a和 b是 TSMatrix(三元组顺序表)类型变量,分别表示矩阵 M和其转置矩阵T。那么,只要做到下面 3 点就可以由 a得到 b,实现矩阵的转置。 将矩阵的行列值相互交互; 将每三元组中的 row和 col相互调换; 重排三元组之间的次序。 在这 3 点中,最关键的是第 3 条,即如何使 b.data 中的三元组以 T的行(M的列)为主序依次排列。
0 12 9 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 0 14 0 0 0 24 0 0 0 0 0 18 0 0 0 0 0 15 0 0 -7 0 0 0 0 0 -3 0 0 15 12 0 0 0 18 0 9 0 0 24 0 0 0 0 0 0 0 -7 0 0 0 0 0 0 0 0 14 0 0 0 0 0 0 0 0 0 row col e row col e M= T= a.data[1] b.data[1] 1 2 12 1 3 9 3 1 -3 3 6 14 4 3 24 5 2 18 6 1 15 6 4 -7 1 3 -3 1 6 15 2 1 12 2 5 18 3 1 9 3 4 24 4 6 -7 6 3 14 b.data[2] a.data[2] b.data[3] a.data[3] b.data[4] a.data[4] b.data[5] a.data[5] a.data[6] b.data[6] b.data[7] a.data[7] b.data[8] a.data[8] b.data a.data 24 转 置 矩 阵 原 矩 阵 原 始 的 三 元 组 表 转 置 的 三 元 组 表
25 使 b.data 中的三元组以 T的行(M的列)为主序依次排列的方法有如下两种: 方法一: 按照 b.data 中三元组的次序,依次在 a.data 中找到相应的三元组进行转置。 方法二: 按照 a.data 中三元组的次序进行转置,并将转置后的三元组置入 b.data 中恰当的位置。
26 ① 算法思想 按照方法一,即按照“被转置矩阵” M的三元组表A的“列序”递增顺序进行转置。为了找到矩阵 M的每一列中所有的非零元素,需要对其三元组 a.data 从第一行起进行扫描,方法如下: 在 A中按三元组的列域值 (col) 开始扫描,依序将三元组 a.data 的列域值 (col) 与行域值 (row) 进行对换,并且存入 B中。由于A是以M的行序为主序来存放每个非零元的,由此得到转置后矩阵的三元组表B恰是 以“行主为主序”。
i 1 1 2 2 3 3 4 6 j 3 6 1 5 1 4 6 3 v -3 15 12 18 9 24 -7 14 i j v 27 利用三元组顺序表存储实现矩阵的转置 原始的三元组表 转置的三元组表 5 3 4 3 6 1 1 6 6 3 1 1 4 3 2 2 24 18 -3 15 9 -7 14 12 b.data a.data
28 ② 算法描述(稀疏矩阵“列序”递增转置算法) void TransposeTSMatrix ( TSMatrix A, TSMatrix *B ) /*采用三元组表结构,求稀疏矩阵 A的转置矩阵 B。在程序中, { int i,j,k; // j指示 B->data 中三元组的序号, i指示 A.data 中三元组的序号, // k指示A 的列号(即B 的行号) B->m = A.n; // 将稀疏矩阵 A的列数值作为其转置矩阵 B的行数值 B->n = A.m; // 将稀疏矩阵 A的行数值作为其转置矩阵 B的列数值 B->len = A.len; // 转置矩阵 B与稀疏矩阵A的非零元个数相等 if ( B->len>0 ) { j = 1;
for (k = 1; k < = A.n; k++) for ( i = 1; i < = A.len; i++ ) if ( A.data[i].col = = k ) { // 进行转置 29 B->data[j].row = A.data[i].col; // 稀疏矩阵A的列域值成为其转置矩阵 B的行域值 B->data[j].col = A.data[i].row; // 稀疏矩阵 A的行域值成为其转置矩阵 M的列域值 B->data[j].e = A.data[i].e; // 将稀疏矩阵 M的非零元值赋给其转置矩阵 T j++; // B->data 中三元组的序号加 1 } // if } // if return OK; }/* TransposeSMatrix*/
30 ③ 算法分析 前面给出的求转置矩阵算法的主要工作是在 i和 k 的两重循环中完成的,所以此算法的时间复杂度为 O(A.n×A.len) 即和矩阵 A的列数和非零元的个数的乘积成正比。 一般矩阵的转置算法(经典算法)为: for ( col = 1; col < = n; ++col ) for ( row = 1; row < = m; ++row ) B[col][row] = B[row][col]; 时间复杂度为 O(m×n)。
当矩阵 M中非零元个数几乎和矩阵元素个数相等时,即 len和 m×n等数量级时,算法时间复杂度就为 O(m×n2),虽然节省了存储空间,但时间复杂度提高了。由此可见,上述求转置矩阵算法只适合于 len << m×n的情况。 31
32 2. 十字链表 当矩阵非零元素的位置或个数经常变动时,就不易采用顺序存储结构表示三元组的线性表。例如,在进行 “将矩阵B加到矩阵 A上” 的操作时,由于非零元素的插入或删除将会引起 A.data 中元素的大量移动。为此,对这种类型的矩阵,采用链式存储结构表示三元组的线性表更为恰当。
row col value down right 33 (1) 稀疏矩阵的十字链表存储表示 在链表中,矩阵的非零元素可用如下结点表示: 矩阵中非零元的行号 row; 矩阵中非零元的列号 col; 矩阵中非零元的值 e; 向右域 right,用以链接同一行中下一个非零元; 向下域 down,用以链接同一列中下一个非零元。 非零元列号 非零元行号 非零元的值 向下域 向右域
34 typedef struct OLNode { // 结点定义 int row, col; // 该非零元的行和列下标 ElementType value; // 该非零元的值 struct OLNode *right, *down; // 该非零元所在的行表和列表的后继链域 } OLNode; *Olink; typedef struct { // 十字链表定义 int m, n, len; // 稀疏矩阵行数、列数和非零元个数 Olink *row_head, *col_head; // 行和列链表头指针向量基址,由 CreateSMatrix 分配 } CrossList; // 十字链表存储结构的类型名
35 5.4 广义表 广义表 (generalized list)是线性表的推广,有时也称为列表(lists,用复数形式以示与统称的表 list 的区别)。广泛地应用于人工智能等领域的 LISP(表处理语言),把广义表作为基本的数据结构,就连程序也表示为一系列的广义表。
36 和数组一样,广义表也可以看成是线性表在下述含义上的扩展:表中的数据元素本身也是一种数据结构。 广义表的逻辑结构 广义表的存储结构
37 5.4.1 广义表的逻辑结构 1. 广义表的定义 广义表一般记作:GL = ( a1 , a2 , … , an ) 其中: GL是广义表 ( a1 , a2 , … , an ) 的名称; n是广义表 GL的长度; ai可以是单个元素,也可以是广义表,分别称为广义表 GL的原子和子表,习惯上用大写字母表示广义表的名称,用小写字母表示原子的名称。
38 2. 广义表的三个重要结论 例5-1A = ( ),A是一个空表,它的长度为零。 例5-2B = (e),B只有一个原子 e,它的长度为 1。 例5-3C = (a, (b, c, d)),C的长度为 2,两个元素分别为原子 a和子表 (b, c, d)。 例5-4D = (A, B, C),D的长度为 3,三个元素分别为 A、B 和 C,都是广义表。显然,将上面所述三个子表的值代入以后,则有 D = (( ), (e), (a, (b, c, d)))。 例5-5E = (a, E),这是一个递归表,它的长度为 2,E 相当于一个无限的广义表 E = (a, (a, (a, …)))。
39 从上述定义和例子推出如下广义表的三个重要结论 (1) 广义表的元素可以是子表,而子表的元素还可以是子表,…。由此,广义表是一个多层次结构。 (2) 广义表可以为其他广义表所共享。例如在上述例子中,广义表 A、B和 C为 D的子表,则在 D中可以不必列出广义表的值,而是通过子表的名称引用。 (3) 广义表可以是一个递归表,即广义表也可以是其本身的一个子表。例如广义表 E就是一个递归的表。
40 3. 广义表的两个基本操作 和线性表相类似,可以对广义表进行的操作有查找、插入、删除和取表元素等。由于广义表在结构上较线性表复杂的多,因此,广义表操作的实现也不如线性表简单。在这些操作中,最重要的两个基本操作是: (1) 取广义表表头 GetHead:表中的第一个元素为此表的表头。 (2) 取广义表表尾 GetTail:表中除第一个元素外的其余元素组成的表为此表的表尾。
任何一个非空广义表的表头可能是原子,也可能是广义表;而其表尾必定是广义表。例如,广义表如下:任何一个非空广义表的表头可能是原子,也可能是广义表;而其表尾必定是广义表。例如,广义表如下: 41 (1) A = ( ) (2) B = (e) (3) C = (a, (b, c, d)) (4) D = (A, B, C) (5) E = (a, E) 对定义表 B,C,D取表头和取表尾的操作结果: GetHead (B) = e GetTail (B) = ( ) GetHead (C) = a GetTail (C) = ((b, c, d)) GetHead (D) = A GetTail (D) = (B, C) 由于 (B, C) 为非空广义表,令F = (B, C),则可以继续分解得到: GetHead (F) = B GetTail (F) = (C)
42 5.4.2 广义表的存储结构 由于广义表(a1, a2, … , an)中的数据元素可以具有不同的结构(或是原子,或是广义表),因此很难用顺序结构表示,通常采用链式存储结构。在这种结构中,需要两种结构的结点。