严蔚敏《数据结构》(C语言版)【教材精讲+考研真题解析】讲义与视频课程【36小时高清视频】
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

5.3 矩阵的压缩存储

视频二维码(扫码观看)

在科学与工程计算问题中,矩阵是一种常用的数学对象,在高级语言编程时,通常将一个矩阵描述为一个二维数组。这样,可以对其元素进行随机存取,各种矩阵运算也非常简单。

压缩存储:

多个相同的非零元素只分配一个存储空间;

零元素不分配空间。

一、特殊矩阵

特殊矩阵是指值相同的元素或者0元素有规律分布的矩阵。

1对称矩阵

若一个n阶方阵A=(aijn×n中的元素满足性质:aij=aji,其中1≤i,j≤n则称A为对称矩阵。

对称矩阵中的元素关于主对角线对称,因此,给每一对对称元素aij和aji(i≠j)分配一个存储空间,则n2个元素压缩存储到n(n+1)/2个存储空间,能节约近一半的存储空间。

不失一般性,假设按“行优先顺序”存储下三角形(包括对角线)中的元素。

设用一维数组(向量)sa[n(n+1)/2]存储n阶对称矩阵。为了便于访问,矩阵元素aij和向量sa[k]之间的对应关系为

对于矩阵中的任意元素aij,均可在一维数组sa中唯一确定其位置k;反之,对所有k=1,2,…,n(n+1)/2,都能确定sa[k]中的元素在矩阵中的位置(i,j)。则称sa[n(n+1)/2]为n阶对称矩阵A的压缩存储。

图5-3 对称矩阵的压缩存储

2三角矩阵

对称矩阵的压缩存储方式同样适用于三角矩阵。

上三角矩阵的下三角(不包括主对角线)中的元素均为常数c(一般为0)。只需在对称矩阵存储基础上,增加常数c的存储空间。

3对角矩阵

矩阵中,除了主对角线和主对角线上或下方若干条对角线上的元素之外,其余元素皆为零。即所有的非零元素集中在以主对角线为了中心的带状区域中。

对角矩阵可按行优先顺序或对角线顺序,将其压缩存储到一个向量中,并且也能找到每个非零元素和向量下标的对应关系。

上述各种特殊矩阵,其非零元素的分布都是有规律的,因此总能找到一种方法将它们压缩存储到一个向量中,并且一般都能找到矩阵中的元素与该向量的对应关系,通过这个关系,仍能对矩阵的元素进行随机存取。

二、稀疏矩阵

【定义】对于稀疏矩阵,目前还没有一个确切的定义。设矩阵A是一个n×m的矩阵中有s个非零元素,设δ=s/(n×m),称δ为稀疏因子,如果某一矩阵的稀疏因子δ满足δ≤0.05时称为稀疏矩阵。

对于稀疏矩阵,采用压缩存储方法时,只存储非0元素。必须存储非0元素的行下标值、列下标值、元素值。因此,一个三元组(i,j,aij)唯一确定稀疏矩阵的一个非零元素。

稀疏矩阵:

其三元组线性表为:((1,2,12),(1,3,9),(3,1,-3),(3,8,4),(4,3,24),(5,2,18),(6,7,-7),(7,4,-6))。

1三元组顺序表

若以行序为主序,稀疏矩阵中所有非0元素的三元组,就可以构成该稀疏矩阵的一个三元组顺序表。相应的数据结构定义如下:

(1)三元组结点定义

#define MAX_SIZE 101
typedef struct
{
    int row;/*行下标*/
    int col;/*列下标*/
    ElemType value;/*元素值 */
}Triple;

(2)三元组顺序表定义

typedef struct
{
    int rn;/*行数*/
    int cn;/*列数*/
    int tn;/*非0元素个数*/
    Triple data[MAX_SIZE+1];//非零元三元组表,data[0]未用
}TSMatrix;

矩阵的运算包括矩阵的转置、矩阵求逆、矩阵的加减、矩阵的乘除等。在此,先讨论在这种压缩存储结构下的求矩阵的转置的运算。

一个m×n的矩阵A,它的转置B是一个n×m的矩阵,且b[i][j]=a[j][i],0≤i≤n,0≤j≤m,即B的行是A的列,B的列是A的行。

设稀疏矩阵A是按行优先顺序压缩存储在三元组表a.data中,若仅仅是简单地交换a.data中i和j的内容,得到三元组表b.data,b.data将是一个按列优先顺序存储的稀疏矩阵B,要得到按行优先顺序存储的b.data,就必须重新排列三元组表b.data中元素的顺序。

图5-4 稀疏矩阵A分别按行、列优先顺序压缩

求转置矩阵的基本算法思想是:

将矩阵的行、列下标值交换。即将三元组表中的行、列位置值i、j相互交换;

重排三元组表中元素的顺序。即交换后仍然是按行优先顺序排序的。

方法一:

算法思想:按稀疏矩阵A的三元组表a.data中的列次序依次找到相应的三元组存入b.data中。

每找转置后矩阵的一个三元组,需从头至尾扫描整个三元组表a.data。找到之后自然就成为按行优先的转置矩阵的压缩存储表示。

算法描述:

void TransMatrix(TSMatrix a,TSMatrix b)
{
    int p,q,col;
    b.rn=a.cn;
    b.cn=a.rn;
    b.tn=a.tn;/*置三元组表b.data的行、列数和非0元素个数*/
    if(b.tn==0)
        printf("The Matrix A=0\n");
    else
    {
        q=0;
        for(col=1;col<=a.cn;col++)/*每循环一次找到转置后的一个三元组*/
            for(p=0;p<a.tn;p++)/*循环次数是非0元素个数*/
                if(a.data[p].col==col)
                {
                    b.data[q].row=a.data[p].col;
                    b.data[q].col=a.data[p].row;
                    b.data[q].value=a.data[p].value;
                    q++;
                }
    }
}

算法分析:本算法主要的工作是在p和col的两个循环中完成的,故算法的时间复杂度为O(cn×tn),即矩阵的列数和非0元素的个数的乘积成正比。

而一般传统矩阵的转置算法为:

for(col=1;col<=n;++col)
    for(row=0;row<=m;++row)
        b[col][row]=a[row][col];

其时间复杂度为O(n×m)。当非零元素的个数tn和m×n同数量级时,算法TransMatrix的时间复杂度为O(m×n2)。

由此可见,方法一虽然节省了存储空间,但时间复杂度却大大增加。所以上述算法只适合于稀疏矩阵中非0元素的个数tn远远小于m×n的情况。

视频二维码(扫码观看)

方法二(快速转置的算法)

算法思想:直接按照稀疏矩阵A的三元组表a.data的次序依次顺序转换,并将转换后的三元组放置于三元组表b.data的恰当位置。

前提:若能预先确定A中每一列的(即B中每一行)第一个非0元素在b.data中应有的位置,则在作转置时就可直接放在b.data中恰当的位置。因此,应先求得A中每一列的非0元素个数。

附设两个辅助数组num[ ]和cpot[ ]。

num[col]:统计A中第col列中非0元素的个数;

cpot[col]:指示A中第col列第一个非0元素在b.data中的恰当位置。

显然有位置对应关系:

cpot[1]=1

cpot[col]=cpot[col-1]+num[col-1],其中2≤col≤a.cn

矩阵A的num[col]和cpot[col]的值,如表5-1所示。

表5-1 num[col]和cpot[col]的值表

算法描述:

void FastTransMatrix(TMatrix a,TMatrix b)
{
    int p,q,col,k;
    int num[MAX_SIZE],copt[MAX_SIZE];
    b.rn=a.cn;
    b.cn=a.rn;
    b.tn=a.tn;/*置三元组表b.data的行、列数和非0元素个数*/
    if(b.tn==0)
        printf("The Matrix A=0\n");
    else
    {
        for(col=1;col<=a.cn;++col)
            num[col]=0;/*向量num[]初始化为0*/
        for(k=1;k<=a.tn;++k)
            ++num[a.data[k].col];/*求原矩阵中每一列非0元素个数*/
        for(cpot[0]=1,col=2;col<=a.cn;++col)
            cpot[col]=cpot[col-1]+num[col-1];/*求第col列中第一个非0元在b.data中的序号*/
        for(p=1;p<=a.tn;++p)
        {
            col=a.data[p].col;
            q=cpot[col];
            b.data[q].row=a.data[p].col;
            b.data[q].col=a.data[p].row;
            b.data[q].value=a.data[p].value;
            ++cpot[col];/*至关重要,本列中其他非0元素 */
        }
    }
}

2行逻辑链接的三元组顺序表

将上述方法二中的辅助数组cpot[ ]固定在稀疏矩阵的三元组表中,用来指示“行”的信息。得到另一种顺序存储结构:行逻辑链接的三元组顺序表。其类型描述如下:

#define MAX_ROW 100
typedef struct
{
    Triple data[MAX_SIZE+1];/*非0元素的三元组表*/
    int rpos[MAX_ROW+1];/*各行第一个非0位置表*/
    int rn,cn,tn;/*矩阵的行、列数和非0元个数*/
}RLSMatrix;

稀疏矩阵的乘法

设有两个矩阵:A=(aijm×n,B=(bijn×p,则C=(cijm×p,其中cij=∑aik×bkj,1≤k≤n,1≤i≤m,1≤j≤p

经典算法是三重循环:

for(i=1;i<=m;++i)
    for(j=1;j<=p;++j)
    {
        c[i][j]=0;
        for(k=1;k<=n;++k)
            c[i][j]=c[i][j]+a[i][k]×b[k][j];
    }

此算法的复杂度为O(m×n×p)。

设有两个稀疏矩阵A=(aijm×n,B=(bijn×p,其存储结构采用行逻辑链接的三元组顺序表。

算法思想:对于A中的每个元素a.data[p](p=1,2,…,a.tn),找到B中所有满足条件:

a.data[p].col=b.data[q].row的元素b.data[q],求得a.data[p].value×b.data[q].value,该乘积是cij中的一部分。求得所有这样的乘积并累加求和就能得到cij

为得到非0的乘积,只要对a.data[1…a.tn]中每个元素(i,k,aik)(1≤i≤a.rn,1≤k≤a.cn),找到b.data中所有相应的元素(k,j,bkj)(1≤k≤b.rn,1≤j≤b.cn)相乘即可。则必须知道矩阵B中第k行的所有非0元素,而b.rpos[ ]向量中提供了相应的信息。

b.rpos[row]指示了矩阵B的第row行中第一个非0元素在b.data[ ]中的位置(序号),显然,b.rpos[row+1]-1指示了第row行中最后一个非0元素在b.data[ ]中的位置(序号)。最后一行中最后一个非0元素在b.data[ ]中的位置显然就是b.tn。

算法描述:

void MultsMatrix(RLSMatrix a,RLSMatrix b,RLSMatrix c)
{/*求矩阵A、B的积C=A×B,采用行逻辑链接的顺序表*/
    ElemType ctemp[Max_Size];
    int p,q,arow,ccol,brow,t;
    if(a.cn!=b.rn)
    {
        printf("Error\n");
        exit(0);
    }
    else
    {
        c.rn=a.rn;
        c.cn=b.n;
        c.tn=0;/*初始化C*/
        if(a.tn*b.tn!=0)
        {/*C是非零矩阵*/
            for(arow=1;arow<=a.rn;++arow)
            {
                ctemp[arow]=0;/*当前行累加器清零*/
                c.rpos[arow]=c.tn+1;
                p=a.rops[arow];
                for(;p<a.rpos[arow+1];++p)
                {/*对第arow行的每一个非0元素*/
                    brow=a.data[p].col;/*找到元素在b.data[]中的行号*/
                    if(brow<b.cn)
                        t=b.rpos[brow+1];
                    else
                        t=b.tn+1;
                    for(q=b.rpos[brow];q<t;++q)
                    {
                        ccol=b.data[q].col;/*乘积元素在c中的列号*/
                        ctemp[ccol]+=a.data[p].value*b.data[q].value;
                    }
                }/*求出c中第arow行中的非0元素*/
                for(ccol=1;ccol<=c.cn;++ccol)
                {
                    if(ctemp[ccol]!=0)
                    {
                        if(++c.tn>MAX_SIZE)
                        {
                            printf("Error\n");
                            exit(0);
                        }
                        else

                            c.data[c.tn]=(arow,ccol,ctemp[ccol]);
                    }
                }
            }
        }
    }
}

视频二维码(扫码观看)

3十字链表

对于稀疏矩阵,当非0元素的个数和位置在操作过程中变化较大时,采用链式存储结构表示,比三元组的线性表更方便。

稀疏矩阵的十字链表存储表示:

typedef struct Clnode
{
    int row,col;/*行号和列号*/
    ElemType value;/*元素值*/
    struct CLNode *down,*right;//该非0元素所在的行表和列表的后继链域
}OLNode;/*非0元素结点*/
typedef struct
{
    int rn;/*矩阵的行数*/
    int cn;/*矩阵的列数*/
    int tn;/*非0元素总数*/
    OLink *chead,*rhead;//行、列表头指针向量,基址由CreateSMatrix分配
}CrossList;

由定义知,稀疏矩阵中同一行的非0元素的由right指针域链接成一个行链表,由down指针域链接成一个列链表。则每个非0元素既是某个行链表中的一个结点,同时又是某个列链表中的一个结点,所有的非0元素构成一个十字交叉的链表。称为十字链表。

此外,还可用两个一维数组分别存储行链表的头指针和列链表的头指针。

对于稀疏矩阵M:

对应的十字交叉链表如图5-5所示。

图5-5 稀疏矩阵M的十字交叉链表