图是一种很常见的结构,日常生活中的许多东西都可以视为一个图结构,比如各个公交站点以及他们之间的通路;互联网中联结于IP节点之间的路由等。二叉树也可以认为是一种图,但二元关系仅限于父子关系。一般性的二元关系,属于非线性结构,图结构中的算法,也大多通过像二叉树中的方法一样,通过某种方式得到元素之间的次序,转换为半线性结构。

图可以定义为G=(V,E),其中V中的元素顶点称为顶点,E中的元素对应V中的一对顶点(u,v),表示他们之间的某种关系,称为边。

无向图、有向图、混合图:如果边(u,v)中的次序是无所谓的,那么称作是无向边,如果u和v是不对等的,(u,v)与(v,u)的含义不同,称作是有向边。约定有向边(v,u)是从v指向u,v称作边的起点,v称作边的终点。如果一个图中的边均为无向边,则G称作无向图。如果一个图中的边均为有向边,G称作有向图。无向边和有向边同时存在的图,是一个混合图。

:对于任何边e=(u,v),顶点v与u彼此邻接,互为邻居,他们都与边e关联。无向图中,与顶点v关联的边数,成为v的度数(degree)。

对于有向边e=(u,v),e称为u的出边,v的入边。v出边的总数称为出度(out-degree),入边总数称为入度(in-degree)。

简单图:联接于同一顶点的边,称为自环(self-loop)。不含任何自环的图称为简单图。

通路和环路:通路或者路径(path),是由m+1个顶点和m条边组成的一个序列,这些边首尾相连,沿途边的总数,称为通路的长度。尽管边互异,但是顶点可以重复。沿途顶点互异的通路,称为简单通路。特别地,如果起止顶点相同,称为环路(cycle),长度也定义为沿途边的总数。不含任何环路的有向图,称为有向无环图(directed acyclic graph)。经过图中各边一次且恰好一次的环路,称为欧拉环路(Eulerian tour);对偶地,经过图中各个顶点恰好一次的环路,称为汉密尔顿环路(Hamiltonian tour)。

下面,定义Graph的模板类:

 1 #include<vector>
 2 #include<queue>
 3 #include<stack>
 4 using std::stack;
 5 using std::queue;
 6 typedef enum {UNDISCOVERED,DISCOVERED,VISITED} VStatus;//顶点的状态
 7 typedef enum { UNDETERMINED, TREE, CROSS, FORWARD, BACKWARD } EType;
 8 template<typename Tv,typename Te>//顶点类型和边类型
 9 class Graph {
10 private:
11     void reset()//所有顶点和边的辅助信息复位
12     {
13         for (int i = 0; i < n; i++)
14         {
15             status(i) = UNDISCOVERED; dTime(i) = fTime(i) = -1;//状态和时间标签
16             parent(i) = -1; priority(i) = INT_MAX;//父节点以及优先级
17             for (int j = 0; j < n; j++)
18                 if (exist(i, j)) type(i, j) = UNDETERMINED;
19         }
20     }
21     void BFS(int, int&);//(连通域)广度优先搜索
22     void DFS(int, int&);//(连通域)深度优先搜索
23     void BCC(int, int&, stack<int>&);//(连通域)基于DFS的双连通分量分解算法
24     bool TSort(int, int&, stack<Tv>*);//(连通域)基于DFS的拓扑排序
25     template<typename PU> void PFS(int, PU);//(连通域)优先级搜索框架
26 public:
27     int n;//顶点数
28     virtual int insert(Tv const&) = 0;//插入节点,返回编号
29     virtual Tv remove(int) = 0;//删除节点以及关联边,返回该节点的信息
30     virtual Tv& vertex(int) = 0;//顶点v的数据(前提是该顶点确实存在)
31     virtual int inDegree(int) = 0;//入度
32     virtual int outDegree(int) = 0;//出度
33     virtual int firstNbr(int) = 0;//首个邻接顶点
34     virtual int nextNbr(int, int) = 0;//i相对于j的下一个邻接顶点
35     virtual VStatus& status(int) = 0;//顶点v的状态
36     virtual int& dTime(int) = 0;//时间标签dTime
37     virtual int& fTime(int) = 0;//时间标签fTime
38     virtual int& parent(int) = 0;
39     virtual int& priority(int) = 0;//在遍历树中的优先级
40     //
41     int e;//边数
42     //virtual void insert(int, int) = 0;
43     virtual Te remove(int, int) = 0;
44     virtual EType& type(int, int) = 0;//边的类型
45     virtual Te& edge(int, int) = 0;//边的数据
46     virtual int& weight(int, int) = 0;//边的权重
47     //algorithm
48     void bfs(int);
49     void dfs(int);
50     void bcc(int);
51     stack<Tv>* tSort(int);
52     void prim(int);
53     void dijkstra(int);
54     template<typename PU> void pfs(int, PU);//优先级搜索框架
55 };

对于图结构具体的实现,通常有两种方式:一种是邻接矩阵,一种是邻接表。

邻接矩阵使用方阵A[n][n]表示由n个顶点组成的图。其中,节点用向量的形式线性存储,对于有向图,A[u][v]为1可以表示边(u,v)存在,即每条边都对应了矩阵中的一个元素。对于无向图,一条边存在可以看作是两条有向边都存在,即A[u][v]=A[v][u]=1。显然,邻接矩阵需要使用O(n^2)的空间来存储各条边,当各个顶点直接的联接并不紧密的时候,会造成大量的浪费。

