一.介绍

什么是稀疏矩阵,假设我们有一个矩阵
(以下部分回去重新编辑)
[ 0 0 0 0 0 0 0 2 5 0 0 0 ] \begin{bmatrix} 0 & 0 &0 &0\\ 0 &0 & 0 &2\\ 5 &0 & 0 & 0 \end{bmatrix} 005000000020

如果我们使用一个二维数组去存储它,需要开辟4x4的一个二维数组。但是数组里有大量的0,所以我们有没有什么办法去简化它的储存空间,肯定是有的。假设我们用一个一维数组来存储这个三元组{(1,3,2),(2,0,5)}然后并储存上它的尺寸,那么我们就可以用一个一维数组+两个int变量来存储这个矩阵。那么我们就称为这个矩阵为“稀疏矩阵”。

二.实现稀疏矩阵的原理

1.稀疏矩阵的顺序存储

当我们存储一个稀疏矩阵的时候,我们依然可以按行作为顺序或者按列作为顺序。
比如:
[ 0 2 0 4 0 5 7 0 0 ] \begin{bmatrix} 0& 2& 0\\ 4& 0& 5\\ 7& 0& 0 \end{bmatrix} 047200050

这个矩阵,我们按行存储为{(0,1,2),(1,0,4),(1,2,5),(2,0,7)},如果按列储存则为{(1,0,4),(2,0,7),(0,1,2),(1,2,5)}

2.稀疏矩阵的转置T

假设我们有矩阵
[ 1 0 2 0 0 4 0 0 3 0 0 7 ] \begin{bmatrix} 1& 0& 2& 0\\ 0& 4& 0& 0\\ 3& 0& 0& 7 \end{bmatrix} 103040200007

它的稀疏矩阵可以表示为
(0,0,1)
(0,2,2)
(1,1,4)
(2,0,3)
(2,3,7)
那么我们转置后为
[ 1 0 3 0 4 0 2 0 0 0 0 7 ] \begin{bmatrix} 1& 0& 3\\ 0& 4& 0\\ 2& 0& 0\\ 0& 0& 7 \end{bmatrix} 102004003007

其稀疏矩阵
(0,0,1)
(0,2,3)
(1,1,4)
(2,0,2)
(3,2,7)
我们不难发现,在稀疏矩阵的转置中,我们不仅矩阵的内容发生了改变(2,0,3)变成了(0,2,3)。而且在稀疏矩阵中的顺序也发生了改变,比如(2,0,3)从index=3变成了index=1。

3.求转置的方法

在二维数组中,我们求转置很简单,只需要循环赋值,交换一下角标A[m][n],它的转置T[n][m]即可。
在稀疏矩阵中,我们如果要求它的转置,需要交换一下每个元组里的前两个下标,并重新排下序即可(但是排序的时间复杂度高啊)。

4.快速求转置法

在稀疏矩阵中,我们可以观察发现,如果只按序排列的话,我们可以通过哈希表的方式,来通过一次遍历得到它转置后的位置。
先看刚才例子的两个矩阵:假设我们规定前两个下标为(i,j)

rowcolrowcol
001001
022023
114114
203202
237327

不难发现:

  1. 两个的数组都是以i为升序排列的。
  2. 右侧的数组同一个i的下标的元组,顺序一定和左侧的数组顺序相同。

第一个性质很好理解。
第二个性质我们看看:那么左侧数组(0,0,1)在(2,0,3)上面,右侧数组在(0,0,1)也在(0,2,3)上面。
为啥?因为ij的过程中,因为i是依次递增的,所以到了j的时候,j也因该是线性变化的。
所以,即使右侧数组遇见了相同的i下标,那么j坐标是原来左侧的i,也是线性排布。
有了这两个性质,我们就可以用一个哈希表来存储它转置后的位置!
过程:

  1. 第一次循环左侧稀疏矩阵,得到以j下标作为键值的哈希表(就是统计个数)。比如刚才左侧的数组,我们的j下标的哈希表为 Hash[col]:Hash[0] = 2; Hash[1]=1; Hash[2]=1;Hash[3]=1;
  2. 用Hash表生成位置表,也就是我们每个元素开始的次序表:cpot[col]。其中,第0行,cpot[0]=0,然后cpot[col] = cpot[col-1]+Hash[col-1]。
  3. 再来一次循环,不过是从后往前循环,将ji下标进行交换,然后它的位置就是cpot[newi] + Hash[newi]-1。并--Hash[newi]。这样就利用Hash这个数组确定了它的位置。循环完成后,我们的转置就完成了。

