Bootstrap

第六章——稀疏矩阵

稀疏矩阵的基本概念

稀疏矩阵也是一种比较特殊的矩阵类型,但比起上一节提到的特殊矩阵类型,它特殊的地方不在于元素的分布而是在于稀疏矩阵中非0元素的个数s相对于矩阵元素的总个数t非常小。例如一个100*100的矩阵,若其中只有100个非0元素,就可称其为稀疏矩阵。

稀疏矩阵中元素的位置分布一般是随机的。


稀疏矩阵的三元组表示

三元组就是指用三种属性来表示某个节点。由于稀疏矩阵的元素分布一般没有规律,就是说不能用数学公式来减少属性的个数,所以在存储非0元素时必须同时存储该非0元素对应的行下标,列下标和元素值。

将表示非0元素结点的三元组线性表按顺序存储结构存储,则称为稀疏矩阵的三元组顺序表,简称三元组表。三元组和三元组表的结构体如下:

#define M//稀疏矩阵的行数
#define N//稀疏矩阵的列数
#define MaxSize//稀疏矩阵中非0元素最多的个数 
struct TupNode{int r,c;	ElemType d;};//三元组定义,r为行编号,c为列编号,d为存储的元素值
struct TSMatrix{int rows,cols,nums;//行数,列数,非0元素个数 
	TupNode data[MaxSize];//三元组顺序表中的三元组数据 
};

但是需要注意的是,三元组表中data域的非0元素通常按照行编号递增的顺序进行排列,后面的讨论都是默认按照行序递增的顺序存储的。


稀疏矩阵的基本运算

这里只讨论一些比较简单的稀疏矩阵基本操作。

从一个二维稀疏矩阵创建其三元组表示

就是给你一个二维的稀疏矩阵,你将它转化成三元组表的存储方式。将二维矩阵遍历一遍,非0元素直接存储即可:

//从一个二维稀疏矩阵创建其三元组表示,A即为被转化的二维稀疏矩阵 
void CreatMat(TSMatrix &t,ElemType A[M][N]){t.rows=M; t.cols=N; t.nums=0;//行和列直接赋值,非0元素个数初始化为0 
	for (int i=0;i<M;i++) for (int j=0;j<N;j++) if (A[i][j]!=0){//遍历二维矩阵寻找非0元素,寻找到nums++ 
		t.data[t.nums].r=i; t.data[t.nums].c=j; t.data[t.nums].d=A[i][j]; t.nums++;//赋值 
	}
}
三元组元素的赋值

就是对目前的三元组表中的某个三元组进行修改或者添加一个新的三元组。赋值和修改的操作并不复杂,难度在于查找大于等于行i和大于等于列i的编号:

//三元组元素的赋值 
bool Value(TSMatrix &t,ElemType x,int i,int j){//i为行编号,j为列编号,x为元素值 
	if (i>=t.rows||j>=t.cols) return false;//赋值的行和列编号越界,赋值失败返回false	
	int k=0;		
	while (k<t.nums&&i>t.data[k].r) k++;//由于是行序递增排列,率先查找出大于等于i的行编号 
	while (k<t.nums&&i==t.data[k].r&&j>t.data[k].c) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号 
	if (t.data[k].r==i&&t.data[k].c==j) t.data[k].d=x;//查找出的行编号和列编号与i和j相等,说明是对已有的三元组修改 
	else{//否则对三元组表插入一个新的三元组,将编号大于等于k的元素全部后移一位来插入新的元素	
		for (int k1=t.nums-1;k1>=k;k1--){//就是顺序表的元素插入 
			t.data[k1+1].r=t.data[k1].r; t.data[k1+1].c=t.data[k1].c; t.data[k1+1].d=t.data[k1].d;
		}t.data[k].r=i; t.data[k].c=j; t.data[k].d=x; t.nums++;//赋值,三元组个数++ 
	}return true;//赋值成功时返回true
}

插入和赋值的结合事实上是。

将指定位置的元素赋给变量

