6.3 邻接矩阵
邻接矩阵(adjacency matrix)是图ADT最基本的实现方式,使用方阵A[n] [n]表示由n个顶点构成的图,其中每个单元,各自负责描述一对顶点之间可能存在的邻接关系,故此得名。
实现(代码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) = 。
当然,对于无向图而言,仍有改进的余地。如图6.5(a)所示,无向图的邻接矩阵必为对称阵,其中除自环以外的每条边,都被重复地存放了两次。也就是说,近一半的单元都是冗余的。为消除这一缺点,可采用压缩存储等技巧,进一步提高空间利用率。
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情”