一、稀疏矩阵

什么是稀疏矩阵?它没有确切的定义,一般是指矩阵中存储非零元素相对整个矩阵存储的总元素来说很少的矩阵称为稀疏矩阵,这个多少怎么看呢?计算矩阵的稀疏因 K = 非零元个数 矩阵全部元素个数 ≤ 0.05 K=\frac{非零元个数}{矩阵全部元素个数}\leq0.05 K=矩阵全部元素个数非零元个数0.05时称为稀疏矩阵

二、存储方法

由于稀疏矩阵中存在大量零元素,从而占用大量空间,如何对它进行压缩存储?所谓压缩存储是指只存储非零元的信息节省不必要的空间浪费。常用的压缩存储方法有:【三元组法】【十字链表法】

2.1三元组法

所谓三元组法是指用一块相同大小的连续的内存空间(顺序表)或者结点形式(链表),并以按矩阵的行顺序存储矩阵中的每个非零元信息。为确保非零元唯一确定,三元组保存信息时需要保存非零元的所在矩阵中的行号和列号以及元素本身,即一个三元组表示为:(i,j,aij),i是行号,j是列号。一个矩阵的所有非零元的三元组作为结构类型的元素存储在连续的内存空间中或链表形成一个集合称为三元组表。(注:按矩阵的行顺序存储是指矩阵的第一行的第一个非零元存储在三元组表的第一个位置,第二个非零元存储在三元组表的第二个位置
有了三元组表可以唯一确定矩阵中的非零元位置,但却不能唯一确定矩阵,因为矩阵的行数和列数没有指明,因此需要用额外的空间存储矩阵的列数和行数,为了方便再加一个空间来存储矩阵中非零元的总数,其存储方式如下:

//这里以顺序表存储为例,以三元组的方式存储稀疏矩阵
#define MAXSIZE 100
typedef int ElemType;//类型重命名【鉴名知意】
typedef struct {
	int i,j;//非零元的行和列
	ElemType e;//非零元
}Triple;

typedef struct{
	Triple data[MAXSIZE];
	int m,n,nonzeroNums;//矩阵的行m,矩阵的列n,非零元数量nonzeroNums
}TSMatrix

三、稀疏矩阵转置

矩阵的转置是指:由原来的矩阵Mmxn转换为另一个矩阵Tnxm,矩阵M中j行i列的位置的元素存储在矩阵T中i行j列位置上,表示为: T ( i , j ) = M ( j , i ) , 1 ≤ i ≤ n , 1 ≤ j ≤ n T(i,j)=M(j,i),1\leq i\leq n,1\leq j\leq n T(i,j)=M(j,i)1in1jn。这时称矩阵M与矩阵T互为转置矩阵。由其转置的定义可知,把一个矩阵转置需要如下步骤
① 将矩阵M的行数和列数交叉赋给矩阵T,矩阵M的行数和列数分别为矩阵T的列数和行数。
② 将每个三元组表中的三元组的i和j互换。
③ 重新排列三元组之间的次序(三元组法存储矩阵M时是按矩阵的行顺序存储的方式把非零元对应的三元组存储在三元组表中,而转置后非零元的行和列顺序改变了,因此要重新调整三元组表中三元组的次序)

3.1朴素思想

我们最终的目标是: 把转置矩阵的T的非零元按矩阵T的行顺序存储的方式把非零元对应的三元组存储在三元表中。由于矩阵M转置为T后,T的第一行元素对应是原来矩阵M的第一列元素,第二行对应M的第二列…,因此重新排列后的三元组表(转置矩阵T对应的三元组表)里的三元组会先存储的是矩阵M的第一列的非零元对应的三元组,然后是第二列的…直到最后一列。代码如下:

Status TransposeSMatrix(TSMatrix M,TsMatrix *T){
//在实际存储中,存储矩阵的二维数组一般是从0行、0列开始的,而不是和逻辑上的矩阵一样从1行1列开始
//当然也可以自定义存储矩阵的二维数组和逻辑的一致,从1行1列开始,下面是以0行0列开始的为例。
	//矩阵M的行、列数变为矩阵T的列、行数
	T->m=M.n;
	T->n=M.m;
	T->nonzeroNums=M.nonzeroNums;
	int col;//矩阵M的列
	int q=0;//转置后的三元组表的下标
	int p=0;//原三元组表的下标
	if(T.nonzeroNums){
		for(col=0;col<M.n;col++){//遍历矩阵M的每一列
			for(p=0;p<M.nonzeroNums;p++){
				if(M.data[p].j==col){
					T->data[q].i=M.data[p].j;
					T->data[q].j=M.data[p].i;
					T->data[q].e=M.data[p].e;
					q++;
				}
			}
		}
	}
	return OK;
}

时间复杂度:O(nnonzeroNums),n是指矩阵M的列数,nonzeroNums是指非零元总数。若一个矩阵中非零元的个数很多就会导致nnonzeroNums的数量级较大,例如,若在M100x500的矩阵中,非零元的数量是10000,虽然压缩了矩阵,但是矩阵转置的时间开销会很大,下面介绍一种快速转置的方法。

3.2快速转置

从上一节的转置的目标可知,转置矩阵T对应的三元组表里的三元组顺序实际就是矩阵转置前的矩阵M中按列遍历非零元的次数。假如M的第一列非零元有两个,则转置后,这两个非零元对应的三元组必定是排在矩阵T对应的三元组表的前两个位置,M第二列的非零元则排在第一列的非零元之后,第三列依次排在第二列的后面,从这里可以知道:若知道矩阵M中每一列的第一个非零元在”新“的三元组表中的位置(相当于把三元组表划分为n段,矩阵M每一列的第一个非零元存放在对应段的开始位置),那么在遍历”旧“的三元组表时,只需要把属于同一列的放在同一个段内即可。因此,需要两个一维数组的辅助空间num[],copt[]。

  • num[i] :表示存储矩阵M的i列的非零元个数
  • copt[i] :表示矩阵M的i列的第一个非零元在”新“三元组表中的位置

显然有:(注:数组、二维数组实际存储中,这里都已从0下标开始)
{ c o p t [ 0 ] = 0 第1列(下标col=0)的第一个非零元一定存放在”新“三元组表的第一个位置(下标为0) c o p t [ c o l ] = c o p t [ c o l − 1 ] + n u m [ c o l − 1 ] , 1 ≤ c o l < n o n z e r o N u m s , 每一列非零元在新三元组表中的位置都是基于前一列在三元组表中的位置的末尾,因此要加 n u m [ c o l − 1 ] \begin{cases} copt[0]=0 & \text {第1列(下标col=0)的第一个非零元一定存放在”新“三元组表的第一个位置(下标为0)} \\\\ copt[col]=copt[col-1]+num[col-1], & 1\leq col< nonzeroNums,每一列非零元在新三元组表中的位置都是基于前一列在三元组表中的位置的末尾,因此要加num[col-1] \end{cases} copt[0]=0copt[col]=copt[col1]+num[col1],1(下标col=0)的第一个非零元一定存放在三元组表的第一个位置(下标为0)1col<nonzeroNums,每一列非零元在新三元组表中的位置都是基于前一列在三元组表中的位置的末尾,因此要加num[col1]
对于同一列的非零元怎么处理?在遍历”旧“三元组表的元素(三元组)过程中,当头次遇到某列的第一个非零元并取其列标赋给col=M.data[p].j ,则去copt中获取该的列所在的段起始位置copt[col],并把该非零元存储到"新"三元组表的copt[col]位置上,然后让copt[col]++,即更新同一列所在的段的起始位置,当再次遇到同一列的非零元时,该非零元会放在上次该列的首个非零元的后边,然后再让copt[col]++…依次直到遍历完”旧三元组表“,转置结束。(备注:三元组是指存储非零元的行、列、非零元本身这样一个记录,我把非零元和三元组等价,指的是同一个意思,若无特指,非零元代表三元组;”旧“三元组表是指转置前矩阵M对应三元组表,”新“三元组表则是转置后矩阵T对应的三元组表,也是我们要的结果

案例(逻辑上):
M = ( 0 12 0 0 − 3 0 0 4 0 0 9 0 7 0 0 5 ) M=\begin{pmatrix} 0 & 12 & 0 &0 \\\\ -3 & 0 &0 &4 \\\\ 0&0&9&0 \\\\ 7 & 0 &0 &5 \\\\ \end{pmatrix} M= 03071200000900405 转置后的矩阵 T = ( 0 − 3 0 7 12 0 0 0 0 0 9 0 0 4 0 5 ) 转置后的矩阵T=\begin{pmatrix} 0 & -3 & 0 &7 \\\\ 12 & 0 &0 &0 \\\\ 0&0&9&0 \\\\ 0 & 4&0 &5 \\\\ \end{pmatrix} 转置后的矩阵T= 01200300400907005

旧三元组表:
[ ( 1 , 2 , 12 ) , ( 2 , 1 , − 3 ) , ( 2 , 4 , 4 ) , ( 3 , 3 , 9 ) , ( 4 , 1 , 7 ) , ( 4 , 4 , 5 ) ] [(1,2,12) ,(2,1,-3) ,(2,4,4) ,(3,3,9) ,(4,1,7) ,(4,4,5)] [(1,2,12),(2,1,3),(2,4,4),(3,3,9),(4,1,7),(4,4,5)]
新三元组表:
[ ( 1 , 2 , − 3 ) , ( 1 , 4 , 7 ) ⏟ 第一列对应的段,新三元组的下标是 0 到 1 , ( 2 , 1 , 12 ) ⏟ 第二列对应的段,下标是 2 , ( 3 , 3 , 9 ) ⏟ 第三列 . . . 下标是 3 , ( 4 , 2 , 4 ) , ( 4 , 4 , 5 ) ⏟ 第四列 . . . 下表是 4 到 5 ] [\underbrace{(1,2,-3),(1,4,7)}_{第一列对应的段,新三元组的下标是0到1},\underbrace{(2,1,12)}_{第二列对应的段,下标是2},\underbrace{(3,3,9)}_{第三列...下标是3},\underbrace{(4,2,4),(4,4,5)}_{第四列...下表是4到5}] [第一列对应的段,新三元组的下标是01 (1,2,3),(1,4,7),第二列对应的段,下标是2 (2,1,12),第三列...下标是3 (3,3,9),第四列...下表是45 (4,2,4),(4,4,5)]
代码如下:

Status FastTransposeSMatrix(TSMatrix M,TsMatrix *T){
	T->m=M.n;
	T->n=M.m;
	T->nonzeroNums=M.nonzeroNums;
	int col,t;
	int q=0;//新三元组表的下标
	int p=0;//旧三元组表的下标
	int num[M.n];//辅助数组,统计每一列的非零元个数
	int copt[M.n];//数组数组,统计每一列第一个元素在新三元组表中的位置(对应段的开始
	if(T->nonzeroNums){
		memset(num,0,sizeof(num));//num数组元素置0
		//遍历旧三元组表统计矩阵M每一列的非零元个数
		for(t=0;col<M.nonzeroNums;t++) num[M.data[t].j]++;
		copt[0]=0;
		//计算矩阵M每一列首个非零元在新三元组表中位置(段的起始位置,一列对应一个段)
		for(col=1;col<M.n;col++) copt[col]=copt[col-1]+num[col-1];
		//遍历旧三元组表
		for(p=0;p<M.nonzeroNums;p++){
			col=M.data[p].j;
			q=copt[col];//获取矩阵M的col的非零元在新三元表中的位置
			T->data[q].j=M.data[p].i;
			T->data[q].i=M.data[p].j;
			T->data[q].e=M.data[p].e;
			copt[col]++;
		}
	}
	return OK;
}

结论:相对比朴素思想的方法,快速转置时间复杂度更低,以空间换时间从而达到时间复杂度:O(n+nonzeroNums),即使nonzeroNums很大为m*n,时间复杂度最多也就和遍历矩阵的时间复杂度O(m x n)相当。
细节:快速转置里的copt的计算类似于计算数组的前缀和,每一列的存放位置都是基于上一列的末尾开始。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