上面一个是插入和赋值的结合,这里就是查找。事实上上面的操作已经将查找基本上所有的步骤给囊括了(我是按照课本上的顺序写的,我也不知道对于这种有序表为什么不先介绍查找再介绍插入和赋值)。

//将指定位置的元素值赋给变量
bool Assign(TSMatrix t,ElemType &x,int i,int j){//i为行编号,j为列编号,x用来存储查找值 
	if (i>=t.rows||j>=t.cols) return false;//赋值的行和列编号越界,赋值失败返回false
	int k=0;
	while (k<t.nums&&i>t.data[k].r) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号
	while (k<t.nums&&i==t.data[k].r&&j>t.data[k].c) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号
	if (t.data[k].r==i&&t.data[k].c==j) x=t.data[k].d;//查找成功进行赋值 
	else x=0; return true;//查找不成功直接返回0		
}
输出三元组

直接打印就好:

//输出三元组
void DispMat(TSMatrix t){if (t.nums<=0) return;//没有非零元素时返回
	printf("\t%d\t%d\t%d\n",t.rows,t.cols,t.nums);//打印行,列,非0元素个数三个属性 
	for (int i=0;i<t.nums;i++) printf("%d\t%d\t%d\n",t.data[i].r,t.data[i].c,t.data[i].d);//打印三元组的三个属性 
}
稀疏矩阵转置

矩阵转置就是将原本N*M的矩阵A[N][M],修改成M*N的矩阵B[M][N],其中修改后的矩阵B的元素b[i][j]=a[j][i]。稀疏矩阵行,列,非0元素个数这三个属性交换一下重新复制即可,问题在于原本的三元组表按照行序递增的顺序排列的,转置后的元素的行属性等于原本元素的列属性,也就是说对于原本稀疏矩阵中存在的非0元素,需要设计一个算法将它们按照列序递增的顺序进行排列。

我个人觉得写起来比较快效率也比较高的做法,就指定一下新的规则排序一下就好了(sort或者qsort),书上提供的做法类似于桶排序,以0到稀疏矩阵的最大宽度作为“桶的编号”进行遍历,在原本的三元组表中寻找到三元组的列编号等于“桶的编号”,将三元组的行列交换后赋值到新的三元组表之中,得到的新的三元组表即是按照原本的列属性的值递增排列的(具体详见计数排序)。

//矩阵转置,转置后矩阵的行等于原本矩阵的列,转置后矩阵的列等于原本矩阵的行,非0元素的个数是一样的 
void TranTat(TSMatrix t,TSMatrix &tb){tb.rows=t.cols; tb.cols=t.rows; tb.nums=t.nums;
	int index=0;//index为转置后新的三元组表的下标		
	if (t.nums!=0){//当存在非零元素时进行转置,列i从0递增到最大宽度,在三元组表中寻找列属性与i相等的进行添加 
		for (int i=0;i<t.cols;i++) for (int j=0;j<t.nums;j++) if (t.data[j].c==i){//本质就是计数排序 
			tb.data[index].r=t.data[j].c; tb.data[index].c=t.data[j].r; tb.data[index].d=t.data[j].d;//交换赋值 
			index++;
		}
	}
}

基本操作合集如下:

#define M//稀疏矩阵的行数
#define N//稀疏矩阵的列数
#define MaxSize//稀疏矩阵中非0元素最多的个数 
struct TupNode{int r,c;	ElemType d;};//三元组定义,r为行编号,c为列编号,d为存储的元素值
struct TSMatrix{int rows,cols,nums;//行数,列数,非0元素个数 
	TupNode data[MaxSize];//三元组顺序表中的三元组数据 
};//从一个二维稀疏矩阵创建其三元组表示,A即为被转化的二维稀疏矩阵 
void CreatMat(TSMatrix &t,ElemType A[M][N]){t.rows=M; t.cols=N; t.nums=0;//行和列直接赋值,非0元素个数初始化为0 
	for (int i=0;i<M;i++) for (int j=0;j<N;j++) if (A[i][j]!=0){//遍历二维矩阵寻找非0元素,寻找到nums++ 
		t.data[t.nums].r=i; t.data[t.nums].c=j; t.data[t.nums].d=A[i][j]; t.nums++;//赋值 
	}
}//三元组元素的赋值 
bool Value(TSMatrix &t,ElemType x,int i,int j){//i为行编号,j为列编号,x为元素值 
	if (i>=t.rows||j>=t.cols) return false;//赋值的行和列编号越界,赋值失败返回false	
	int k=0;		
	while (k<t.nums&&i>t.data[k].r) k++;//由于是行序递增排列,率先查找出大于等于i的行编号 
	while (k<t.nums&&i==t.data[k].r&&j>t.data[k].c) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号 
	if (t.data[k].r==i&&t.data[k].c==j) t.data[k].d=x;//查找出的行编号和列编号与i和j相等,说明是对已有的三元组修改 
	else{//否则对三元组表插入一个新的三元组,将编号大于等于k的元素全部后移一位来插入新的元素	
		for (int k1=t.nums-1;k1>=k;k1--){//就是顺序表的元素插入 
			t.data[k1+1].r=t.data[k1].r; t.data[k1+1].c=t.data[k1].c; t.data[k1+1].d=t.data[k1].d;
		}t.data[k].r=i; t.data[k].c=j; t.data[k].d=x; t.nums++;//赋值,三元组个数++ 
	}return true;//赋值成功时返回true
}//将指定位置的元素值赋给变量
bool Assign(TSMatrix t,ElemType &x,int i,int j){//i为行编号,j为列编号,x用来存储查找值 
	if (i>=t.rows||j>=t.cols) return false;//赋值的行和列编号越界,赋值失败返回false
	int k=0;
	while (k<t.nums&&i>t.data[k].r) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号
	while (k<t.nums&&i==t.data[k].r&&j>t.data[k].c) k++;//如果查找到的行编号等于i,紧接着查找大于等于j的列编号
	if (t.data[k].r==i&&t.data[k].c==j) x=t.data[k].d;//查找成功进行赋值 
	else x=0; return true;//查找不成功直接返回0		
}//输出三元组
void DispMat(TSMatrix t){if (t.nums<=0) return;//没有非零元素时返回
	printf("\t%d\t%d\t%d\n",t.rows,t.cols,t.nums);//打印行,列,非0元素个数三个属性 
	for (int i=0;i<t.nums;i++) printf("%d\t%d\t%d\n",t.data[i].r,t.data[i].c,t.data[i].d);//打印三元组的三个属性 
}//矩阵转置,转置后矩阵的行等于原本矩阵的列,转置后矩阵的列等于原本矩阵的行,非0元素的个数是一样的 
void TranTat(TSMatrix t,TSMatrix &tb){tb.rows=t.cols; tb.cols=t.rows; tb.nums=t.nums;
	int index=0;//index为转置后新的三元组表的下标		
	if (t.nums!=0){//当存在非零元素时进行转置,列i从0递增到最大宽度,在三元组表中寻找列属性与i相等的进行添加 
		for (int i=0;i<t.cols;i++) for (int j=0;j<t.nums;j++) if (t.data[j].c==i){//本质就是计数排序 
			tb.data[index].r=t.data[j].c; tb.data[index].c=t.data[j].r; tb.data[index].d=t.data[j].d;//交换赋值 
			index++;
		}
	}
}

稀疏矩阵的十字链表表示

十字链表相较于三元组表是稀疏矩阵的一种链式存储结结构,构建方法书面语言描述起来相当的庞杂,简略一点说就是对稀疏矩阵中的每个非0元素创建一个包含行号,列号,元素值三个属性的结点来存放它。对于同一行和同一列的所有结点,我们构建一个带头结点的循环单链表来进行存储。

对于循环单链表的头结点,第i行对应的循环单链表和第i列对应的循环单链表可以共用一个h[i],即h[i]同时包含有行指针和列指针,h[i]的行指针指向行号为i的单链表的首结点,列指针指向列号为i的单链表的首结点,这样头结点的个数为行和列的较大值。