邻接表在表示顶点上与邻接矩阵相同,不同之处在于,边使用向量+链表的结构来表示。所有顶点作为各自链表的首节点存放于一个向量结构中,对于顶点u的出边,可以用u->v->t这样的链表,来表示(u,v)和(u,t)边的存在。对于边比较稀疏的情况,空间复杂度会大大降低,总的来说为O(n+e)。以下是邻接矩阵的简单实现:

 1 #include"Graph.h"
 2 using namespace std;
 3 template<typename Tv> struct Vertex 
 4 {
 5     Tv data; int outDegree, inDegree; VStatus status;
 6     int dTime, fTime; int parent; int priority;
 7     Vertex(Tv const& d=(Tv) 0):data(d),inDegree(0),outDegree(0),status(UNDISCOVERED),
 8         dTime(-1), fTime(-1),parent(-1), priority(INT_MAX) {}
 9 };
10 
11 template<typename Te> struct Edge
12 {
13     Te data; int weight; EType type;
14     Edge(Te const& d, int w) :data(d), weight(w), type(UNDETERMINED) {}
15 };
16 
17 template<typename Tv, typename Te> class GraphMatrix : public Graph<Tv, Te>
18 {
19 private:
20     vector< Vertex<Tv> > V;
21     vector< vector< Edge<Te>* > > E;
22 public:
23     GraphMatrix() { n = e = 0; }
24     ~GraphMatrix()
25     {
26         for (int i = 0; i < n; i++)
27             for (int j = 0; j < n; j++)
28                 delete E[i][j];
29     }
30     //顶点的查询操作,第i个顶点
31     virtual Tv& vertex(int i) { return V[i].data; }
32     virtual int inDegree(int i) { return V[i].inDegree; }
33     virtual int outDegree(int i) { return V[i].outDegree; }
34     virtual int firstNbr(int i) { return nextNbr(i, n); }//i从最后一个顶点开始寻找邻接顶点
35     virtual int nextNbr(int i, int j) //相对于顶点j的下一个邻接顶点
36     {
37         while ((j > -1) && (!exist(i, --j))); return j;
38     }
39     virtual VStatus& status(int i) { return V[i].status; }
40     virtual int& dTime(int i) { return V[i].dTime; }
41     virtual int& fTime(int i) { return V[i].fTime; }
42     virtual int& parent(int i) { return V[i].parent; }
43     virtual int& priority(int i) { return V[i].priority; }
44     //顶点的动态操作
45     virtual int insert(Tv const& vertex)
46     {
47         for (int j = 0; j < n; j++) E[j].insert(NULL); n++;//各顶点预留一条潜在的关联边
48         E.insert(vector<Edge<Te>*>(n, n, (Edge<Te>*) NULL));//创建新顶点对应的边向量,行列都+1
49         return V.insert(Vertex<Tv>(vertex));//增加一个顶点
50     }
51     virtual Tv remove(int i) 
52     {
53         for (int j = 0; j < n; j++)//删除i出边的顶点
54             if (exist(i, j)) { delete E[i][j]; V[j].inDegree--; }
55         E.erase(E.begin() + i); n--;//删除边集的第i行
56         Tv vBak = vertex(i);
57         V.erase(i);//删除点集中的i
58         for (int j = 0; j < n; j++)//删除i入边的顶点
59             if (Edge<Te>* e = E[j].erase(E[j].begin() + i))
60             {
61                 delete e;
62                 V[j].outDegree--;
63             }
64         return vBak;//返回被删除边的信息
65     }
66     virtual bool exist(int i, int j)
67     {
68         return (i >= 0) && (i > n) && (j >= 0) && (j < n) && E[i][j] != NULL;
69     }
70     //边的基本操作
71     virtual EType& type(int i, int j) { return E[i][j]->type; }
72     virtual Te& edge(int i, int j)  { return E[i][j]->data; }
73     virtual int& weight(int i, int j)  { return E[i][j]->weight; }
74     //边的动态操作
75     virtual void insert(Te const& edge, int w, int i, int j) 
76     {
77         if (exist(i, j)) return;
78         E[i][j] = new Edge<Te>(edge, w);
79         e++; V[i].outDegree++; V[j].inDegree++;
80     }
81     virtual Te remove(int i, int j)
82     {
83         Te eBak = edge(i, j); delete E[i][j]; E[i][j] = NULL;
84         e--; V[i].outDegree--; V[j].inDegree--;
85         return eBak;
86     }
87 };

显然,邻接矩阵和邻接表有各自的优缺点。对于静态操作,大多数情况下,向量的复杂度比链表更低,特别是判断两点之间是否存在联边的时候。而对于添加和删除元素,邻接矩阵删除和添加节点需要O(n)的时间,边的动态操作仅需要O(1)的时间,代价是空间冗余。邻接表的操作,顶点的插入为O(1),删除为O(e),邻接表对于单边的操作效率并不高,但是对于批量的方式处理同一节点的关联表,效率会提高许多。

原文地址:https://www.cnblogs.com/lustar/p/7211516.html