数据结构学习笔记--稀疏矩阵的压缩存储

 
先简单介绍一下概念。设一个m行n列的矩阵具有n个值不等于零的元素,则称t/(m*n)为该矩阵的稀疏因子。通常称稀疏因子小于0.5的矩阵为稀疏矩阵(sparse matrix)。
由于零元素较多,再使用通常的2维数组表示会造成存储空间的浪费,同时在计算时所有零元也要参与运算,导致效率很低。所以有必要研究如何存储稀疏矩阵,即压缩存储。书上这里介绍了3种存储方式,其实第二种行链接的方式是顺序存储的改进方法,二者可以归到一起。由于前两种存储方式实现起来非常简单,书上讲得很细,这里就不啰嗦了,下面主要介绍第三种实现:十字链表。
十字链表是这样构成的:用链表模拟矩阵的行(或者列,这可以根据个人喜好来定),然后再构造代表列的链表,将每一行中的元素节点插入到对应的列中去。打个比方吧:这个十字链表的逻辑结构就像是一个围棋盘(没见过,你就想一下苍蝇拍,这个总见过吧),而非零元就好像是在棋盘上放的棋子,总共占的空间就是,确定那些线的表头节点和那些棋子代表的非零元节点。最后,我们用一个指针指向这个棋盘,这个指针就代表了这个稀疏矩阵。
现在,让我们看看非零元节点最少需要哪几个域,val是必须的,为了把线画下去还要有down、right,好像不需要别的了。但为了提高加法与乘法的效率,还要在每个节点增加两个值row,col分别表示节点所在的行、列。再看看表头节点,由于是链表的表头节点,所以就和后边的节点一样了。然后,行链表和列链表的表头节点实际上也各构成了一个链表。最后,我们只需要存储两个分别指向行表头与列表头节点的指针,就可以访问稀疏矩阵了。具体表示如下图:

 

为了能随机访问任意一行或一列,这里将头结点存放在一维数组中。下面是节点的定义:
 
class OLNode {
public:
    
int row,col,val;
    OLNode
* right;
    OLNode
* down;
    OLNode(
int i = 0int j = 0int v = 0,
        OLNode
* rgt = NULL, OLNode* dwn = NULL)
        : row(i), col(j), val(v), right(rgt), down(dwn) {}
    friend std::ostream
& operator<<(std::ostream& os,
        OLNode
& eom)
    {
        os 
<< "(" << eom.row << "," << eom.col
            
<< "," << eom.val << ")";
        
return os;
    }
};
 
下边会依次介绍矩阵的转置、加法和乘法,但在这之前先来设计一下类的输出输出接口,一个好的接口能为接下来的运算提供许多方便。除了通常的重载>>与<<之外,为了乘法实现的简便还需要重载=,这在下面会看到:
 