5.稀疏矩阵加法(减法同理)

为了时间复杂度更小,我们通过开辟一个新的稀疏矩阵作为结果。并将大小开辟为相加的两个矩阵的和。并采用双指针开始赋值。前提是两个矩阵的形状要一样。

  1. 设置一个ptrA和ptrB,直到ptrA==A.len || ptrB==B.len
  2. 否则,就对比ptrA和ptrB指向的元素的值。
  3. 若相等,则值相加后加入到C并++ptrAB。若不等,判断下标的先后,然后加入到C并++ptrA或ptrB。
  4. 跳出循环后,看A和B数组有谁没有添加完,就全部添加到C。

6.稀疏矩阵的乘法

假设矩阵A为
[ 0 2 0 1 0 5 0 0 4 ] \begin{bmatrix} 0& 2& 0 \\ 1 &0& 5\\ 0 &0& 4 \end{bmatrix} 010200054
(0,1,2)
(1,0,1)
(1,2,5)
(2,2,4)
矩阵B为
[ 6 0 0 7 8 0 ] \begin{bmatrix} 6& 0\\ 0& 7\\ 8& 0 \end{bmatrix} 608070
(0,0,6)
(1,1,7)
(2,0,8)
那么我们的结果是
[ 0 14 46 0 32 0 ] \begin{bmatrix} 0 & 14 \\ 46 & 0\\ 32 & 0 \end{bmatrix} 046321400

算法很简单,C(i,j)我们只需要将A中的所有元素(i,k)找到B中相等的元素(k,j)相乘相加即可。
举例:(0,1,2)就需要在B里找(1,j,?),那就是(1,1,7),那么得C(0,1,14)
(1,0,1)在B找(0,j,?),那就是(0,0,6),得C(1,0,6)
(1,2,5)在B找(2,j,?),那就是(2,0,8),得C(1,0,6+40)
(2,2,4)在B找(2,j,?),那就是(2,0,8),得C(2,0,32)
这样,我们的C就求完了

三.稀疏矩阵的代码定义

1.稀疏矩阵的元素

对于矩阵的一个元素,我们需要存在值value,行下标row,列下表col。那么就定义一个结构体,并开辟三个整数(为了方便,其实值也可以是一个结构体)。

2.稀疏矩阵的定义

一个矩阵,我们肯定要有矩阵的行大小和列大小,且稀疏矩阵因该还有变量存非零元素的个数。
那么我们还是动态生成一段空间储存元素,然后三个变量:mrow mcol nzero
其中我们规定:0 <= row < mrow 0 <= col < mcol

3.稀疏矩阵的快速转置

流程已经在上面原理介绍了,所以不赘述。

4.稀疏矩阵的加减法

流程也描述了,不赘述

5.稀疏矩阵的乘法

具体的算法代码可能会在后文写出。

四.代码实现

#include <stdio.h>
#include <stdlib.h>


#define         OK          1
#define         ERROR       0

typedef struct element{
    int row;
    int col;
    int value;
}element;

typedef struct matrix{
    int mrow;
    int mcol;
    int nzero;
    element *base;
}matrix;        //定义元素和矩阵


matrix *MatInit(void);
int MatShow(matrix *Mat, int way);
matrix *MatOP(matrix *MatA, matrix *MatB, int (*operation)(int a, int b));
int Plus(int a, int b);
int Subtract(int a, int b);
matrix* MatTrans(matrix *Mat);
matrix *MatMult(matrix *MatA, matrix *MatB);

int main()
{
    matrix *mat1 = MatInit();
    //matrix *mat2 = MatInit();
    //matrix *mat3 = MatOP(mat1, mat2, Plus);
    //MatShow(mat3, 0);
    matrix *mat4 = MatTrans(mat1);
    MatShow(mat1, 0);
    MatShow(mat4, 0);

    return 0;
}


