图的基本概念
在树型结构中,结点间具有分支层次关系,每一层上的结点只能和上一层中的至多一个结点相关,但可能和下一层的多个节点相关。树的关系也叫一对多的关系,而在图状结构中,任意两个结点之间都可能相关,即结点的邻接关系可以是任意的。图的结构是任意两个数据对象之间都可能存在某种特定关系的数据结构,是一种多对多的关系。
图的定义和术语
图(Graph)是由两个集合构成,一个是非空但有限的顶点集合 V,另一个是描述顶点之间关系——边的集合 E(可以是空集)。因此图可以表示为 G=(V,E)。每条边是一顶点对 (v,w) 且 v,w∈V。通常用 |V| 表示定点数量 |E| 表示边的数量。
关于图的定义,与以前的线性表和树比较,还有几点需要注意:
图的抽象数据类型
类型名称:图(Graph)。
数据对象集:一个非空顶点集合 Vertex 和一个边集合 Edge,每条边用对应的一对顶点表示。
操作集:对于任意的图 G∈Graph,顶点 V∈Vertex,边 E∈Edge,以及任一访问顶点的函数 Visit(),我们主要关心下列操作:
1.Graph CreateGraph(int VertexNum)
:构造一个有 VertexNum 个顶点但没有边的图;
2.void InsertEdge(Graph G, Edge E)
:在 G 中增加新边 E;
3.void DeleteEdge(Graph G, Edge E)
:从 G 中删除边 E;
4.bool IsEmpty(Graph G)
:判断图是否为空;
5.void DFS(Graph G, Vertex V, (*Visit)(Vertex))
:在图 G 中,从顶点 V 出发进行深度优先遍历;
6.void BFS(Graph G, Vertex V, (*Visit)(Vertex))
:在图 G 中,从顶点 V 出发进行广度优先遍历。
图的存储结构
邻接矩阵
所谓邻接矩阵的存储结构,就是用矩阵表示图中各顶点之间的邻接关系和权值。以下是一个无向图的临界矩阵表示:
从图的邻接矩阵存储方法容易看出这种表示具有以下特点:
1.无向图的邻接矩阵一定是个对称矩阵。因此在具体存放邻接矩阵时只需要存放上三角或者下三角的元素即可。所需要存储的元素个数是:|V|*(|V|-1)/2。
2.对于无向图,邻接矩阵的第 i 行(或第 i 列)非 0 元素的个数正好是第 i 个顶点的度(Degree)。
3.对于有向图,邻接矩阵第 i 行(或第 i 列)非 0 元素的个数正好是第 i 个顶点的出度(或入度)。
4.用临界矩阵方法存储图,很容易确定图中任意两点之间是否有边相连,只需要考察邻接矩阵对应的元素即可;确定一个顶点的所有邻接点,也只需要邻接矩阵对应的一行(或一列);但是要确定图中有多少边,则必须按行(或按列)对每个元素进行检测,所花费的时间代价是 O(|V|^2)。这是用邻接矩阵来存储图的局限性。
以下是邻接矩阵的 C 语言描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #define MaxVertexNum 100 #define INFINITY 65535 typedef int Vertex; typedef int WeightType; typedef char DataType;
struct GNode { int Nv; int Ne; WeightType G[MaxVertexNum][MaxVertexNum]; DataType Data[MaxVertexNum]; }; typedef struct GNode* PtrToGNode; typedef PtrToGNode MGraph;
struct ENode { Vertex V1, V2; WeightType Weight; }; typedef struct ENode* PtrToENode; typedef PtrToENode Edge;
|
有了图的结构和类型定义之后,先创建一个包含全部顶点但是没有边的图,再逐条插入边,从而创建一个无向网图的数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| MGraph CreateGraph(int VertexNum) { MGraph Graph = (MGraph)malloc(sizeof(struct GNode)); Graph->Nv = VertexNum; Graph->Ne = 0;
for(int i = 0;i < Graph->Nv;i++) { for(int j = 0;j < Graph->Nv;j++) { Graph->G[i][j] = INFINITY; } }
return Graph;
} void InsertEdeg(MGraph Graph, Edge E) {
Graph->G[E->V1][E->V2] = E->Weight;
Graph->G[E->V2][E->V1] = E->Weight;
} MGraph BuildGraph() { MGraph Graph; int VertexNum;
scanf("%d", &VertexNum);
Graph = CreateGraph(VertexNum);
scanf("%d", &Graph->Ne);
if(Graph->Ne != 0) {
Edge E = (Edge)malloc(sizeof(struct ENode));
for(int i = 0; i < Graph->Ne;i++) {
scanf("%d %d %d",&E->V1, &E->V2, &E->Weight);
InsertEdeg(Graph, E);
} } for(int i = 0;i < Graph->Nv;i++) { scanf("%c",&Graph->Data[i]); }
return Graph;
}
|
邻接矩阵是一种表示各类图的简洁的数据结构。但是我们发现,不论图中边的数量或多或少,我们都花费了 O(|V|^2) 的存储空间,这对于稠密图来说是一种高效的存储方法。但是如果面对的是一个稀疏图,则邻接矩阵中的大多数项为 0 或 无穷,形成了所谓的稀疏矩阵,就会浪费很多空间。因此对于稀疏图,我们考虑另一种存储方法。
邻接表
邻接表是一种图的顺序存储与链式存储相结合的存储方法。
图的邻接表存储具有以下特点:
1.方便查找任一顶点的所有邻接点。
2.节约稀疏图的空间。需要 N 个头指针 + 2E 个结点(每个结点至少两个域)。
3.对于无向图来说方便计算任一顶点的度,对于有向图来说只能计算出度。
4.不方便检查任一对顶点间是否存在边。
以下是图的邻接表存储的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #define MaxVertexNum 100 typedef int Vertex; typedef int WeightType; typedef char DataType;
struct ENode { Vertex V1,V2; WeightType Weight; }; typedef struct ENode* Edge;
typedef struct AdjVNode* PtrToAdjVNode; struct AdjVNode { Vertex AdjV; WeightType Weight; PtrToAdjVNode Next; };
typedef struct Vnode { PtrToAdjVNode FirstEdge; DataType Data; } AdjVList[MaxVertexNum];
typedef struct GNode* PtrToGNode; struct GNode { int Nv; int Ne; AdjVList G; }; typedef PtrToGNode LGraph;
LGraph CreateGraph(int VertexNum);
void InsertEdge(LGraph Graph, Edge E);
LGraph BuildGraph();
LGraph CreateGraph(int VertexNum) { LGraph Graph = (LGraph)malloc(sizeof(struct GNode)); Graph->Nv = VertexNum; Graph->Ne = 0;
for(int i = 0;i < Graph->Nv;i++) { Graph->G[i].FirstEdge = NULL; }
return Graph;
} void InsertEdge(LGraph Graph, Edge E) { PtrToAdjVNode NewNode;
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode)); NewNode->AdjV = E->V2; NewNode->Weight = E->Weight;
NewNode->Next = Graph->G[E->V1].FirstEdge; Graph->G[E->V1].FirstEdge = NewNode;
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode)); NewNode->AdjV = E->V1; NewNode->Weight = E->Weight;
NewNode->Next = Graph->G[E->V2].FirstEdge; Graph->G[E->V2].FirstEdge = NewNode; } LGraph BuildGraph() { LGraph Graph; int VertexNum;
scanf("%d", &VertexNum); Graph = CreateGraph(VertexNum);
scanf("%d", &Graph->Ne);
if(Graph->Ne != 0) { Edge E = (Edge)malloc(sizeof(struct ENode)); for(int i = 0;i < Graph->Ne;i++) { scanf("%d %d %d", &E->V1, &E->V2, &E->Weight); InsertEdge(Graph, E); } }
for(int i = 0;i < Graph->Nv;i++) scanf("%c", &(Graph->G[i].Data));
return Graph; }
|
图的遍历
图的遍历就是从图中任一顶点出发,对图中所有顶点访问一次且仅访问一次的次序序列。
深度优先搜索(Depth First Search,DFS)
深度优先搜索类似于树的先序遍历,是树的先序遍历的推广。假设初始状态所有顶点都没被访问过,则深度优先搜索从图中的任一顶点出发,设为v0 ,访问此顶点,然后从v0的邻接点中的一个出发递归地进行同样的深度优先搜索,直至图中所有节点都被访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Visit(Vertex V) { printf("正在访问顶点 %d", V); }
void DFS(LGraph Graph, Vertex V, void (*Visit)(Vertex)) {
Visit(V); Visited[V] = true;
PtrToAdjVNode W; for(W = Graph->G[V].FirstEdge;W;W = W->Next) { if(! Visited[W->AdjV]) { DFS(Graph, W->AdjV, Visit); } } }
|
广度优先搜索(Breadth First Search,BFS)
广度优先搜索类似于树的层次遍历。从顶点v0出发,在访问了v0之后,依次访问v0各个未被访问的邻接点,然后分别从这些邻接点出发,访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。直至图中所有已被访问的顶点的邻接点都被访问到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void Visit(Vertex V) { printf("正在访问顶点 %d", V); }
bool IsEdge(MGraph Graph, Vertex V, Vertex W) { return Graph->G[V][W] < INFINITY ? true : false; }
void BFS(MGraph Graph, Vertex S, void(* Visit)(Vertex)) { Vertex V,W;
Visit(S); Visited[S] = true;
Queue Q; Q = CreateQueue(MaxSize);
AddQ(Q, S);
while(!IsEmpty(Q)) { V = DeleteQ(Q); for(W = 0;W < Graph->Nv;W++) { if(!Visited[W] && IsEdge(Graph, V, W)) Visit(W); Visited[W] = true; AddQ(Q, W); } }
}
|
若有 N 个顶点、E 条边,DFS 和 BFS 的时间复杂度为:
用邻接表存储图,为 O(N+E);
用邻接矩阵存储图,为 O(N^2)。