class SMatrix_OL {
    
int maxRows, maxCols, total;
    OLNode
* pRowHead;
    OLNode
* pColHead;
    
void initialize() {
        maxRows 
= maxCols = total = 0;
        pRowHead 
= pColHead = NULL;
    }
    
void clear();
    
void copy(const SMatrix_OL& sm);
    friend 
const SMatrix_OL 
        vectorMultiply(OLNode
* colVector, OLNode* rowVector, 
        
int rows, int cols);
public:
    SMatrix_OL() { initialize(); }
    
//初始化一个i行j列的零矩阵
    SMatrix_OL(int i, int j) {
        require(i 
> 0 && j > 0
            
"SMatrix_OL::SMatrix illegal matrix!");
        maxRows 
= i;
        maxCols 
= j;
        total 
= 0;
        pRowHead 
= new OLNode[maxRows + 1];
        pColHead 
= new OLNode[maxCols + 1];
    }
    SMatrix_OL(
const SMatrix_OL& sm) {
        copy(sm);
    }
    SMatrix_OL
& operator=(const SMatrix_OL& sm) {
        
if (maxRows) clear();
        copy(sm);
        
return *this;
    }
    
~SMatrix_OL() {
        clear();
    }
    friend std::ostream
& operator<<(std::ostream& os, 
        
const SMatrix_OL& sm) {
        
for (int i = 1; i <= sm.maxRows; i++) {
            OLNode
* p = sm.pRowHead[i].right;
            
if (i == 1)
                os 
<< "/";
            
else if (i == sm.maxRows)
                os 
<< "/";
            
else os << "|";
            
for (int j = 1; j <= sm.maxCols; j++) {
                
int val;
                
if (p == NULL || p->col > j)
                    val 
= 0;
                
else {
                    val 
= p->val;
                    p 
= p->right;
                }
                os.setf(std::ios::right, std::ios::adjustfield);
                os.width(
4);
                os 
<< val;
            }
            os.setf(std::ios::right, std::ios::adjustfield);
            os.width(
4);
            
if (i == 1)
                os 
<< "/";
            
else if (i == sm.maxRows)
                os 
<< "/";
            
else os << "|";
            os 
<< std::endl;
        }
        
return os;
    }
    friend std::istream
& operator>>(std::istream& is,
        SMatrix_OL
& sm) {
        cout 
<< "创建稀疏矩阵:" << endl;
        cout 
<< "输入行数、列数和非零元个数:" << endl;
        
is >> sm.maxRows >> sm.maxCols >> sm.total;
        require(sm.maxRows 
> 0 && sm.maxCols > 0 && sm.total >= 0,
            
"SMatrix_OL::operator>> illegal input!");
        sm.pRowHead 
= new OLNode[sm.maxRows + 1];
        sm.pColHead 
= new OLNode[sm.maxCols + 1];
        
for (int n = 1; n <= sm.total; n++) {
            cout 
<< "输入第" << n << "个非零元:" << endl;
            
int i, j, val;
            
is >> i >> j >> val;
            require(i 
> 0 && j > 0,
                
"SMatrix_OL::operator>> illegal input!");
            OLNode
* pNew = new OLNode(i, j, val);
            OLNode
* prior = &sm.pRowHead[i];
            
while (prior->right != NULL && prior->right->col < j)
                prior 
= prior->right;
            pNew
->right = prior->right;
            prior
->right = pNew;//行链接完毕
            prior = &sm.pColHead[j];
            
while (prior->down != NULL && prior->down->row < i)
                prior 
= prior->down;
            pNew
->down = prior->down;
            prior
->down = pNew;//列链接完毕
        }
        cout 
<< "稀疏矩阵创建完毕!" << endl;
        
return is;
    }
};
void SMatrix_OL::clear() {
        
for (int i = 1; i <= maxRows; i++) {
            
if (pRowHead[i].right == NULL) continue;
            OLNode
* p = pRowHead[i].right;
            
//删除当前行的所有结点
            while (p != NULL) {
                OLNode
* q = p;
                p 
= p->right;
                delete q;
            }
        }
        delete []pRowHead;
        delete []pColHead;
}
void SMatrix_OL::copy(const SMatrix_OL& sm) {
        maxCols 
= sm.maxCols;
        maxRows 
= sm.maxRows;
        total 
= sm.total;
        pRowHead 
= new OLNode[maxRows + 1];
        pColHead 
= new OLNode[maxCols + 1];
        OLNode
** pUpNode = new OLNode*[maxCols + 1];
        
for (int n = 1; n <= maxCols; n++)
            pUpNode[n] 
= &pColHead[n];
        
for (int i = 1; i <= maxRows; i++) {
            
if (sm.pRowHead[i].right == NULL) continue;
            OLNode
* prior = &pRowHead[i];
            OLNode
* pOld = sm.pRowHead[i].right;
            
while (pOld != NULL) {
                OLNode
* pCopy = new 
                    OLNode(i, pOld
->col, pOld->val);
                prior 
= prior->right = pCopy;
                pUpNode[pOld
->col] = 
                    pUpNode[pOld
->col]->down = pCopy;
                pOld 
= pOld->right;
            }
        }
}
 
这些都是很常规的实现,不再细说了。首先是转置运算,如果按照一般的方法需要在转置的同时改动很多指针的指向,比较麻烦,这里使用的方式是先把矩阵复制一个临时副本,然后清除原来的矩阵,再以列序从副本复制回来,则现在的矩阵就是原来的转置。代码如下:
 
//先把自己复制给sm,然后把自己构造成sm的转置
void Transpose() {
    SMatrix_OL sm 
= *this;
    clear();
    maxRows 
= sm.maxCols;
    maxCols 
= sm.maxRows;
    total 
= sm.total;
    pRowHead 
= new OLNode[maxRows + 1];
    pColHead 
= new OLNode[maxCols + 1];
    OLNode
** pUpNode = new OLNode*[maxCols + 1];
    
for (int n = 1; n <= maxCols; n++)
        pUpNode[n] 
= &pColHead[n];
    
for (int j = 1; j <= sm.maxCols; j++) {
        
if (sm.pColHead[j].down == NULL) continue;
        OLNode
* prior = &pRowHead[j];
        OLNode
* pOld = sm.pColHead[j].down;
        
while (pOld != NULL) {
            OLNode
* pNew = new OLNode(j, pOld->row, pOld->val);
            prior 
= prior->right = pNew;
            pUpNode[pNew
->col] = 
                pUpNode[pNew
->col]->down = pNew;
            pOld 
= pOld->down;
        }
    }
}
const SMatrix_OL operator~() {
    SMatrix_OL sm 
= *this;
    sm.Transpose();
    
return SMatrix_OL(sm);
}
friend 
const SMatrix_OL 
    
operator+(const SMatrix_OL& smA, const SMatrix_OL& smB) {
    require(smA.maxRows 
== smB.maxRows && 
        smA.maxCols 
== smB.maxCols,
        
"SMatrix_OL::operator+ cannot add!");
    SMatrix_OL smC 
= smA;
    smC.Add(smB);
    
return SMatrix_OL(smC);
}
 
