520 likes | 690 Views
第 5 ç« æ•°ç»„å’Œå¹¿ä¹‰è¡¨. 5.1 数组的定义和è¿ç®— 5.2 数组的顺åºå˜å‚¨å’Œå®žçŽ°  5.3 特殊矩阵的压缩å˜å‚¨ 5.4 广义表. 1. 数组的定义和è¿ç®—. 数组是 n(n > 1) 个相åŒç±»åž‹æ•°æ®å…ƒç´ a 0 ,a 1 ,…,a n-1 æž„æˆçš„有é™åºåˆ— , 且该有é™åºåˆ—å˜å‚¨åœ¨ä¸€å—地å€è¿žç»çš„内å˜å•å…ƒä¸ã€‚ 数组的定义类似于采用顺åºå˜å‚¨ç»“æž„çš„çº¿æ€§è¡¨ï¼Œæ˜¯çº¿æ€§è¡¨åœ¨ç»´æ•°ä¸Šçš„æ‰©å¼ ï¼Œä¹Ÿå°±æ˜¯çº¿æ€§è¡¨ä¸çš„å…ƒç´ åˆæ˜¯ä¸€ä¸ªçº¿æ€§è¡¨ã€‚
E N D
第5章 数组和广义表 5.1 数组的定义和运算 5.2 数组的顺序存储和实现 5.3 特殊矩阵的压缩存储 5.4 广义表
1. 数组的定义和运算 • 数组是n(n>1)个相同类型数据元素a0,a1,…,an-1构成的有限序列,且该有限序列存储在一块地址连续的内存单元中。 • 数组的定义类似于采用顺序存储结构的线性表,是线性表在维数上的扩张,也就是线性表中的元素又是一个线性表。 • n维数组,bi是第i维的长度,则n维数组共有 个数据元素,每个元素受n个关系的制约,就单个关系而言,这n个关系仍然是线性的。
数组具有以下性质: (1)数组中的数据元素数目固定。一旦定义了一个 数组,其数据元素数目不再有增减变化。 (2)数组中的数据元素具有相同的数据类型。 (3)数组中的每个数据元素都和一组惟一的下标值对 应。 (4)数组是一种随机存储结构。可随机存取数组中的 任意数据元素。 • 数组的基本操作 (1)取值 Value(A,&e, index1,..., indexn) (2)赋值Assign(&A, e, index1,…, indexn)
2.数组的存储结构 • 一维数组中, LOC(a0)确定, 每个数据元素占用L个存储单元,则任一数据元素ai的存储地址LOC(ai)就可由以下公式求出: LOC(ai)=LOC(a0)+i*L (0≤i≤n-1) • 二维数组,由于计算机的存储结构是线性的,如何用线性的存储结构存放二维数组元素就有一个行/列次序排放问题。
Am×n看成m个行向量的线性表 • Am×n看成n个列向量的线性表
以行序为主序的存储方式 (BASIC, C, PASCAL) a00, …, a0,n-1, a10, …, a1,n-1, …, am-1,0, …, am-1,n-1 • LOC(aij)=LOC(a00)+(i*n+j)*L,其中n为列数 • 以列序为主序的存储方式 (FORTRAN) a00, …, am-1,0, a01, …, am-1,1, …, a0,n-1, … am-1,n-1 LOC(aij)=LOC(a00)+(j*m+i)*L , 其中m为行数
n维数组 每一元素对应下标(j1, j2, …, jn),0≤ji≤bi-1,bi为第i维的长度。以行序为主序。 LOC(j1, j2, …, jn) = LOC(0,0,…,0) + (bn×…×b2×j1 + bn×…b3×j2 + … + bnjn-1 + jn) L
例: 对二维数组float a[5][4]计算: (1)数组a中的数组元素数目; (2)若数组a的起始地址为2000,且每个数组元素长度为32位(即4个字节),数组元素a[3][2]的内存地址。 元素数目共有5*4=20个。 C语言采用行序为主序的存储方式,则有: • LOC(a3,2)=LOC(a0,0)+(i*n+j)*k • =2000+(3*4+2)*4=2056。
3. 矩阵的压缩存储 • 特殊矩阵(对称矩阵、三角矩阵、对角矩阵) 特殊矩阵是指非零元素或零元素的分布有一定规律的矩阵,为了节省存储空间,特别是在高阶矩阵的情况下,可以利用特殊矩阵的规律,对它们进行压缩存储,也就是说,使多个相同的非零元素共享同一个存储单元,对零元素不分配存储空间。
对称矩阵的压缩存储 若一个n阶方阵A中的元素满足ai,j=aj,i(1≤i,j≤n),则称其为n阶对称矩阵。 (1)只存储对称矩阵中上三角或下三角中的元素, (2)将n2个元素压缩存储到n(n+1)/2个元素的空间中,以一个一维数组作为A的存储空间。
n2个元素←→ n(n+1)/2个元素 aij(1≤i,j≤n)←→ B[n(n+1)/2] • 1+2+…+(i-1)+j-1; • aij←→ aji
上三角矩阵的压缩存储 B[n(n+1)/2+1] (i-1)*n-i(i-1)/2+(i-1)+j-(i-1) -1= (i-1)(2n-i+2)/2+j-i
对角矩阵 :所有的非零元都集中在以主对角线为中心 的带状区域内。 半带宽为b的对角矩阵
三对角矩阵 B[3n-2]; k=2(i-1)+j-1; ①共有3n-2个非零元。 ②主对角线左下方的元素下标有关系式:i=j+1 k = 3(i-1)-1+1-1 = 3(i-1)-1 = 2(i-1)+i-2 = 2(i-1)+j-1. ③主对角线上的元素下标有关系式:i=j k = 3(i-1)-1+2-1 = 3(i-1) = 2(i-1)+i-1 = 2(i-1)+j-1. ④主对角线右上方的元素下标有关系式:i=j-1 k = 3(i-1)-1+3-1 = 3(i-1)+1 = 2(i-1)+i = 2(i-1)+j-1.
稀疏矩阵 非零元较零元少,且没有一定规律。 m×n的矩阵,有t个非零元, 矩阵的稀疏因子:δ=t/(m*n),当δ≤0.05时为稀疏矩阵。 压缩存储,只存储非零元 ①三元组顺序表(i,j,ai,j) ②行逻辑链接的顺序表 ③十字链表 • ((1,2,12),(1,3,9),(3,1,-3),(3,6,14), • (4,3,24),(5,2,18),(6,1,15),(6,4,-7))
#define MaxSize 100 /*矩阵中非零元素最多个数*/ typedef struct { int i; /*行号*/ int j; /*列号*/ ElemType e; /*元素值*/ } Triple; /*三元组定义*/ typedef struct { int rows; /*行数*/ int cols; /*列数*/ int nums; /*非零元素个数*/ Triple data[MaxSize+1]; /*data[0]未用*/ } TSMatrix; /*三元组顺序表定义*/
用三元组表实现稀疏矩阵的转置运算 一个6*7的矩阵A,以行序为主序顺序排列
Status TransposeSMatrix(TSMatrix A, TSMatrix &B) { B.rows=A.cols; B.cols=A.rows; B.nums=A.nums; if (B.nums!=0){ q = 1; /*q为B.data的下标*/ for (col=1; col<=A.cols; col++) for (p=1; p<=A.nums; p++) /*p为A.data的下标*/ if (A.data[p].j == col){ B.data[q].i = A.data[p].j; B.data[q].j = A.data[p].i; B.data[q].e = A.data[p].e; q++; } } return OK; }
效率分析 1、主要时间消耗在查找M.data[p].j=col的元素,由两重循环完成: for(col=1; col<=M.nu; col++) 循环次数=nu {for(p=1; p<=M.tu; p++) 循环次数=tu 所以该算法的时间复杂度为O(nu*tu) ----即M的列数与M中非零元素的个数之积 最恶劣情况:M中全是非零元素,此时tu=mu*nu, 时间复杂度为 O(nu2*mu ) 注:若M中基本上是非零元素时,即使用非压缩传统转置算法的时间复杂度也不过是O(nu*mu)(程序见下面) 结论:压缩转置算法不能滥用。 前提:仅适用于非零元素个数很少(即tu<<mu*nu)的情况。
方法1时间复杂度: O(cols*nums) 当非零元个数nums和cols*rows同数量级时, O(rows*cols2) 仅适用于nums<<rows*cols. 常规存储方式时, 实现矩阵转置的经典算法如下: for(i=0; i<m; i++) for (j=0; j< n; j++) T[i][ j] = M[j][i]; O(cols*rows)
p 1 ① (1, 3, -3) 2 ② (1, 6, 15) 3 ③ (2 ,1, 12) ④ 4 (2, 5, 18) ⑤ (3, 1, 9) ⑥ (3, 4, 24) ⑦ (4, 6, -7) ⑧ (5, 3, 14) 方法2快速转置 思路:依次把a.data中的元素直接送入b.data的恰当位置上(即M三元组的p指针不回溯)。 q 三 元 组 表 b.data 三 元 组 表 a.data 3 5 关键:怎样寻找b.data的“恰当”位置?
设计思路: 如果能预知M矩阵每一列(即T的每一行)的非零元素个数,又能预知第一个非零元素在b.data中的位置,则扫描a.data时便可以将每个元素准确定位(因为已知若干参考点)。 请注意a.data特征:每列首个非零元素必定先被扫描到。 技巧:利用带辅助向量的三元组表,它正好携带每行(或列)的非零元素个数 NUM(i)以及每行(或列)的第一个非零元素在三元组表中的位置POS(i)等信息。 请回忆: 规律:POS(1)=1 POS(i)=POS(i-1)+NUM(i-1) 不过我们需要的是按列生成的M矩阵的辅助向量。
0129 0 0 0 00 0 0 0 0 -3 0 0 0 14 0 0 0 24 0 0 0 018 0 0 0 0 150 0 -7 0 0 令:M中的列变量用col表示;num[ col ]:存放M中第col 列中非0元素个数,cpot[ col ]:存放M中第col列的第一个非0元素的位置,(即b.data中待计算的“恰当”位置所需参考点) col 1 2 3 4 5 6 M 3 5 7 8 8 规律: cpot(1)=1 cpot[col]=cpot[col-1] + num[col-1] 讨论:按列优先的辅助向量求出后,下一步该如何操作? 由a.data中每个元素的列信息,即可直接查出b.data中的重要参考点之位置,进而可确定当前元素之位置!
Status FastTranSMatrix(TSMatrix A, TSMatrix &B) { B.rows=A.cols; B.cols=A.rows; B.nums=A.nums; if(B.nums){ for(col=1; col<=A.nums; ++col) num[col] = 0; for(t=1; t<=A.nums; ++t) ++num[A.data[t].j]; cpot[1] = 1; for(col=2; col<=A.nums; ++col) cpot[col] = cpot[col-1]+num[col-1]; for(p=1; p<=A.nums; ++p){ col = A.data[p].j; q = cpot[col]; B.data[q].i = A.data[p].j; B.data[q].j = A.data[p].i; B.data[q].e = A.data[p].e; ++cpot[col]; } } return OK; } O(cols+nums) 当nums和cols*rows同数量级时 O(rows*cols)
row col e 1 3 -3 cpot[1] cpot[2] cpot[2] cpot[3] cpot[3] cpot[4] cpot[6] 1 2 3 4 5 6 7 8 1 6 15 2 1 12 2 5 18 3 1 9 3 4 24 4 6 -7 6 3 14
快速转置算法的效率分析: 1.与常规算法相比,附加了生成辅助向量表的工作。增开了2个长度为列长的数组(num[ ]和cpos[ ])。 2.从时间上,此算法用了4个并列的单循环,而且其中前3个单循环都是用来产生辅助向量表的。 for(col = 1; col <=M.nu; col++) 循环次数=nu; for( i = 1; i <=M.tu; i ++) 循环次数=tu; for(col = 2; col <=M.nu; col++) 循环次数=nu; for( p =1; p <=M.tu ; p ++ ) 循环次数=tu; 该算法的时间复杂度=(nu*2)+(tu*2)=O(nu+tu) 讨论:最恶劣情况是tu=nu*mu(即矩阵中全部是非零元素), 而此时的时间复杂度也只是O(mu*nu),并未超过传统转置算法的时间复杂度。 传统转置:O(mu*nu) 压缩转置:O(mu*tu) 压缩快速转置:O(nu+tu)——牺牲空间效率换时间效率。 小结:
row col val down right 数组的链式存储结构 2.三元组线性表的链式存储结构称为十字链表 当矩阵非零元的位置或个数经常变化时,采用链式存储结构更为恰当. 1). 链表结点结构 链域down:链接同一列中下一个非零元; 链域right:链接同一行中下一个非零元; 稀疏矩阵中同一列的非零元通过down链接成一个链表,同一行的非零元通过right链接成一个链表。
∧ ∧ 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)理解数组和一般线性表之间的差异。 (2)重点掌握数组的顺序存储结构和元素地址计算方法。 (3)掌握各种特殊矩阵如对称矩阵、上、下三角矩阵和对角矩阵的压缩存储方法。 (4)掌握稀疏矩阵的各种存储结构以及基本运算实现算法。 (5)灵活运用数组这种数据结构解决一些综合应用问题。
练习: 1. 将一个A[1..100, 1..100]的三对角矩阵,按行优先存入一维数组B[1..298],A中元素a66,65在B数组中的位置k=?。 在主对角线左下方,65*3-1+1 = 195。
2. 有二维矩阵A[m][n],其元素类型为int,每行每列按从小到大有序,试给出一个算法求数组中值为x的元素的行号和列号,前提是x在A中存在,要求比较次数不超过m+n次。 void search(int **A, int x, int &i, int &j) { i = 0; j = n-1; while(A[i][j] != x){ if(A[i][j] > x) j--; else i++; } }
3. ()编写算法,求串s所含不同字符的总数和每种字符的个数。 void StrCount(SString s) { SString r; int *num = new int[s[0]]; for(i=1; i<=s[0]; i++) num[i] =0; r[1] = s[1]; num[1] = 1; r[0] = 1; for(i=2; i<=s[0]; i++){ j = 1; while( s[i]!=r[j] && j<=r[0]) ++j ++num[j]; if(j>r[0]) { r[j] = s[i]; ++r[0]; } } }
void StrCount(SString s) { int num[256]; for(i=0; i<256; ++i) num[i] = 0; for(i=0; i<s[0]; ++i) ++num[int(s[i])]; j = 0; for(i=0; i<256; ++i) if(num[i]) ++j; }
5.4 广义表 1. 广义表的定义(列表) 广义表是线性表的推广,是由零个或多个单元素或子表所 组成的有限序列。 LS=(a1,a2,…,ai,…,an) ai:是单个数据元素,则ai是广义表的原子;如果ai是一个广义表,则ai是广义表的子表。 规定用小写字母表示原子,用大写字母表示广义表的 表名。 • 广义表与线性表的区别: 线性表的成份都是结构上不可分的单个数据元素,而广 义表的成份即可以是单元素,也可以是有结构的表,其定义 是递归的定义。
广义表的长度:广义表中所含元素的个数n,n≥0。广义表的长度:广义表中所含元素的个数n,n≥0。 • 广义表的深度:广义表展开后所含的括号的最大层数。(广义表中括号嵌套的最大次数,广义表中括弧的重数)
特点 • 有次序性 • 有长度 • 有深度 • 可递归 • 可共享 一个直接前驱和一个直接后继 =表中元素个数 =表中括号的重数 自己可以作为自己的子表 可以为其他广义表所共享 特别提示:任何一个非空表,表头可能是原子,也可能是列表;但表尾一定是列表。
D=( )空表,长度为0,深度为1。 • A=(a, (b, c)) 长度为2,第一个元素为原子a,第二个元素为子表(b, c),深度为2。 • B=(A, A, D) 长度为3, 其前两个元素为表A, 第三个元素为空表D,深度为3。 • C=(a, C) 长度为2递归定义的广义表,C相当于无穷表C=(a,(a, (a,…))),深度无限。 递归表的深度是无穷值,长度是有限值。 • F=((a, (a, b), ((a, b), c))) 长度为1,深度为4。
广义表的表头(Head)和表尾(Tail): 当广义表非空时,称第一个元素a1为广义表的表头, 其余元素组成的表(a2, a3, …,an)称为广义表的表尾。 表头可能是原子,也可能是广义表,但表尾一定是广 义表。 • 广义表的图形表示 用方框表示原子,用圆圈表示广义表。
A=( ); B=(e); C=(a, (b, c, d)) D=(A, B, C); E=((a, (a, b), ((a, b), c)))
2. 广义表的基本操作 • 取表头 GetHead(LS) = a1。 • 取表尾 GetTail(LS) = (a2,a3,…,an)。 ①B=(e) GetHead(B) = e; GetTail(B) = ( ). ②A=(a, ((b, c), d, e)) GetHead( GetHead( GetTail(A))) = GetHead( GetHead( (((b, c), d, e)) )) = GetHead( ((b, c), d, e) ) = (b, c). ③A=( ); B = ( ( ) ) A空表,长度0,深度1,无表头和表尾; B长度1,深度2,表头( ),表尾( )。
3. 广义表的存储结构 广义表是一种递归的数据结构,因此很难为每个广义表分配固定大小的存储空间,所以其存储结构只好采用动态链式结构。 • 广义表的头尾链表存储表示 • 广义表的扩展线性链表存储表示
原子结点 表结点 • 广义表的头尾链表存储表示 hp:指向该表的表头;tp:指向该表的表尾。 typedef enum {ATOM, LIST} ElemTag; //ATOM=0,表示原子;LIST=1,表示子表 typedef struct GLNode { ElemTag tag; //标志位tag用来区别原子结点和表结点 union{ AtomType atom; //原子结点的值域atom struct { struct GLNode * hp, *tp; } ptr;//表结点的指针域 }; }*GList;
C=(a, (b, c, d)) • A=( ) A = NULL; (a, (b, c, d)) ((b, c, d)) (b, c, d) (c, d) (d)
(a, b, c) (b, c) ( c) • D=( ); A= (a, b, c); B=(A, A, D); C = (a, C). (A, D) (D)=(( )) (A, A, D)
孩子兄弟表示法 • 在孩子兄弟表示法中,也有两种结点:一种是有孩子的表结点,另一种是无孩子的元素结点,用以表示单元素。在表结点中应该包括一个指向第一个孩子的指针和指向兄弟的指针;而在元素结点中应该包括一个指向兄弟的指针和单元素的元素值。为了区分这两类结点,在结点中还要设置一个标志域,如果标志为1,则表示该结点为表结点;如果标志为0,则表示该结点为元素结点。
广义表的扩展线性链表存储表示 • (孩子兄弟表示法带表头结点) 原子结点 表结点 hp指向第一个元素,tp指向与本元素同一层的下一元素。
C=(a, (b, c, d)) (b, c, d)