数据结构 | 第6章 邻接矩阵

130 阅读4分钟

6.3 邻接矩阵

邻接矩阵(adjacency matrix)是图ADT最基本的实现方式,使用方阵A[n] [n]表示由n个顶点构成的图,其中每个单元,各自负责描述一对顶点之间可能存在的邻接关系,故此得名。

image-20220713165901185.png

实现(代码6.2):

 0001 #include "Vector/Vector.h" //引入向量  
 0002 #include "Graph/Graph.h" //引入图ADT
 0003 
 0004 template <typename Tv> struct Vertex { //顶点对象(为简化起见,并未严格封装)
 0005    Tv data; int inDegree, outDegree; VStatus status; //数据、出入度数、状态
 0006    int dTime, fTime; //时间标签
 0007    Rank parent; int priority; //在遍历树中的父节点、优先级数
 0008    Vertex ( Tv const& d = ( Tv ) 0 ) : //构造新顶点
 0009       data ( d ), inDegree ( 0 ), outDegree ( 0 ), status ( UNDISCOVERED ),
 0010       dTime ( -1 ), fTime ( -1 ), parent ( -1 ), priority ( INT_MAX ) {} //暂不考虑权重溢出
 0011 };
 0012 
 0013 template <typename Te> struct Edge { //边对象(为简化起见,并未严格封装)
 0014    Te data; int weight; EType type; //数据、权重、类型
 0015    Edge ( Te const& d, int w ) : data ( d ), weight ( w ), type ( UNDETERMINED ) {} //构造
 0016 };
 0017 
 0018 template <typename Tv, typename Te> //顶点类型、边类型
 0019 class GraphMatrix : public Graph<Tv, Te> { //基于向量,以邻接矩阵形式实现的图
 0020 private:
 0021    Vector< Vertex< Tv > > V; //顶点集(向量)
 0022    Vector< Vector< Edge< Te > * > > E; //边集(邻接矩阵)
 0023 public:
 0024    GraphMatrix() { n = e = 0; } //构造
 0025    ~GraphMatrix() { //析构
 0026       for ( Rank v = 0; v < n; v++ ) //所有动态创建的
 0027          for ( Rank u = 0; u < n; u++ ) //边记录
 0028             delete E[v][u]; //逐条清除
 0029    }
 0030 // 顶点的基本操作:查询第v个顶点(0 <= v < n)
 0031    virtual Tv& vertex ( Rank v ) { return V[v].data; } //数据
 0032    virtual int inDegree ( Rank v ) { return V[v].inDegree; } //入度
 0033    virtual int outDegree ( Rank v ) { return V[v].outDegree; } //出度
 0034    virtual Rank firstNbr ( Rank v ) { return nextNbr ( v, n ); } //首个邻接顶点
 0035    virtual Rank nextNbr ( Rank v, Rank u ) //相对于顶点u的下一邻接顶点(改用邻接表可提高效率)
 0036    { while ( ( -1 < u ) && ( !exists ( v, --u ) ) ); return u; } //逆向线性试探
 0037    virtual VStatus& status ( Rank v ) { return V[v].status; } //状态
 0038    virtual int& dTime ( Rank v ) { return V[v].dTime; } //时间标签dTime
 0039    virtual int& fTime ( Rank v ) { return V[v].fTime; } //时间标签fTime
 0040    virtual Rank& parent ( Rank v ) { return V[v].parent; } //在遍历树中的父亲
 0041    virtual int& priority ( Rank v ) { return V[v].priority; } //在遍历树中的优先级数
 0042 // 顶点的动态操作
 0043    virtual Rank insert ( Tv const& vertex ) { //插入顶点,返回编号
 0044       for ( Rank u = 0; u < n; u++ ) E[u].insert ( NULL ); n++; //各顶点预留一条潜在的关联边
 0045       E.insert ( Vector<Edge<Te>*> ( n, n, ( Edge<Te>* ) NULL ) ); //创建新顶点对应的边向量
 0046       return V.insert ( Vertex<Tv> ( vertex ) ); //顶点向量增加一个顶点
 0047    }
 0048    virtual Tv remove ( Rank v ) { //删除第v个顶点及其关联边(0 <= v < n)
 0049       for ( Rank u = 0; u < n; u++ ) //所有出边
 0050          if ( exists ( v, u ) ) { delete E[v][u]; V[u].inDegree--; e--; } //逐条删除
 0051       E.remove ( v ); n--; //删除第v行
 0052       Tv vBak = vertex ( v ); V.remove ( v ); //删除顶点v
 0053       for ( Rank u = 0; u < n; u++ ) //所有入边
 0054          if ( Edge<Te> * x = E[u].remove ( v ) ) { delete x; V[u].outDegree--; e--; } //逐条删除
 0055       return vBak; //返回被删除顶点的信息
 0056    }
 0057 // 边的确认操作
 0058    virtual bool exists ( Rank v, Rank u ) //边(v, u)是否存在
 0059    { return ( v < n ) && ( u < n ) && E[v][u] != NULL; }
 0060 // 边的基本操作:查询顶点v与u之间的联边(0 <= v, u < n且exists(v, u))
 0061    virtual EType & type ( Rank v, Rank u ) { return E[v][u]->type; } //边(v, u)的类型
 0062    virtual Te& edge ( Rank v, Rank u ) { return E[v][u]->data; } //边(v, u)的数据
 0063    virtual int& weight ( Rank v, Rank u ) { return E[v][u]->weight; } //边(v, u)的权重
 0064 // 边的动态操作
 0065    virtual void insert ( Te const& edge, int w, Rank v, Rank u ) { //插入权重为w的边(v, u)
 0066       if ( exists ( v, u ) ) return; //确保该边尚不存在
 0067       E[v][u] = new Edge<Te> ( edge, w ); //创建新边
 0068       e++; V[v].outDegree++; V[u].inDegree++; //更新边计数与关联顶点的度数
 0069    }
 0070    virtual Te remove ( Rank v, Rank u ) { //删除顶点v和u之间的联边(exists(v, u))
 0071       Te eBak = edge ( v, u ); delete E[v][u]; E[v][u] = NULL; //备份后删除边记录
 0072       e--; V[v].outDegree--; V[u].inDegree--; //更新边计数与关联顶点的度数
 0073       return eBak; //返回被删除边的信息
 0074    }
 0075 };

时间性能:

按照代码6.2的实现方式,各顶点的编号可直接转换为其在邻接矩阵中对应的秩,从而使得图ADT中所有的静态操作接口,均只需O(1)时间——这主要是得益于向量“循秩访问”的特长与优势。另外,边的静态和动态操作也仅需O(1)时间——其代价是邻接矩阵的空间冗余。

空间性能:

上述实现方式所用空间,主要消耗于邻接矩阵,即其中的二维边集向量E[] []。每个Edge对象虽需记录多项信息,但总体不过常数。根据2.4.4节的分析结论,Vector结构的装填因子始终不低于50%,故空间总量渐进地不超过O(n × n) = O(n2)O(n^2)

当然,对于无向图而言,仍有改进的余地。如图6.5(a)所示,无向图的邻接矩阵必为对称阵,其中除自环以外的每条边,都被重复地存放了两次。也就是说,近一半的单元都是冗余的。为消除这一缺点,可采用压缩存储等技巧,进一步提高空间利用率。

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情