matrix *MatInit(void)
{
    int i = 0;
    int row, col, value;

    matrix *Mat = (matrix*)malloc(sizeof(matrix));
    if (!Mat) exit(0); 

    printf("Plase input the row(s) and col(s) of the Matrix and the number of data as int int int:\n"); 
    scanf("%d %d %d", &row, &col, &value);
    Mat->mrow = row;
    Mat->mcol = col;
    Mat->nzero = value;
    if (Mat->mcol <= 0 || Mat->mrow <= 0 || Mat->nzero < 0 || Mat->nzero > (Mat->mrow*Mat->mcol)) exit(1);
    //输入并保存行信息

    Mat->base = (element*)malloc(sizeof(element)*Mat->nzero);
    if (Mat->nzero == 0)
    {
        return Mat;
    }
    printf("\nplease input col row value:\n");
    for (; i < Mat->nzero; ++i)
    {
        scanf("%d %d %d", &row, &col, &value);
        if (row < Mat->mrow && col < Mat->mcol)
        {
            (Mat->base+i)->row = row;
            (Mat->base+i)->col = col;
            (Mat->base+i)->value = value;
        }
        else
        {
            printf("\ninput error\n");
            exit(1);
        }
    }

    return Mat;
}

int MatShow(matrix *Mat, int way)
{
    int i = 0;
    int ptr = 0, row = 0, col = 0;

    if (way == 1)       //  这种输出方式就不多描述了
    {
        for (i = 0; i < Mat->nzero; ++i)
        {
            printf("(%3d,%3d,%3d)\n", (Mat->base+i)->row, (Mat->base+i)->col, (Mat->base+i)->value);
        }
        return OK;
    }
    else
    {
        printf("Matrix:\n");
        while (row < Mat->mrow && col < Mat->mcol && ptr < Mat->nzero)
        // 循环的条件是 还有非零元素未输出或者未超出最大长度
        {
            
            if (row == (Mat->base+ptr)->row && col == (Mat->base+ptr)->col)     // 如果该位置有非零元素
            {
                printf("%3d ", (Mat->base+ptr)->value);
                ++ptr;
            }
            else        // 零元素
            {
                printf("%3d ", 0);
            }
            ++col;
            if (col == Mat->mcol)   // 如果输出完了一行,那么就让col从零开始 并到下一行
            {
                col = 0;
                ++row;
                printf("\n");
            }
        }
        
        while(row < Mat->mrow && col < Mat->mcol)   // 有可能只是非零元素输出完了, 所以再把剩下的0补齐
        {
            printf("%3d ", 0);
            ++col;
            if (col == Mat->mcol)
            {
                col = 0;
                ++row;
                printf("\n");
            }
        }
    }
    
    return OK;
}

int Plus(int a, int b)
{
    return a+b;
}

int Subtract(int a, int b)
{
    return a-b;
}

