图
图是一种很常见的结构,日常生活中的许多东西都可以视为一个图结构,比如各个公交站点以及他们之间的通路;互联网中联结于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),邻接表对于单边的操作效率并不高,但是对于批量的方式处理同一节点的关联表,效率会提高许多。