最后将所有的头结点h[i]构建成一个带头结点的循环单链表,总头结点记作hm,总头结点中存储稀疏矩阵行数和列数等信息。

头结点h[i]和存放非0元素的结点用同一种结构体表示,设置一个联合体tag,当结构体表示的是头结点时tag存储h[i]指针,结构体表示的是非0元素时tag存储元素值。

#define M 3//矩阵行数 
#define N 4//矩阵列数 
#define Max ((M)>(N)?(M):(N))//矩阵行数和列数的较大值,即头结点的个数 
typedef int ElemType;
struct MatNode{int row,col;//十字链表结点类型,行号和列号
	MatNode *right,*down;//向右和向下,即行和列链表的指针
	union{ElemType value; MatNode *link;}tag;//头结点中tag存储的是h[i]指针,非0元素时tag存储元素值value 
};

稀疏矩阵的基本操作

这个书上没有写,我也是凭自己能力读代码解释的。稀疏矩阵十字链表基本操作的写法比较繁琐就不多做解释。

根据稀疏矩阵a创建对应的十字链表

首先创建各行各列的头结点以及总头结点,将M行的头结点按照尾插法构建成一个循环链表,N行的头结点同理。然后查询出稀疏矩阵中的非0元素,按照非0元素的行号和列号,在对应的行链表和列链表中查找插入的位置。

//根据稀疏矩阵a创建对应的十字链表
void CreatMat(MatNode *&mh,ElemType a[][N]){
	int j;
	MatNode *h[Max],*p,*q,*r;//r为头结点h[i]构成的链表的尾结点
	//创建十字链表的总头结点mh,mh存储稀疏矩阵行和列的信息,r指向mh 
	mh=(MatNode *)malloc(sizeof(MatNode)); mh->row=M; mh->col=N; r=mh; 
	for (int i=0;i<Max;i++){//采用尾插法创建头结点h[i]构成的循环链表
		h[i]=(MatNode *)malloc(sizeof(MatNode)); r->tag.link=h[i];//代表头结点的tag存储h[i] 
		r=h[i];//r始终指向头结点h[i]构成的链表的尾结点 
		h[i]->down=h[i]->right=h[i];//分别以h[i]作为头结点构建存储行和列的循环链表	
	}r->tag.link=mh;//调整为循环链表
	for (int i=0;i<M;i++) for (int j=0;j<N;j++)	if (a[i][j]!=0){//(按照行序递增)寻找非0元素
		//创建结点存储非0元素值,行序,列序等属性 
		p=(MatNode *)malloc(sizeof(MatNode)); p->row=i; p->col=j; p->tag.value=a[i][j];
		//查找在行表中的插入位置,行表中按照列序递增的顺序排位 
        q=h[i]; while (q->right!=h[i]&&q->right->col<j) q=q->right;
		p->right=q->right; q->right=p;//完成行表的插入
		//查找在列表中的插入位置,列表中按照行序递增的顺序排位
		q=h[j];	while (q->down!=h[j]&&q->down->row<i) q=q->down; 
		p->down=q->down; q->down=p;//完成列表的插入
	}
}	
销毁十字链表

销毁十字链表时需要注意一点的就是,当你在创建十字链表时需要行链表与列链表之间的关系需要分别建立,分别连接。但当你销毁十字链表时,只需要销毁行链表的所有结点就相当于销毁列链表的所有结点,所以只需要销毁一次就好了。

//销毁十字链表
void DestroyMat(MatNode *&mh){MatNode *pre,*p,*mp; mp=mh->tag.link;//mp遍历头结点 
	while (mp!=mh){//销毁行链表,即释放所有存储非0元素的结点
		pre=mp->right;			
		if (pre!=mp){//p指向后继结点,释放行链表空间 
			p=pre->right; while (p!=mp){free(pre); pre=p; p=p->right;}
		}mp=mp->tag.link;//mp指向下一个头结点
	}//释放所有的头结点,p指向后继节点 
	pre=mh->tag.link; p=pre->tag.link;			
	while (p!=mh){free(pre); pre=p; p=p->tag.link;} free(mh);
}
打印十字链表

