第 5章 数组和稀疏矩阵
5.1 数组
5.2 稀疏矩阵本章小结
5.1.1 数组的基本概念数组是 n(n> 1)个相同类型数据元素 a1,a2,…,an构成的有限序列,且该有限序列存储在一块地址连续的内存单元中 。
由此可见,数组的定义类似于采用顺序存储结构的线性表 。
数组具有以下性质:
(1)数组中的数据元素数目固定 。 一旦定义了一个数组,其数据元素数目不再有增减变化 。
(2)数组中的数据元素具有相同的数据类型 。
(3)数组中的每个数据元素都和一组惟一的下标值对应 。
(4)数组是一种随机存储结构 。 可随机存取数组中的任意数据元素 。
5.1.2 数组的存储结构在一维数组中,一旦 a1的存储地址 LOC(a1)确定,并假设每个数据元素占用 k个存储单元,则任一数据元素
ai的存储地址 LOC(ai)就可由以下公式求出:
LOC(ai)=LOC(a1)+(i-1)*k (0≤i≤n)
上式说明,一维数组中任一数据元素的存储地址可直接计算得到,即一维数组中任一数据元素可直接存取,因此,一维数组是一种随机存储结构 。 同样,二维及多维数组也满足随机存储特性 。

nmmm
n
n
nm
aaa
aaa
aaa
A
,2,1,
,22,21,2
,12,11,1
对于一个 m行 n列的二维数组 Am× n,有:
将 Am*n简记为 A,A是这样的一维数组:
A=(a1,a2,…,ai…,am)
其中,ai=(ai,1,ai,2,…,ai,n) (1≤j≤m)。
显然,二维数组同样满足数组的定义 。 一个二维数组可以看作是每个数据元素都是相同类型的一维数组的一维数组 。 以此类推,任何多维数组都可以看作一个线性表,这时线性表中的每个数据元素也是一个线性表 。 多维数组是线性表的推广 。
对于二维数组来说,由于计算机的存储结构是线性的,如何用线性的存储结构存放二维数组元素就有一个行/列次序排放问题 。
以行序为主序的存储方式:即先存储第 1行,然后紧接着存储第 2行,最后存储第 m行 。 此时,二维数组的线性排列次序为:
a1,1,a1,2,…,a1,n,a2,1,a2,2,…,a2,n,…,am,1,am,2,… am,n
对一个已知以行序为主序的计算机系统中,当二维数组第一个数据元素 a1,1的存储地址 LOC(a1,1)和每个数据元素所占用的存储单元 k确定后,则该二维数组中任一数据元素 ai,j的存储地址可由下式确定:
LOC(ai,j)=LOC(a1,1)+[(i-1)*n+(j-1)]*k
其中 n为列数 。
同理可推出在以列序为主序的计算机系统中有:
LOC(ai,j)=LOC(a1,1)+[(j-1)*m+(i-1)]*k
其中 m为行数 。
例 5.1 对二维数组 float a[5][4]计算:
(1)数组 a中的数组元素数目;
(2)若数组 a的起始地址为 2000,且每个数组元素长度为 32位 (即 4个字节 ),数组元素 a[3][2]的内存地址 。
解:由于 C语言中数组的行,列下界均为 0,
该数组行上界为 5-1=4,列上界为 4-l=3,所以该数组的元素数目共有 (4-0+1)*(3-0+1)=5*4=20个 。
又由于 C语言采用行序为主序的存储方式,
则有:
LOC(a3,2)=LOC(a0,0)+(i*n+j)*k
=2000+(3*4+2)*4=2056
5.1.3 特殊矩阵的压缩存储特殊矩阵是指非零元素或零元素的分布有一定规律的矩阵,为了节省存储空间,特别是在高阶矩阵的情况下,可以利用特殊矩阵的规律,对它们进行压缩存储,
也就是说,使多个相同的非零元素共享同一个存储单元,对零元素不分配存储空间 。
特殊矩阵的主要形式有对称矩阵,对角矩阵等,
它们都是方阵,即行数和列数相同 。
1,对称矩阵的压缩存储若 一 个 n 阶方阵 A[n][n] 中 的 元 素 满 足
ai,j=aj,i(0≤i,j≤n-1),则称其为 n阶对称矩阵 。
由于对称矩阵中的元素关于主对角线对称,因此在存储时可只存储对称矩阵中上三角或下三角中的元素,使得对称的元素共享一个存储空间 。 这样,就可以将 n2个元素压缩存储到个元素的空间中 。 不失一般性,我们以行序为主序存储其下三角 (包括对角线 )的元素 。
n2个元素 ←→ n(n+1)/2个元素
A[0..n-1,0..n-1] ←→ B[0..n(n+1)/2 -1]
a[i][j] ←→ b[k]
2
1)i(i?
k=
+ j i≥j
+ i i< j
2
1)j(j?
上三角矩阵:
2
)12(* ini
2
)1(?nn
k=
+ i –j i≤j时
i> j时下三角矩阵:
jii +2 )1(*?
2
)1(?nn
k=
i≥j时
i< j时
2,对角矩阵的压缩存储若一个 n阶方阵 A满足其所有非零元素都集中在以主对角线为中心的带状区域中,则称其为 n阶对角矩阵 。 其主对角线上下方各有 b条次对角线,称 b为矩阵半带宽,(2b+1)为矩阵的带宽 。 对于半带宽为
b(0≤b≤(n-1)/2)的对角矩阵,其 |i-j|≤b的元素 ai,j不为零,
其余元素为零 。 下图所示是半带宽为 b的对角矩阵示意图 。
..,
b 条
b 条
0
0
...
半带宽为 b的对角矩阵当 b= 1时称为三对角矩阵。
其压缩地址计算公式如下:
k=2i+j
A ←→ B
a[i][j] ←→ b[k]
例 5.2 按行优先顺序和按列优先顺序列出四维数组 A[2][2][2][2]所有元素在内存中的存储次序 。
解,按行优先的存储次序,
A[0][0][0][0],A[0][0][0][1],A[0][0][1][0],
A[0][0][1][1],A[0][1][0][0],A[0][1][0][1],
A[0][1][1][0],A[0][1][1][1],A[1][0][0][0],
A[1][0][0][1],A[1][0][1][0],A[1][0][1][1],
A[1][1][0][0],A[1][1][0][1],A[1][1][1][0],
A[1][1][1][1]
按列优先的存储次序,
A[0][0][0][0],A[1][0][0][0],A[0][1][0][0],
A[1][1][0][0],A[0][0][1][0],A[1][0][1][0],
A[0][1][1][0],A[1][1][1][0],A[0][0][0][1],
A[1][0][0][1],A[0][1][0][1],A[1][1][0][1],
A[0][0][1][1],A[1][0][1][1],A[0][1][1][1],
A[1][1][1][1]
例 5.3 对于二维数组 A[m][n],其中 m≤80,n≤80,先读入 m和 n,然后读该数组的全部元素,对如三种情况分别编写相应函数,
(1)求数组 A靠边元素之和 ;
(2)求从 A[0][0]开始的行,列互不相邻的各元素之和 ;
(3)当 m=n时,分别求两条对角线上的元素之和,
否则打印出 m≠n的信息 。
解,(1)对应算法如下:
void proc1(ElemType A[][n])
{ int s=0,i,j;
for (i=0;i<m;i++) /*第一列 */
s=s+A[i][0];
for (i=0;i<m;i++) /*最后一列 */
s=s+A[i][n-1];
for (j=0;j<n;j++) /*第一行 */
s=s+A[0][j];
for (j=0;j<n;j++) /*最后一行 */
s=s+A[m-1][j];
s=s-A[0][0]-A[0][n-1]-A[m-1][0]-A[m-1][n-1];
/*减去 4个角的重复元素值 */
printf("s=%d\n",s);
}
(2)对应算法如下:
void proc2(maxix A)
{ int s=0,i=0,j=0;
do
{ do
{ s=s+A[i][j];
j=j+2; /*跳过一列 */
} while (j<n);
i=i+1; /*下一行 */
if (j==0) j=1;
else j=0;
} while (i<m);
printf("s=%d\n",s);
}
(3)对应算法如下:
void proc3(maxix A)
{ int i,s=0;
if (m!=n) printf("m≠n");
else
{ for (i=0;i<m;i++) s=s+A[i][i];
/*求第一条对角线之和 */
for (i=0;i<n;i++) s=s+A[n-i-1][i];
/*累加第二条对角线之和 */
s-=A[n/2][n/2];
printf("s=%d\n",s);
}
}
5.2 稀疏矩阵一个阶数较大的矩阵中的非零元素个数 s相对于矩阵元素的总个数 t十分小时,即 s<<t时,称该矩阵为稀疏矩阵 。 例如一个 100× 100的矩阵,若其中只有 100个非零元素,就可称其为稀疏矩阵 。
5.2.1 稀疏矩阵的三元组表示稀疏矩阵的压缩存储方法是只存储非零元素 。
由于稀疏矩阵中非零元素的分布没有任何规律,所以在存储非零元素时还必须同时存储该非零元素所对应的行下标和列下标 。 这样稀疏矩阵中的每一个非零元素需由一个三元组 (i,j,ai,j)惟一确定,稀疏矩阵中的所有非零元素构成三元组线性表 。
假设有一个 6× 7阶稀疏矩阵 A(为图示方便,我们所取的行列数都很小 ),A中元素如下图所示 。 则对应的三元组线性表为:
((0,2,1),(1,1,2),(2,0,3),(3,3,5),
(4,4,6),(5,5,7),(5,6,4))
4700000
0060000
0005000
0000003
0000020
0000100
76
A
一个稀疏矩阵 A
若把稀疏矩阵的三元组线性表按顺序存储结构存储,则称为稀疏矩阵的三元组顺序表 。 则三元组顺序表的数据结构可定义如下:
#define MaxSize 100 /*矩阵中非零元素最多个数 */
typedef struct
{ int r; /*行号 */
int c; /*列号 */
ElemType d; /*元素值 */
} TupNode; /*三元组定义 */
typedef struct
{ int rows; /*行数值 */
int cols; /*列数值 */
int nums; /*非零元素个数 */
TupNode data[MaxSize];
} TSMatrix; /*三元组顺序表定义 */
其中,data域中表示的非零元素通常以行序为主序顺序排列,它是一种下标按行有序的存储结构 。
这种有序存储结构可简化大多数矩阵运算算法 。
下面的讨论假设 data域按行有序存储 。
(1)从一个二维矩阵创建其三元组表示以行序方式扫描二维矩阵 A,将其非零的元素插入到三元组 t的后面 。 算法如下:
void CreatMat(TSMatrix &t,ElemType A[M][N])
{ int i,j; t.rows=M;t.cols=N;t.nums=0;
for (i=0;i<M;i++)
{ for (j=0;j<N;j++)
if (A[i][j]!=0) /*只存储非零元素 */
{ t.data[t.nums].r=i;t.data[t.nums].c=j;
t.data[t.nums].d=A[i][j];t.nums++;
}
}
}
(2)三元组元素赋值先在三元组 t中找到适当的位置 k,将 k~ t.nums个元素后移一位,将指定元素 x插入到 t.data[k]处 。 算法如下:
int Value(TSMatrix &t,ElemType x,int rs,int cs)
{ int i,k=0;
if (rs>=t.rows || cs>=t.cols) return 0;
while (k<t.nums && rs>t.data[k].r) k++; /*查找行 */
while (k<t.nums && cs>t.data[k].c) k++; /*查找列 */
if (t.data[k].r==rs && t.data[k].c==cs)
t.data[k].d=x; /*存在这样的元素
else /*不存在这样的元素时插入一个元素 */
{ for (i=k;i<t.nums;i++)
{ t.data[i+1].r=t.data[i].r;
t.data[i+1].c=t.data[i].c;
t.data[i+1].d=t.data[i].d;
}
t.data[k].r=rs;t.data[k].c=cs;t.data[k].d=x;
t.nums++;
}
return 1;
}
(3)将指定位置的元素值赋给变量先在三元组 t中找到指定的位置,将该处的元素值赋给 x。 算法如下:
int Assign(TSMatrix t,ElemType &x,int rs,int cs)
{ int k=0;
if (rs>=t.rows || cs>=t.cols) return 0;
while (k<t.nums && rs>t.data[k].r) k++;
while (k<t.nums && cs>t.data[k].c) k++;
if (t.data[k].r==rs && t.data[k].c==cs)
{ x=t.data[k].d; return 1; }
else return 0;
}
(4)输出三元组从头到尾扫描三元组 t,依次输出元素值 。 算法如下:
void DispMat(TSMatrix t)
{ int i;
if (t.nums<=0) return;
printf(\t%d\t%d\t%d\n",t.rows,t.cols,t.nums);
printf(" ------------------\n");
for (i=0;i<t.nums;i++)
printf("\t%d\t%d\t%d\n",t.data[i].r,t.data[i].c,
t.data[i].d);
}
(5)矩阵转置对于一个 m× n的矩阵 Am× n,其转置矩阵是一个
n× m 的矩阵 。 设为 Bn× m,满足 ai,j=bj,i,其中
1≤i≤m,1≤j≤n。 其完整的转置算法如下:
void TranTat(TSMatrix t,TSMatrix &tb)
{ int p,q=0,v; /*q为 tb.data的下标 */
tb.rows=t.cols;tb.cols=t.rows;tb.nums=t.nums;
if (t.nums!=0)
{ for (v=0;v<t.cols;v++)
for (p=0;p<t.nums;p++) /*p为 t.data的下标 */
if (t.data[p].c==v)
{ tb.data[q].r=t.data[p].c;
tb.data[q].c=t.data[p].r;
tb.data[q].d=t.data[p].d;
q++;
}
}
}
以上算法的时间复杂度为 O(t.cols*t.nums),而将二维数组存储在一个 m行 n列矩阵中时,其转置算法的时间复杂度为 O(m*n)。最坏情况是当稀疏矩阵中的非零元素个数 t.nums和 m*n同数量级时,上述转置算法的时间复杂度就为 O(m*n2)。对其他几种矩阵运算也是如此。可见,常规的非稀疏矩阵应采用二维数组存储,只有当矩阵中非零元素个数 s满足 s<<m*n时,方可采用三元组顺序表存储结构。这个结论也同样适用于下面要讨论的三元组的十字链表。
5.2.2 稀疏矩阵的十字链表表示十字链表为稀疏矩阵的每一行设置一个单独链表,
同时也为每一列设置一个单独链表 。
这样稀疏矩阵的每一个非零元素就同时包含在两个链表中,即每一个非零元素同时包含在所在行的行链表中和所在列的列链表中 。 这就大大降低了链表的长度,方便了算法中行方向和列方向的搜索,因而大大降低了算法的时间复杂度 。
(a) 结点结构 (b) 头结点结构对于一个 m× n的稀疏矩阵,每个非零元素用一个结点表示,结点结构可以设计成如下图 (a)所示结构 。 其中 i,j,value分别代表非零元素所在的行号,列号和相应的元素值; down和 right分别称为向下指针和向右指针,分别用来链接同列中和同行中的下一个非零元素结点 。
十字链表中设置行头结点、列头结点和链表头结点。它们采用和非零元素结点类似的结点结构,具体如上图 (b)所示。其中行头结点和列头结点的 i,j域值均为 0;行头结点的 right指针指向该行链表的第一个结点,它的 down指针为空;列头结点的 down指针指向该列链表的第一个结点,它的 right指针为空。行头结点和列头结点必须顺序链接,这样当需要逐行 (列 )
搜索时,才能一行 (列 )搜索完后顺序搜索下一行 (列 ),
行头结点和列头结点均用 link指针完成顺序链接。

4000
0300
2001
B 43
一个稀疏矩阵
1
3 4
0 0
0 3 2
1 2 3
2 3 4
h4
h3
h2
h1
hm
h0 h1
h2 h3
h4
十字链表结点结构和头结点的数据结构可定义如下:
#define M 3 /*矩阵行 */
#define N 4 /*矩阵列 */
#define Max ((M)>(N)?(M):(N)) /*矩阵行列较大者 */
typedef struct mtxn
{ int row; /*行号 */
int col; /*列号 */
struct mtxn *right,*down; /*向右和向下的指针 */
union
{ int value;
struct mtxn *link;
} tag;
} MatNode; /*十字链表类型定义 */
本章小结本章基本学习要点如下:
(1)理解数组和一般线性表之间的差异 。
(2)重点掌握数组的顺序存储结构和元素地址计算方法 。
(3)掌握各种特殊矩阵如对称矩阵,上,下三角矩阵和对角矩阵的压缩存储方法 。
(4)掌握稀疏矩阵的各种存储结构以及基本运算实现算法 。
(5)灵活运用数组这种数据结构解决一些综合应用问题 。
练习题教材中 p113习题 3,5和 6。