matrix *MatOP(matrix *MatA, matrix *MatB, int (*operation)(int a, int b))   // 后面是个程序指针,用于传入是加还是减
{
    int ptrA = 0, ptrB = 0, ptrC = 0;
    int mark = 0;
    matrix *MatC = (matrix*)malloc(sizeof(matrix));
    
    if (MatA->mrow != MatB->mrow || MatA->mcol != MatB->mcol)   // 大小不同退出
    {
        printf("two mats have different size!");
        exit(1);
    }

    MatC->mrow = MatA->mrow;
    MatC->mcol = MatA->mcol;
    MatC->nzero = MatA->nzero + MatB->nzero;
    MatC->base = (element*)malloc(sizeof(element)*MatC->nzero);

    while(ptrA < MatA->nzero && ptrB < MatB->nzero)     // 还是遍历整个矩阵
    {
        mark = 0;       // 标记位, 如果是-1说明A有一个非零元素 在B前面
        if ((MatA->base+ptrA)->row < (MatB->base+ptrB)->row)
        {
            mark = -1;
        }
        else if ((MatA->base+ptrA)->row == (MatB->base+ptrB)->row)
        {
            if ((MatA->base+ptrA)->col < (MatB->base+ptrB)->col) // 标记位, 如果是-1说明A有一个非零元素 在B前面
            {
                mark = -1;
            }
            else if ((MatA->base+ptrA)->col == (MatB->base+ptrB)->col) // 标记位, 等于0,说明两个相同位置由非零元素
            {
                mark = 0;
            }
            else
            {
                mark = 1;
            }
        }
        else
        {
            mark = 1;   // 标记位, 等于1,说明B有个非零元素在A前面
        }
        if (mark == -1) // 把A非零元素操作了,并指向A的下一个元素
        {
            (MatC->base+ptrC)->col = (MatA->base+ptrA)->col;
            (MatC->base+ptrC)->row = (MatA->base+ptrA)->row;
            (MatC->base+ptrC)->value = operation(0,(MatA->base+ptrA)->value);
            ++ptrA;
            ++ptrC;
        }
        else if (mark == 0)// 把AB非零元素同时操作了,并指向AB的下一个元素
        {
            (MatC->base+ptrC)->col = (MatA->base+ptrA)->col;
            (MatC->base+ptrC)->row = (MatA->base+ptrA)->row;
            (MatC->base+ptrC)->value = operation((MatB->base+ptrB)->value,(MatA->base+ptrA)->value);
            ++ptrA;
            ++ptrB;
            ++ptrC;
        }
        else     // 把B非零元素操作了,并指向B的下一个元素
        {
            (MatC->base+ptrC)->col = (MatB->base+ptrB)->col;
            (MatC->base+ptrC)->row = (MatB->base+ptrB)->row;
            (MatC->base+ptrC)->value = operation(0,(MatB->base+ptrB)->value);
            ++ptrB;
            ++ptrC;
        }
    }

    while (ptrA < MatA->nzero)  //  可能A有未输出的非零元素,将其全部输出
    {
        (MatC->base+ptrC)->col = (MatA->base+ptrA)->col;
        (MatC->base+ptrC)->row = (MatA->base+ptrA)->row;
        (MatC->base+ptrC)->value = operation(0,(MatA->base+ptrA)->value);
        ++ptrA;
        ++ptrC;
    }
    while (ptrB < MatB->nzero) //  可能B有未输出的非零元素,将其全部输出
    {
        (MatC->base+ptrC)->col = (MatB->base+ptrB)->col;
        (MatC->base+ptrC)->row = (MatB->base+ptrB)->row;
        (MatC->base+ptrC)->value = operation(0,(MatB->base+ptrB)->value);
        ++ptrB;
        ++ptrC;
    }
    
    return MatC;
}

matrix* MatTrans(matrix *Mat)
{
    int i = 0, newi = 0, index;

    int *hash = (int*)malloc(sizeof(int)*Mat->mcol);
    int *cpot = (int*)malloc(sizeof(int)*Mat->mcol);

    matrix* MatNew = (matrix*)malloc(sizeof(matrix));

    MatNew->nzero = Mat->nzero;
    MatNew->mrow = Mat->mcol;
    MatNew->mcol = Mat->mrow;
    MatNew->base = (element*)malloc(sizeof(element)*(MatNew->nzero));

    for (i = 0; i < Mat->mcol; ++i)     //  初始化 hash和起始下标 数组
    {
        hash[i] = 0;
        cpot[i] = 0;
    }

    for (i = 0; i < Mat->nzero; ++i)    // 统计hash
    {
        hash[(Mat->base+i)->col] += 1;
    }

    for (i = 1; i < Mat->mcol; ++i) //  统计cpot
    {
        cpot[i] = cpot[i-1] + hash[i-1];
    }

    for (i = Mat->nzero - 1; i >= 0; --i)   //倒叙输出,将通过hash和cpot同时确定元素位置。
    {
        newi = (Mat->base+i)->col;
        index = cpot[newi]+hash[newi]-1;
        (MatNew->base+index)->row = (Mat->base+i)->col;
        (MatNew->base+index)->col = (Mat->base+i)->row;
        (MatNew->base+index)->value = (Mat->base+i)->value;
        --hash[newi];
    }

    return MatNew;
}

Logo

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

更多推荐