和销毁十字链表基本一样

//输出十字链表
void DispMat(MatNode *mh){MatNode *p,*q;//p遍历头结点,q遍历头结点对应的行链表 
	printf("行=%d  列=%d\n", mh->row,mh->col); p=mh->tag.link;
	while (p!=mh){q=p->right;//遍历输出行链表 
		while (p!=q){printf("%d\t%d\t%d\n", q->row,q->col,q->tag.value); q=q->right;}
		p=p->tag.link;//访问下一个头结点 
	}
}

基本操作合集如下:

#define M 3//矩阵行数 
#define N 4//矩阵列数 
#define Max ((M)>(N)?(M):(N))//矩阵行数和列数的较大值,即头结点的个数 
typedef int ElemType;
struct MatNode{int row,col;//十字链表结点类型,行号和列号
	MatNode *right,*down;//向右和向下,即行和列链表的指针
	union{ElemType value; MatNode *link;}tag;//头结点中tag存储的是h[i]指针,非0元素时tag存储元素值value 
};//根据稀疏矩阵a创建对应的十字链表
void CreatMat(MatNode *&mh,ElemType a[][N]){
	int j;
	MatNode *h[Max],*p,*q,*r;//r为头结点h[i]构成的链表的尾结点
	//创建十字链表的总头结点mh,mh存储稀疏矩阵行和列的信息,r指向mh 
	mh=(MatNode *)malloc(sizeof(MatNode)); mh->row=M; mh->col=N; r=mh; 
	for (int i=0;i<Max;i++){//采用尾插法创建头结点h[i]构成的循环链表
		h[i]=(MatNode *)malloc(sizeof(MatNode)); r->tag.link=h[i];//代表头结点的tag存储h[i] 
		r=h[i];//r始终指向头结点h[i]构成的链表的尾结点 
		h[i]->down=h[i]->right=h[i];//分别以h[i]作为头结点构建存储行和列的循环链表	
	}r->tag.link=mh;//调整为循环链表
	for (int i=0;i<M;i++) for (int j=0;j<N;j++)	if (a[i][j]!=0){//(按照行序递增)寻找非0元素
		//创建结点存储非0元素值,行序,列序等属性 
		p=(MatNode *)malloc(sizeof(MatNode)); p->row=i; p->col=j; p->tag.value=a[i][j];
		//查找在行表中的插入位置,行表中按照列序递增的顺序排位 
        q=h[i]; while (q->right!=h[i]&&q->right->col<j) q=q->right;
		p->right=q->right; q->right=p;//完成行表的插入
		//查找在列表中的插入位置,列表中按照行序递增的顺序排位
		q=h[j];	while (q->down!=h[j]&&q->down->row<i) q=q->down; 
		p->down=q->down; q->down=p;//完成列表的插入
	}
}//销毁十字链表
void DestroyMat(MatNode *&mh){MatNode *pre,*p,*mp; mp=mh->tag.link;//mp遍历头结点 
	while (mp!=mh){//销毁行链表,即释放所有存储非0元素的结点
		pre=mp->right;			
		if (pre!=mp){//p指向后继结点,释放行链表空间 
			p=pre->right; while (p!=mp){free(pre); pre=p; p=p->right;}
		}mp=mp->tag.link;//mp指向下一个头结点
	}//释放所有的头结点,p指向后继节点 
	pre=mh->tag.link; p=pre->tag.link;			
	while (p!=mh){free(pre); pre=p; p=p->tag.link;} free(mh);
}//输出十字链表
void DispMat(MatNode *mh){MatNode *p,*q;//p遍历头结点,q遍历头结点对应的行链表 
	printf("行=%d  列=%d\n", mh->row,mh->col); p=mh->tag.link;
	while (p!=mh){q=p->right;//遍历输出行链表 
		while (p!=q){printf("%d\t%d\t%d\n", q->row,q->col,q->tag.value); q=q->right;}
		p=p->tag.link;//访问下一个头结点 
	}
}			
;