OK,该本篇文章的压轴算法出场了^_^那就是乘法,书上根本就没有提到十字链表的乘法,原因就是太麻烦了。你想啊,每个节点有两个指针域,而且在做乘法运算时一个矩阵是按行访问,另一个是按列访问,乘积矩阵中的每一个元素都是一行与一列的乘积。我做到这里时头都大了,于是我又想能不能换一种方式:如果矩阵A是行矢量,矩阵B是列矢量,那么A*B很好实现。而一个普通的矩阵可以分解成由子矩阵组成的矢量,所以把A变成行矢量,把B变成列矢量,那么乘积矩阵可由n个子矩阵相加组成,其中n等于A的列数(或B的行数)。见下图:
 

这样一来,乘法就容易实现了:
friend const SMatrix_OL 
    operator
*(const SMatrix_OL& smA, const SMatrix_OL& smB) {
    require(smA.maxCols 
== smB.maxRows, 
        
"SMatrix_OL operator* cannot mult!");
    SMatrix_OL smC(smA.maxRows, smB.maxCols);
    
//smC = A1*B1+A2*B2+...+An*Bn,n=smA.maxCols=smB.maxRows
    for (int n = 1; n <= smA.maxCols; n++)
        smC.Add(
            vectorMultiply(
            smA.pColHead[n].down, 
            smB.pRowHead[n].right, 
            smA.maxRows, smB.maxCols));
    
return SMatrix_OL(smC);
}
其中的核心就是私有成员函数vectorMultiply,将两个矢量相乘,其实现也很简单:
const SMatrix_OL 
        vectorMultiply(OLNode
* colVector, 
        OLNode
* rowVector, int rows, int cols) 
{
    SMatrix_OL sm(rows, cols);
    
if (colVector != NULL && rowVector != NULL) {
        OLNode
** pUpNode = new OLNode*[sm.maxCols + 1];
        
for (int n = 1; n <= sm.maxCols; n++)
            pUpNode[n] 
= &sm.pColHead[n];
        
//行、列矢量相乘
        for (; colVector != NULL; colVector = colVector->down) {
            OLNode
* prior = &sm.pRowHead[colVector->row];
            
for (OLNode* pCur = rowVector; 
                pCur 
!= NULL; pCur = pCur->right) 
            {
                OLNode
* pNew = new 
                    OLNode(colVector
->row, 
                    pCur
->col, colVector->val * pCur->val);
                prior 
= prior->right = pNew;
                pUpNode[pNew
->col] = 
                    pUpNode[pNew
->col]->down = pNew;
                sm.total
++;
            }
        }
    }
    
return SMatrix_OL(sm);
}
 
下面该测试了,这里同样使用了sstream文件,其功能在上一篇文章已经说过了。
 
#include "SMatrix.h"
#include 
<iostream>
#include 
<sstream>
using namespace std;
int main() {
    stringstream input(
"
        3 2 6 
        
1 1 1 1 2 2 2 1 3 2 2 4 3 1 5 3 2 6 
    
");
    SMatrix_OL A;
    input 
>> A;
    cout 
<< "A =" << endl << A;
    SMatrix_OL B 
= A;
    cout 
<< "B =" << endl << B;
    cout 
<< "A + B =" << endl << A + B;
    cout 
<< "B's transpose matrix = " << endl << ~B;
    cout 
<< "A * ~B =" << endl << A * ~B;
    
return 0;
}
 
可以看出,重载了一些符号之后使得矩阵的运算与内部类型就很相似了,下面是运行结果:
 

 
好了,第五章到此就结束了。本来后面还有广义表,但由于最近时间比较紧张就不打算再写了。这篇文章也是很匆忙地写完的,总觉得有些仓促,以后有时间会再修改一下,毕竟十字链表不比前几章学过的线性表,在实现上要复杂一些,包括下一节的广义表也是。虽然这部分内容不是重点,但我觉得系统地学习一下还是有好处的,可以为后面学习树打下基础,因为树的大部分运算都是递归的,并且其中的二叉树与十字链表一样有两个指针域。下一篇开始介绍第六章--树,也就是数据结构这门课程最重要的章节~预计分3篇文章,如果时间足够的话可能会把广义表补上,但广义表我又是在想不出一个好的应用,只写基本操作也没什么意思,到时候再说吧,呵呵~
最后说几句题外话,最近忙主要是因为把精力都花在学外语上了。这个星期是我们学校的英语周,从美国来了10多个老外,每天晚上都有讲座或者是娱乐活动。我每天下午都会去找他们聊天,练习口语,感觉这个星期收获很大,刚开始的时候只能一个词一个词的往外蹦,现在进本上能一段一段的说了!并且与一个叫Christ的老头儿交上了朋友,今天我去送得他,还送给他一个自己做得小礼物。老外很有意思,他们和你说话时面部表情特别丰富,还是不是加上许多肢体动作,让人感觉很亲切,呵呵~

没有更多推荐了,返回首页