数据结构个人笔记 第13课 稀疏数组
数组
稀疏矩阵
稀疏矩阵的压缩存储,至少需要存储以下信息:
- 矩阵中各非0元素的值,以及所在矩阵中的行标和列标
- 矩阵的总行数和总列数
三元组顺序表
三元组压缩存储:
组中的数据分别表示(行标、列标、元素值)
在c语言中,三元组需要用结构体来实现
//三元体结构体
typedef struct{
int i ,j;//行标i,列标j
int data;
}triple;
由于稀疏矩阵中非0元素有多个,因此需要建立triple数组存储各个元素的三元组。除此之外,考虑到还要存储矩阵的总行数和总列数,因此可以采用以下结构表示整个稀疏矩阵:
#define number 20
//矩阵的结构表示
typedef struct {
triple data[number];//存储该矩阵中所有非0元素的三元组
int n,m,num;//n和m分别记录矩阵的行数和列数,num记录矩阵中所有非0元素的个数
}TSMatrix
可以看出,TSMatrix是一个结构体,其包含一个三元组数组,以及用于存储矩阵总行数、总列数和非0元素个数的变量
代码实现
#include<stdio.h>
#define number 3
typedef struct {
int i , j;
int data;
}triple;
typedef struct{
triple data[number];
int num,n,m;
}TSMatrix;
void display(TSMatrix M);
int main(){
TSMatrix M;
M.m = 3;
M.n = 3;
M.num = 3;
M.data[0].i = 1;
M.data[0].j = 1;
M.data[0].data = 1;
M.data[1].i = 2;
M.data[1].j = 3;
M.data[1].data = 5;
M.data[2].i = 3;
M.data[2].j = 1;
M.data[2].data = 3;
display(M);
return 0;
}
void display(TSMatrix M){
for(int i = 1;i<= M.n;i++){
for(int j = 1;j <= M.m;j++){
int value = 0;
for(int k = 0;k < M.num;k++){
if(i == M.data[k].i && j == M.data[k].j){
printf("%d ",M.data[k].data);
value = 1;
break;
}
}
if(value == 0){
printf("0 ");
}
}
printf("\n");
}
}
结果:
行逻辑链接的顺序表
通过研究实现代码,我们可以发现三元组顺序表每次提取指定元素都需要遍历整个数组,运行效率很低
行逻辑链接的顺序表,可以看做是三元组顺序表的升级版,即在三元组顺序表的基础的上改善了提取数据的效率。
行逻辑链接的顺序表和三元组顺序表的实现过程类似,它们存储矩阵的过程完全相同,都是将矩阵中非0元素的三元组存储在一维数组中。但为了提高提取数据的效率,前者在存储矩阵时比后者多使用了一个数组,专门记录矩阵中每行第一个非0数组在一维数组中的位置
实现行逻辑链接的顺序表共需要做以下这些工作:
- 将矩阵中的非0元素采用三元组的形式存储到一维数组data中,
- 使用数组rpos记录矩阵中每行第一个非0元素在一维数组中的存储位置
三元组存储稀疏矩阵
rpos
从行逻辑连接的顺序表中提取元素,则可以借助rpos数组提高遍历数组的效率
代码实现
#include<stdio.h>
#define MAXSIZE 12500
#define MAXRC 100
#define ElemType int
typedef struct{
int i,j;
ElemType e;
}Triple;
typedef struct{
Triple data[MAXSIZE+1];
int rops[MAXRC+1];//每行第一个非零元素在data数组中的位置
int mu,nu,tu;//行数,列数,元素个数
}RLSMatrix;
void display(RLSMatrix M){
for(int i = 1;i <= M.mu; i++){
for(int j = 1;j <= M.nu;j++){
int value = 0;
if(i+1 <= M.mu){
for(int k = M.rops[i];k<M.rops [i+1];k++){
if(i == M.data[k].i && j == M.data [k].j){
printf("%d ",M.data[k].e);
value = 1;
break;
}
}
if(value == 0){
printf("0 ");
}
}else{
for(int k = M.rops[i];k <= M.tu;k++){
if(i == M.data[k].i && j == M.data[k].j){
printf("%d ",M.data[k].e);
value = 1;
break;
}
}
if(value == 0){
printf("0 ");
}
}
}
printf("\n");
}
}
int main(int argc,char* argv[]){
RLSMatrix M;
M.tu = 4;
M.mu = 3;
M.nu = 4;
M.rops [1] = 1;
M.rops [2] = 3;
M.rops [3] = 4;
M.data[1].e = 3;
M.data[1].i = 1;
M.data[1].j = 2;
M.data[2].e = 5;
M.data[2].i = 1;
M.data[2].j = 4;
M.data[3].e = 1;
M.data[3].i = 2;
M.data[3].j = 3;
M.data[4].e = 2;
M.data[4].i = 3;
M.data[4].j = 1;
display(M);
return 0;
}
十字链表法
十字链表存储稀疏矩阵,该存储方式采用的是“链表+数组”结构
可以看到,使用十字链表压缩存储稀疏矩阵时,矩阵中各行各列都各用一个链表存储,与此同时,所有行链表的表头存储到一个数组rhead,所有列链表的表头存储到另一个数组chead中
因此,各个链表中节点的结构应为:
两个指针域分别用于链接所在行的下一个元素以及所在列的下一个元素。
结构体为:
typedef struct OLNode{
int i ,j;
int data;
struct OLNode * right,*down;
}OLNode;
代码展示
#include<stdio.h>
#include<stdlib.h>
typedef struct OLNode{
int i,j,data;
struct OLNode *right,*down;
}OLNode,*OLink;
typedef struct{
OLink *rhead,*chead;//行和列链表的头指针
int mu,nu,tu;//矩阵的行数、列数和非零元的个数
}CrossList;
CrossList CreateMatrix_OL(CrossList M);
void display(CrossList M);
int main(){
CrossList M;
M.rhead = NULL;
M.chead = NULL;
M = CreateMatrix_OL(M);
printf("输出矩阵M:\n");
display(M);
return 0;
}
CrossList CreateMatrix_OL(CrossList M){
int m,n,t;
int i,j,data;
OLNode *p,*q;
printf("输入矩阵的行数、列数和非0元素个数:");
scanf("%d%d%d",&m,&n,&t);
M.mu = m;
M.nu = n;
M.tu = t;
if(!(M.rhead = (OLink*)malloc((m+1)*sizeof(OLink))) || !(M.chead = (OLink*)malloc((n+1)*sizeof(OLink)))){
printf("初始化矩阵失败");
exit(0);
}
for(i = 1; i<= m;i++){
M.rhead[i] = NULL;
}
for(j = 1;j<=n;j++){
M.chead[j] = NULL;
}
for(scanf("%d%d%d",&i,&j,&data);0 != i;scanf("%d%d%d",&i,&j,&data)){
if(!(p = (OLNode*)malloc(sizeof(OLNode)))){
printf("初始化三元组失败");
exit(0);
}
p->i = i;
p->j = j;
p->data = data;
//链接到行的指定位置
if(NULL == M.rhead[i] || M.rhead[i]->j > j){
p -> right = M.rhead[i];
M.rhead[i] = p;
}else{
for(q = M.rhead[i];(q -> right)&&q->right->j <j;q = q->right);
p->right = q->right;
q->right = p;
}
//链接到列的指定位置
if(NULL == M.chead[j] || M.chead[j]->i >i){
p -> down = M.chead[j];
M.chead[j] = p;
} else{
for(q= M.chead[j];(q->down)&&q->down->i <i;q = q->down);
p ->down = q->down;
q->down = p;
}
}
return M;
}
void display(CrossList M){
for(int i = 1;i <= M.nu;i++){
if(NULL != M.chead[i]){
OLink p = M.chead[i];
while(NULL != p){
printf("%d\t%d\t%d\n",p->i,p->j,p->data);
p = p->down;
}
}
}
}
稀疏矩阵的转制算法
矩阵的转置,即互换矩阵中所有元素的行标和列标
但如果想通过程序实现矩阵的转置,互换行标和列标只是第一步。因为实现矩阵转置的前提是将矩阵存储起来,数据结构中提供了3中存储稀疏矩阵的结构,三元组、行逻辑、十字链表。如果采用前两种结构,矩阵的转置过程会涉及三元组表也跟着改变的问题
不仅如此,如果矩阵的行数和列数不等,也需要将它们互换
因此,通过上面的分析,矩阵转置的实现过程需完成以下3步:
- 将矩阵的行数和列数互换
- 将三元组表(存储矩阵)中的i列和j列互换,实现矩阵的转置
- 以j列为序,重新排列三元组表中存储各三元组的先后顺序
矩阵转置的实现思路是:不断遍历存储矩阵中的三元租表,每次都取出表中j列最小的那个三元组,互换行标和列标的值,并按次序存储到一个新的三元组表中
因此实际步骤可以分为以下:
- 新建一个三元组表(用于存储转置矩阵),并将原矩阵的行数和列数互换赋值给新三元组
- 遍历三元组表,找到表中j列最小值所在的三元组,然后将其行标和列表互换后添加到一个新的三元组表中
- 继续遍历三元组表,找到表中j列次小值的三元组,根据找到他们的先后次序将各自的行标和列标互换后添加到新三元组表中
- 依次类推
代码展示
#include<stdio.h>
#define number 10
typedef struct{
int i,j;
int data;
}triple;
typedef struct{
triple data[10];
int n,m,num;
}TSMatrix;
TSMatrix transposeMatrix(TSMatrix M,TSMatrix T){
T.m = M.n;
T.n = M.m;
T.num = M.num;
if(T.num){
int q = 0;
for(int col = 1; col <= M.m;col++){
for(int p = 0;p < M.num ;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].data = M.data[p].data;
q++;
}
}
}
}
return T;
}
void display(TSMatrix M){
for(int i = 1;i<= M.n;i++){
for(int j = 1;j <= M.m;j++){
int value = 0;
for(int k = 0;k < M.num;k++){
if(i == M.data[k].i && j == M.data[k].j){
printf("%d ",M.data[k].data);
value = 1;
break;
}
}
if(value == 0){
printf("0 ");
}
}
printf("\n");
}
}
int main(){
TSMatrix M;
M.m = 2;
M.n = 3;
M.num = 4;
M.data[0].i = 1;
M.data[0].j = 2;
M.data[0].data = 1;
M.data[1].i = 2;
M.data[1].j = 2;
M.data[1].data = 3;
M.data[2].i = 3;
M.data[2].j = 1;
M.data[2].data = 6;
M.data[3].i = 3;
M.data[3].j = 2;
M.data[3].data = 5;
display(M);
TSMatrix T;
for(int k = 0;k < number;k++){
T.data[k].i = 0;
T.data[k].j = 0;
T.data[k].data = 0;
}
T = transposeMatrix(M,T);
for(int i = 0;i < T.num;i++){
printf("(%d,%d,%d)\n",T.data[i].i,T.data[i].j,T.data[i].data);
}
display(T);
return 0;
}
稀疏矩阵的快速转置
稀疏矩阵的快速转置算法和前面的普通算法的区别仅在第3步,快速转置能够做到遍历一次三元组表即可完成第三步的工作
稀疏矩阵的快速转置是这样的,在普通算法的基础上增设两个数组(假设分别为array 和 copt):
- array数组负责记录原矩阵每一列非0元素的个数
- copt数组用于计算稀疏矩阵中每列第一个非0元素在新三元表中存放的位置。我们通常默认第一列的第一个非0元素存放到新三元组表中的位置为1,然后通过cpot[col] = copt[col - 1]+ array[col -1]公式可计算出后续各列首个非0元素存放到新三元组表的位置
普通转置算法时间复杂度为o(n^2),这里的快速转置算法的效率更高
代码实现
TSMatrix fastTransposeMatrix(TSMatrix M,TSMatrix T){
T.m = M.n;
T.n = M.m;
T.num = M.num;
if(T.num){
//计算array数组
int array[number];
for(int col = 1;col <= M.m;col++){
array[col] = 0;
}
for(int t = 0;t < M.num;t++){
int j = M.data[t].j;
array[j]++;
}
int cpot[T.m+1];
cpot[1] = 1;//第一列中第一个非0元素的位置默认为1
for(int col = 2;col <= M.m;col++){
cpot[col] = cpot[col -1]+array[col -1];
}
//遍历一次即可实现三元组表的转置
for(int p = 0;p < M.num;p++){
//提取当前三元组的列数
int col = M.data[p].j;
//根据列数和cpot数组,找到当前元素需要存放的位置
int q = cpot[col];
//转置矩阵的三元组默认从数组下标0开始,而得到的q值是单纯的位置,所以要减一
T.data[q-1].i = M.data [p].j;
T.data [q-1].j = M.data[p].i;
T.data[q-1].data = M.data[p].data;
//存放完成后cpot数组对应的位置要+1,以便下次该列存储下一个三元组
cpot[col] ++;
}
}
return T;
}
矩阵乘法(行逻辑连接的顺序表)
矩阵相乘的前提条件是:乘号之前的矩阵的列数要和乘号后的矩阵的行数相等。且矩阵的乘法运算没有交换律,也即AB和BA是不一样的。这些知识点都在线性代数一课中有讲解
计算方法是:用矩阵A的第i行和矩阵B中的每一列j对应的数值做乘法运算,乘积一一相加,所得结果即为矩阵C中第i行第j列的值
如果用正常逻辑来写:
int C[MAX][MAX];
for (int i=0; i<m1;i++) {
for (int j=0; j<n2; j++) {
C[i][j]=0;
for (int k=0; k<n1; k++) {
C[i][j]+=A[i][k]*B[k][j];
}
}
}
时间复杂度为O(m1n2n1),是比较高的
而稀疏矩阵在做乘法运算时,由于本身矩阵中含有的非0元素少,普通算法会出现很多00或者k0或者0*k的情况,如果采用上面的算法效率较低。下面介绍使用行逻辑连接的顺序表计算矩阵乘积的方法
行逻辑连接的顺序表解决矩阵乘积算法:
对矩阵的乘积进行深度剖析,矩阵A、B相乘的运算过程是这样的:
- 首先,找到矩阵A中第一行的非0元素(由于行逻辑连接的顺序表中存储的都是非0元素,查找的过程就需要使用记录每行第一个非0元素的首地址的数组来完成)
- 用A中第一行非0元素和B中对应的第一行中的非0元素分别相乘,并相加
- 以此类推
现在的问题就是,如何知道顺序表中存放的非0元素是哪一行的呢?
解决方案:由于使用的是行逻辑连接的顺序表,所以,已经知道了每一个矩阵中的每一行有多少个非0元素,而且第一行的第一个非0元素的位置一定是1.
所以,第n行的非0元素的位置范围是:大于或等于第n行第一个元素的位置,小于第n+1行第一个元素的位置
代码实现
#include<stdio.h>
#define MAXSIZE 12500
#define MAXRC 100
#define ElemType int
typedef struct{
int i ,j;
ElemType e;
}Triple;
typedef struct{
Triple data[MAXSIZE+1];
int rpos[MAXRC+1];
int mu ,nu ,tu;
}RLSMatrix;
RLSMatrix MultSMatrix(RLSMatrix A,RLSMatrix B,RLSMatrix C){
//如果矩阵A的列数与矩阵B 的行数不等,则不能做矩阵乘运算
if(A.nu != B.mu)
return C;
C.mu = A.mu;
C.nu = B.nu;
C.tu = 0;
//如果其中任意矩阵的元素个数为零,做乘法元素没有意义,全是零
if(A.tu * B.tu == 0)
return C;
else{
int arow;
int ccol;//遍历矩阵A的每一行
for(arow = 1;arow <= A.mu ;arow++){
//创建一个临时存储乘积结果的数组,且初始化为0,遍历每次都需要清空
int ctemp[MAXRC+1] = {};
C.rpos[arow] = C.tu + 1;
int tp;
if(arow < A.mu)
tp = A.rpos[arow+1];//获取矩阵A的下一行第一个非0元素在data数组中的位置
else
tp = A.tu +1;//若当前行是最后一行,则取最后一个元素+1
int p;
int brow;
for(p = A.rpos[arow];p < tp;p++){
brow = A.data [p].j;//取该非0元素的列数,便于去B中找对应的做乘积的非0元素
int t;//判断如果对于A中非0元素,找到矩阵B中做乘法的那一行中的所有的非0元素
if(brow < B.mu)
t = B.rpos [brow+1];
else
t = B.tu + 1;
int q;//遍历找到对应的非0元素,开始做乘积运算
for(q = B.rpos [brow];q < t;q++){
//得到的乘积结果,每次和ctemp数组中响应位置的数值做加和运算
ccol = B.data[q].j;
ctemp[ccol] += A.data [p].e * B.data [q].e;
}
} //矩阵C的行数等于矩阵A的行数,列数等于矩阵B的列数,所以得到的ctemp存储的结果,也会在C的列数的范围内
for(ccol = 1;ccol <= C.nu;ccol++){
//由于结果可以是0,而0不需要存储,所以在这里需要判断
if(ctemp[ccol]){
//不为0,则记录矩阵中非0元素的个数的变量tu要+1,且该值不能超过存放三元素数组的空间的大小
if(++C.tu > MAXSIZE)
return C;
else{
C.data[C.tu].e = ctemp[ccol];
C.data [C.tu].i = arow;
C.data[C.tu].j = ccol;
}
}
}
}
return C;
}
}
int main(int argc,char* argv[])
{
RLSMatrix M,N,T;
M.tu = 4;
M.mu = 3;
M.nu = 4;
M.rpos[1] = 1;
M.rpos[2] = 3;
M.rpos[3] = 4;
M.data[1].e = 3;
M.data[1].i = 1;
M.data[1].j = 1;
M.data[2].e = 5;
M.data[2].i = 1;
M.data[2].j = 4;
M.data[3].e = -1;
M.data[3].i = 2;
M.data[3].j = 2;
M.data[4].e = 2;
M.data[4].i = 3;
M.data[4].j = 1;
N.tu = 4;
N.mu = 4;
N.nu = 2;
N.rpos[1] = 1;
N.rpos[2] = 2;
N.rpos[3] = 3;
N.rpos[4] = 5;
N.data[1].e = 2;
N.data[1].i = 1;
N.data[1].j = 2;
N.data[2].e = 1;
N.data[2].i = 2;
N.data[2].j = 1;
N.data[3].e = -2;
N.data[3].i = 3;
N.data[3].j = 1;
N.data[4].e = 4;
N.data[4].i = 3;
N.data[4].j = 2;
T= MultSMatrix(M,N,T);
for (int i=1; i<=T.tu; i++) {
printf("(%d,%d,%d)\n",T.data[i].i,T.data[i].j,T.data[i].e);
}
return 0;
}
矩阵加法(基于十字链表)
矩阵之间能够进行加法运算的前提条件是:各矩阵的行数和列数必须相等
因为在类似于矩阵的加法运算中,矩阵中的数据元素变化较大(这里主要指的是非0元素变为0、0变为非0元素),就需要考虑采用另一种结构–链式存储结构来存储三元组
在解决矩阵相加的问题时,由于采用的是十字链表法存储矩阵的三元组,所以在相加的过程中,针对矩阵B中每一个非0元素,需要判断在矩阵A中相对应的位置,有三种情况:
- 提取到的B中的三元组在A相应位置上没有非0元素,此时直接加到矩阵A该行链表的对应位置上
- 提取到的B中的三元组在A相应位置上有非0元素,且相加不为0,此时只需要更改A中对应位置上的三元组的值即可
- 提取到的B中三元组在A相应位置上有非0元素,但相加为0,此时需要删除矩阵A中对应节点
- 提示:算法中,只需要逐个提取矩阵B中的非0元素,然后判断A中对应位置上是否有非0元素,根据不同的情况,相应作出处理
代码实现
#include<stdio.h>
#include<stdlib.h>
typedef struct OLNode
{
int i,j,e; //矩阵三元组i代表行 j代表列 e代表当前位置的数据
struct OLNode *right,*down; //指针域 右指针 下指针
}OLNode,*OLink;
typedef struct
{
OLink *rhead,*chead; //行和列链表头指针
int mu,nu,tu; //矩阵的行数,列数和非零元的个数
}CrossList;
CrossList CreateMatrix_OL(CrossList M);
CrossList AddSMatrix(CrossList M,CrossList N);
void display(CrossList M);
int main()
{
CrossList M,N;
printf("输入测试矩阵M:\n");
M=CreateMatrix_OL(M);
printf("输入测试矩阵N:\n");
N=CreateMatrix_OL(N);
M=AddSMatrix(M,N);
printf("矩阵相加的结果为:\n");
display(M);
}
CrossList CreateMatrix_OL(CrossList M)
{
int m,n,t;
int i,j,e;
OLNode *p,*q;
scanf("%d%d%d",&m,&n,&t);
M.mu=m;
M.nu=n;
M.tu=t;
if(!(M.rhead=(OLink*)malloc((m+1)*sizeof(OLink)))||!(M.chead=(OLink*)malloc((n+1)*sizeof(OLink))))
{
printf("初始化矩阵失败");
exit(0);
}
for(i=1;i<=m;i++)
{
M.rhead[i]=NULL;
}
for(j=1;j<=n;j++)
{
M.chead[j]=NULL;
}
for(scanf("%d%d%d",&i,&j,&e);0!=i;scanf("%d%d%d",&i,&j,&e)) {
if(!(p=(OLNode*)malloc(sizeof(OLNode))))
{
printf("初始化三元组失败");
exit(0);
}
p->i=i;
p->j=j;
p->e=e;
if(NULL==M.rhead[i]||M.rhead[i]->j>j)
{
p->right=M.rhead[i];
M.rhead[i]=p;
}
else
{
for(q=M.rhead[i];(q->right)&&q->right->j<j;q=q->right);
p->right=q->right;
q->right=p;
}
if(NULL==M.chead[j]||M.chead[j]->i>i)
{
p->down=M.chead[j];
M.chead[j]=p;
}
else
{
for (q=M.chead[j];(q->down)&& q->down->i<i;q=q->down);
p->down=q->down;
q->down=p;
}
}
return M;
}
CrossList AddSMatrix(CrossList M,CrossList N){
OLNode * pa,*pb;
OLink * hl=(OLink*)malloc(M.nu*sizeof(OLink));
OLNode * pre=NULL;
for (int j=1; j<=M.nu; j++) {
hl[j]=M.chead[j];
}
for (int i=1; i<=M.mu; i++) {
pa=M.rhead[i];
pb=N.rhead[i];
while (pb!=NULL) {
OLNode * p=(OLNode*)malloc(sizeof(OLNode));
p->i=pb->i;
p->j=pb->j;
p->e=pb->e;
p->down=NULL;
p->right=NULL;
if (pa==NULL||pa->j>pb->j) {
if (pre==NULL) {
M.rhead[p->i]=p;
}else{
pre->right=p;
}
p->right=pa;
pre=p;
if (!M.chead[p->j]||M.chead[p->j]->i>p->i) {
p->down=M.chead[p->j];
M.chead[p->j]=p;
}else{
p->down=hl[p->j]->down;
hl[p->j]->down=p;
}
hl[p->j]=p;
}else{
if (pa->j<pb->j) {
pre=pa;
pa=pa->right;
continue;
}
if (pa->j==pb->j) {
pa->e+=pb->e;
if (pa->e==0) {
if (pre==NULL) {
M.rhead[pa->i]=pa->right;
}else{
pre->right=pa->right;
}
p=pa;
pa=pa->right;
if (M.chead[p->j]==p) {
M.chead[p->j]=hl[p->j]=p->down;
}else{
hl[p->j]->down=p->down;
}
free(p);
}
}
}
pb=pb->right;
}
}
display(M);
return M;
}
void display(CrossList M){
printf("输出测试矩阵:\n");
printf("M:\n---------------------\ni\tj\te\n---------------------\n");
for (int i=1;i<=M.nu;i++)
{
if (NULL!=M.chead[i])
{
OLink p=M.chead[i];
while (NULL!=p)
{
printf("%d\t%d\t%d\n",p->i,p->j,p->e);
p=p->down;
}
}
}
}
这篇代码我自己跑,没跑成功,等有空的时候再调试一下