图(Graph)
(参考资料:《大话数据结构》《算法导论》)图是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G(V,E),其中G表示一个图,V是图G中顶点(Vertex)的集合,E是图中边的集合。
图的相关术语
1.无向图与有向图无向图:图中任意两个顶点u和v之间的边没有方向(无向边),每一条无向边用无序偶对(u,v)或(v,u)表示。
无向完全图:任意两个顶点之间都存在边,边个数:n*(n-1)/2(n个顶点)。
有向图:图中任意两个顶点u和v之间的边具有方向(有向边,弧),每一条从顶点u到顶点v的有向边用有序偶对 < u,v > 表示,u称为弧尾,v称为弧头。
有向完全图:任意两个顶点之间都存在方向相反的两条弧,边个数:n*(n-1)(n个顶点)。
- 无向边用”(u,v)”表示,有向边用“ < u,v > “表示。
2.稀疏图和稠密图
稀疏图:边的条数|E|远小于|v|^2的图。
反之,称为稠密图。
3.权重图(网)
图中的每一条边都带有一个相关的权重的图。
权重图可以由邻接表和邻接矩阵表示(用权重值代替1),若含0权重,则可以用无穷数表示无边情况。
4.顶点的度
无向图:顶点u的度是和u相关联的边的数目。
有向图:顶点u的入度是以顶点u为弧头的边的数目;顶点u的出度是以顶点u为弧尾的边的数目。
5.路径
从顶点u到顶点v的路径是一个顶点序列。
路径的长度:路径上的边或弧的数目。
简单路径:序列中顶点不重复出现的路径。
回路或环:第一个顶点到最后一个顶点相同的路径。
简单回路或简单环:除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
图的表示
1.邻接表邻接链表表示有一个包含|V|条链表的数组Adj所构成,每个顶点有一条链表。对于每个顶点u,邻接链表Adj[u]包含所有与顶点u之间有边相连的顶点v。
存储空间:O(V+E)。
无向图:所有邻接链表的长度之和为2|E|;有向图:所有邻接链表的长度之和为|E|。
优点
1.表示稀疏图,表示紧凑,节约空间,表示简单。
2.鲁棒性高,可以对其进行简单修改来支持许多其他的图变种。
缺点
1.无法快速判断两个顶点u和v之间是否是图中的一条边,需要在邻接链表Adj[u]里搜索结点v。
#define MAXVEX 10 // maximum vertex numbers in the graph typedef int VertexType; // vertex data type typedef int EdgeType; // edge weight type // edge node to record the adjacent vertex node for a vertex node typedef struct EdgeNode { int adjvex; // the adjacent vertex node position in array EdgeType weight; // edge weight struct EdgeNode* next; // next edge node }EdgeNode; // vertex node to record the data for a vertex node typedef struct VertexNode { VertexType data; // vertex data EdgeNode *firstedge; // first edge node pointer }VertexNode; // graph typedef struct { VertexNode adj[MAXVEX]; int numVertexes; int numEdges; }Graph; Graph* CreateGraph() { Graph* G = new Graph; cout << "input vertex num and edge num:" << endl; cin >> G->numVertexes >> G->numEdges; cout << "input vertex data one by one:" << endl; for (int index = 0; index < G->numVertexes; index++) { cin >> G->adj[index].data; G->adj[index].firstedge = NULL; } cout << "input edge each by two vertex index(begin from 0):" << endl; for (int index = 0; index < G->numEdges; index++) { int i, j; cin >> i >> j; EdgeNode* e = new EdgeNode; e->adjvex = j; e->next = G->adj[i].firstedge; G->adj[i].firstedge = e; #define NODRECTIONGRAPH #ifdef NODRECTIONGRAPH e= new EdgeNode; e->adjvex = i; e->next = G->adj[j].firstedge; G->adj[j].firstedge = e; #endif } return G; } void BFS(Graph* G) { memset(visit, 0, sizeof(visit[0]) * MAXVEX); queue<EdgeNode*> q; EdgeNode* temp; for (int index = 0; index < G->numVertexes; index++) { if (!visit[index]) { visit[index] = true; q.push(G->adj[index].firstedge); cout << " from vertex data: " << G->adj[index].data << endl; while (!q.empty()) { temp = q.front(); if (temp && !visit[temp->adjvex]) { cout << "get weight: " << temp->weight << " to " << temp->adjvex << endl; visit[temp->adjvex] = true; q.push(temp->next); } q.pop(); } } } } void DFS_VISIT(Graph* G, int index) { visit[index] = true; cout << "vertex data: " << G->adj[index].data << endl; EdgeNode* temp = G->adj[index].firstedge; while (temp) { if (!visit[temp->adjvex]) DFS_VISIT(G, temp->adjvex); temp = temp->next; } } void DFS(Graph* G) { memset(visit, 0, sizeof(visit[0]) * MAXVEX); for (int index_i = 0; index_i < G->numVertexes; index_i++) { if (!visit[index_i]) DFS_VISIT(G, index_i); } }
2.邻接矩阵
由一个|V|x|V|的矩阵A = (aij)表示。aij = 1,若含有(i,j)边;aij = 0,其他。
存储空间:O(V^2)。
无向图:邻接矩阵为对称矩阵,可以减少图存储空间一半需求。
优点
1.表示稠密图。
2.可以快速判断两个顶点u和v之间是否是图中的一条边。
缺点
1.消耗非常大的存储空间。
#define MAXVEX 10 // maximum vertex numbers in the graph typedef int VertexType; // vertex data type typedef int EdgeType; // edge weight type #define INFINITY 65535 // set as infinity value // graph typedef struct { VertexType vertex[MAXVEX]; // vertex list EdgeType arc[MAXVEX][MAXVEX]; // adjacent matrix int numVertexes; int numEdges; }Graph; Graph* CreateGraph() { Graph* G = new Graph; cout << "input vertex num and edge num:" << endl; cin >> G->numVertexes >> G->numEdges; cout << "input vertex data one by one:" << endl; for (int index = 0; index < G->numVertexes; index++) { cin >> G->vertex[index]; } // initial adjacent matrix for (int index_i = 0; index_i < G->numVertexes; index_i++) for (int index_j = 0; index_j < G->numVertexes; index_j++) G->arc[index_i][index_j] = INFINITY; cout << "input edge each by two vertex index and weight:" << endl; for (int index = 0; index < G->numEdges; index++) { int i, j, w; cin >> i >> j >> w; G->arc[i][j] = w; #define NODRECTIONGRAPH #ifdef NODRECTIONGRAPH G->arc[j][i] = w; #endif } return G; } void BFS(Graph* G) { memset(visit, 0, sizeof(visit[0]) * MAXVEX); queue<int> q; for (int index = 0; index < G->numVertexes; index++) { if (!visit[index]) { visit[index] = true; q.push(index); while (!q.empty()) { int index_i = q.front(); cout << " from vertex " << index_i << " data: " << G->vertex[index_i] << endl; for (int index_j = 0; index_j < G->numVertexes; index_j++) { if (G->arc[index_i][index_j] != INFINITY && !visit[index_j]) { visit[index_j] = true; cout << "get weight: " << G->arc[index_i][index_j] << " to " << index_j << endl; q.push(index_j); } } q.pop(); } } } } void DFS_VISIT(Graph* G, int index) { visit[index] = true; cout << "vertex data: " << G->vertex[index] << endl; for (int index_j = 0; index_j < G->numVertexes; index_j++) { if (G->arc[index][index_j] != INFINITY && !visit[index_j]) DFS_VISIT(G, index_j); } } void DFS(Graph* G) { memset(visit, 0, sizeof(visit[0]) * MAXVEX); for (int index_i = 0; index_i < G->numVertexes; index_i++) { if (!visit[index_i]) DFS_VISIT(G, index_i); } }
图的搜索
1.广度优先搜索 BFS Breadth-First-Search
- 算法始终将已发现顶点和未发现顶点之间的边界,沿其广度方向向外扩展。也就是说,算法需要在发现所有距离源顶点s为k的所有顶点后,才会发现距离源顶点s为k+1距离的其他顶点。
- 对每个顶点,设定标志位记录是否被”发现“过。
- 采用先进先出队列实现(可以联想二叉树的层次遍历,是图广度优先搜索的子集)。
- 顶点u的属性u.d为最短距离,记录从源顶点s到顶点u之间的距离。
- 广度优先树:BFS过程中生成的前驱子图(通过记录每个顶点的前驱顶点)。
时间复杂度:O(V + E)。
广度优先搜索伪代码
BFS(G,s) for each vertex u in G.V - {s} u.color = WHITE u.d = INFINITY u.parent = NIL s.color = GRAY s.d = 0 s.parent = NIL Q = EMPTY ENQUEUE(Q,s) while Q != EMPTY u = DEQUEUE(Q,s) for each vertex v in G.adj[u] if v.color == WHITE v.color = GRAY v.d = v.d + 1 v.parent = u ENQUEUE(Q,s) u.color = BLACK
2.深度优先搜索 DFS Depth-First-Search
- 算法总是对最近才发现的顶点的出发边进行探索,直到该顶点的所有出发边都被发现为止。算法重复整个过程,直到所有的顶点都被发现为止。
- 对每个顶点,设定标志位记录是否被”发现“过。
- 采用函数递归调用实现(可以联想二叉树的先序、中序、后序遍历,是图深度优先搜索的子集)。
- 顶点u的属性u.d为时间戳,发现时间,记录从顶点u第一次被发现的时间。
- 顶点u的属性u.f为时间戳,完成时间,记录完成对顶点u的邻接链表扫描的时间。
- 深度优先森林:深度优先搜索的前驱子图形成一个由多棵深度优先树构成的深度优先森林(搜索可能从多个源结点重复进行)。
- DFS对边进行分类
- 树边(u,v):深度优先森林的边,顶点v因算法对边(u,v)的探索而首先被发现。(u.d < v.d < v.f < u.f)
- 后向边(u,v):是将结点u连接到深度优先树中一个祖先结点v的边(有向图中的自循环)。(v.d < =u.d < u.f <= v.f)
- 前向边(u,v):是将结点u连接到深度优先树中一个后代结点v的边。 (u.d < v.d < v.f < u.f)
- 横向边(u,v):上述三种边以外的边。 (v.d < v.f < u.d < v.f)
- 无向图:每条边要么是树边,要么是后向边。
时间复杂度:O(V + E)。
广度优先搜索伪代码
DFS(G) for each vertex u in G.V u.color = WHITE u.parent = NIL time = 0 for each vertex u in G.V if (u.color == WHITE) DFS-VISIT(G,u) DFS-VISIT(G,u) time = time + 1 u.d = time u.color = GRAY for each vertex v in G.adj[u] if v.color == WHITE v.parent = u DFS-VISIT(G,v) u.color = BLACK time = time + 1 u.f = time
应用:有向无环图的拓扑排序
- 利用深度优先搜索算法。
- 有向无环图的深度优先搜索不产生后向边。
应用:将有向图分解为强连通分量
- 利用深度优先搜索算法。