5.4稀疏矩阵压缩存储下
“列序”递增转置法的思考:采用稀疏矩阵存储方法,可否降低时间复杂度?(提示: 通过降低对稀疏矩阵三元组的扫描次数实现)
方法二:“一次定位快速转置”法
【算法思想】
在方法一中,为了使转置后矩阵的三元组表 B 仍按“行序递增”存放,必须 多次扫描被转置矩阵的三元组表 A,以保证按被转置矩阵列序递增进行转置。因此要通过双 重循环来完成。改善算法的时间性能,必须去掉双重循环,使整个转置过程通过一重循环来完成,即只对被转置矩阵的三元组表 A 扫描一次,就使 A 中所有非零元的三元组“一次定位”直接放到三元组表 B 的正确位置上。
为了能将被转置三元组表A 中的元素一次定位到三元组表B的正确位置上,需要预先计算以下数据:
- ( 1 )待转置矩阵三元组表 A 每一列中非零元素的总个数(即转置后矩阵三元组表 B 的每 一行中非零元素的总个数)。
- ( 2 )待转置矩阵每一列中第一个非零元素在三元组表 B 中的正确位置(即转置后矩阵每一行中第一个非零元素在三元组表 B 中的正确位置)。
为此,需要设两个数组分别为 num[]和 position[]。其中 num[ col]用来存放三元组表 A 第 col 列中非零元素总个数(三元组表 B 第 col 行中非零元素的总个数)。 position[ col]用来存放转置前三元组表 A 中第 col 列(转置后三元组表 B 中第 col 行)中第一个非零元素在三元组表 B 中的存储位置(下标值)。
num[ col]的计算方法:将三元组表A扫描一遍,对于其中列号为col的元素,给相应的num 数组中下标为 col 的元素加 1
说明:在 num[ col]的计算中,采用了“数组下标计数法”。
position[ col]的计算方法:
① position[ 1 ]=1 ,表示三元组表A 中,列值为 1的第一个非零元素在三元组表B 中的下标值;
② position[ col]=position[ col-1 ]+num[ col-1 ]。其中 2 ≤ col≤ A.n。
思考题:选票统计问题。由 10 位候选人参与选举,编号分别为 1 ,2 ,…, 10 。现有 20 000 张有效选票(选票上均标记 1 ~10 之间的数字表明所选候选人),统计各位候选人的得票数。 说明方法,给出程序
【提示】一次扫描 2 万张选票,利用数组统计候选人得票数。
“一次定位快速转置”的具体做法: position[ col]的初值为三元组表 A 中第 col 列(三元 组表 B 的第 col 行)中第一个非零元素的正确位置,当三元组表 A 中第 col 列有一个元素加入 到三元组表 B时,则 position[ col]=position[ col]+1 ,即:使 position[ col]始终指向三元组表A中第 col 列中下一个非零元素在三元组表 B 中的正确存放位置(下标值)。
【算法描述】
FastTransposeTSMatrix (TSMatrix A, TSMatrix * B)
{ /*基于矩阵的三元组表示,采用“一次定位快速转置”法,将矩阵 A 转置为矩阵 B*/
int col , t , p,q;
int num[MAXSIZE], position[MAXSIZE] ;
B->len= A.len ;
B->n= A.m ;
B->m= A.n ;
if(B->len)
{
for(col=1;col<=A.n;col++)
num[col]=0;
for(t=1;t<=A.len;t++)
num[A.data[t].col]++; /*采用数组下标计数法计算每一列的非零元素的个数*/
position[1]=1;
for(col=2;col<=A.n;col++) /*求 col 列中第一个非零元素在 B.data[ ]中的正确位置*/
position[col]=position[col-1]+num[col-1];
for(p=1;p<=A.len;p++)/*将被转置矩阵的三元组表 A 从头至尾扫描一次,实现矩阵转置*/
{
col=A.data[p].col;
q=position[col];
B->data[q].row=A.data[p].col;
B->data[q]..col=A.data[p].row;
B->data[q].e=A.data[p].e;
position[col]++; /* position[col]加1,指向下一个列号为col的非零元素在三元组表B中的存放位置*/
}/*end of for*/
}
}
【算法分析】
“一次定位快速转置”算法的时间主要耗费在 4 个并列的单循环上,这 4 个 并列的单循环分别执行了A.n,A.len,A.n-1,A.len次,因而总的时间复杂度为O( A.n)+O( A.len)+ O( A.n)+ O( A.len),即为 O( A.n +A.len),当待转置矩阵 M 中非零元素个数接近于 A.m ×A.n 时,其时间复杂度接近于经典算法的时间复杂度 O( A.m ×A.n)。
对例子给出的 A 矩阵 A.m=100 ,A.n=500 ,A.len=100 ,采用 “列序“递增转置法的时间耗费为 A.n× A.len =50 000 次 。 而采用一次定位快速转置法的时间耗费为 A.n+A.len+A.n+A.len=1200 次。显然,“一次定位快速转置”算法的时间效率要高得多。在 时间性能上优于递增转置法,但在空间耗费上增加了两个辅助向量空间,即 num[ 1 … A.n],position[ 1 … A.n](本例中为 1000),由此可见,算法在时间上的节省是以更多的存储空间为代价的。
2.稀疏矩阵的链式存储结构:十字链表
与用二维数组存储稀疏矩阵相比较,用三元组表表示法的稀疏矩阵不仅节约了空间,而且 使得矩阵某些运算的时间效率优于经典算法。但是当需进行矩阵加法、减法和乘法等运算时, 有时矩阵中非零元素的位置和个数会发生很大的变化。如 A =A+B,将矩阵 B 加到矩阵 A 上, 此时若用三元组表表示法,势必会为了保持三元组表“以行序为主序”而大量移动元素。
为了避免大量移动元素,介绍稀疏矩阵的链式存储法———十字链表,它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的各种运算。
( 1 )十字链表的存储表示
在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了( row,col,value)以外, 还要有以下两个链域:
- right:用于链接同一行中的下一个非零元素
- down:用于链接同一列中的下一个非零元素。
在十字链表中,同一行的非零元素通过 right 域链接成一个单链表。同一列的非零元素通 过 down 域链接成一个单链表。
这样,矩阵中任一非零元素 M[ i][ j]所对应的结点既处在第 i 行的行链表上,又处在第 j 列的列链表上,这好像是处在一个十字交叉路口上,所以称其为十字链表。同时再附设一个存放所有行链表的头指针的一维数组和一个存放所有列链表的头指针的一维数组。
整个十字链表的结构如图所示。
( 2 )十字链表的类型定义
typedef struct OLNode
{
int row, col; /*非零元素的行和列下标*/
ElementType value;
struct OLNode * right,*down; /*非零元素所在行表、列表的后继链域*/
}OLNode; *OLink;
typedef struct
{
OLink * row_head, *col_head; /*行、列链表的头指针向量*/
int m, n, len; /*稀疏矩阵的行数、列数、非零元素的个数*/
}CrossList;