1、图的基本概念
- 按照有无方向分为无向图、有向图。无向图中如果任意两个顶点之间都存在边则称为无向完全图,含有n个顶点的无向完全图含有 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2条边;有向中如果任意两个顶点之间都存在两条方向相反的边则称为有向完全图,含有n个顶点的有向完全图含有 n ( n − 1 ) n(n-1) n(n−1)条边;
- 简单图:图中没有重复边以及顶点到自身的边(环)
- 图的边(无向边或者有向边)带上权值就称为网
- 无向图中如果任意两个顶点都是连通的则称为连通图;有向图中任意一对顶点 v i 、 v j v_i、v_j vi、vj,如果从 v i v_i viTo v j v_j vj和从 v j v_j vjTo v i v_i vi都是连通的,则称该有向图为强连通图。
- 无向图中连通且含有n个节点n-1条边的为生成树
2、图的存储结构
1、邻接矩阵存储
用两个数组来表示图。一个一维数组用来存储图中顶点信息,一个二维数组(邻接矩阵)用来存储图中的边或弧的信息。
网的每一条边都带有权值,如果用邻接矩阵来表示网呢?
下面看一下邻接矩阵存储结构的定义:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX]; /*顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /*邻接矩阵,用来表示边的情况*/
int numVertexes, numEdges; /*图中当前的顶点数和边数*/
}MGraph;
邻接矩阵存储方式的缺点:存储稀疏图(边数相对于顶点数较少的图)会浪费大量存储空间。
2、邻接表存储
邻接表的处理方式如下:
- 图中的顶点用一个一维数组存储,顶点数组中的每个数据元素除了需要存储顶点的值以外,还需要存储指向第一个邻接点的指针(用一维数组比用链表的好处是:数组可以比较容易容易的读取顶点信息)
- 图中每个顶点 v i v_i vi的所有邻接点构成一个线性表,用单链表存储。在无向图中,该链表被称为边表;在有向图中,该链表被称为出边表。
如下图所示:
如果是网,则用下面的存储方式:
下面看一下邻接表存储结构的定义:
typedef char VertexType;
typedef int EdgeType;
//边表结点定义
typedef struct EdgeNode {
int adjvex; //顶点对应的下标
EdgeType weight; //权值
struct EdgeNode *next; // 指向边表的下一个节点
}EdgeNode;
//顶点表结点
typedef struct VertexNode {
VertexType data; //存储顶点信息
EdgeNode *firstEdge; //边表头指针
}VertexNode, AdjList[MAXVEX];
//图定义
typedef struct {
AdjList adjList;
int numVertexes, numEdges;
}GraphAdjList;
邻接表存储方式的缺点: 若图为有向图的话,从邻接表中只能找到某一个顶点的出度,如果想要知道它的入度的话,则需要遍历整个图;如果使用逆邻接表的话,也只能找到某一个顶点的入度,想要知道它的出度的话,则需要遍历整个图。
3、十字链表
十字链表将邻接表和逆邻接表整合到了一起。存储方式如下:
重新定义顶点表结构如下:
- firstin: 入边表头指针,指向该顶点的入边表中第一个结点
- firstout: 出边表头指针,指向该顶点的出边表中的第一个结点
重新定义边表结点结构如下:
- tailvex: 弧尾顶点在顶点表中的下标
- headvex: 弧头顶点在顶点表中的下标
- headlink: 入边表指针域,指向终点相同的下一条边
- taillink: 出边表指针域,指向起点相同的下一条边
- 如果是网的话,还需要增加一个weight域用来存储权值
十字链表存储的示例:
十字链表的好处就是它把邻接表和逆邻接表整合到了一起,这样既容易找到以
v
i
v_i
vi为尾的弧,也容易找到以
v
i
v_i
vi为头的弧,可以轻易求得顶点的出度和入度。
4、邻接多重表
邻接多重表有益于对无向图中的边的操作,重新定义的边表结点结构如下:
- ivex、jvex是与某条边依附的两个顶点在顶点表中的下标
- ilink指向依附顶点ivex的下一条边
- jlink指向依附顶点jvex的下一条边
如下图所示:
5、边集数组
边集数组由两个一维数组构成。一个存储顶点的信息;另一个是存储边的信息,边数组的每个数据元素由这条边的起点下标(begin)、终点下标(end)和权值(weight)组成。
3、图的遍历(深度优先遍历、广度优先遍历)
深度优先遍历:
邻接矩阵的深度优先遍历伪代码:
邻接表的深度优先遍历伪代码:
复杂度分析:
- 邻接矩阵表示法需要将邻接矩阵的每个元素都遍历一次,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 邻接表 表示法需要将顶点表和边表的每个元素都遍历一次,时间复杂度为 O ( n + e ) O(n+e) O(n+e)
广度优先遍历:
邻接矩阵存储方式的广度优先遍历伪代码:
邻接表存储方式的广度优先遍历伪代码:
广度优先遍历的时间复杂度与深度优先遍历的时间复杂度是相同的。总结一句:深度优先遍历就是纵向发展,广度优先遍历是横向发展。
4、最小生成树
最小生成树是相对于网而言的:其含有网的全部n个节点n-1条边,并且n-1条边的权值之和最小。
生成最小生成树的算法:
- 普里姆算法
- 克鲁斯卡尔算法
4-1 普里姆算法
基本思想:从任意顶点v开始,先把该顶点加入一个集合V中,然后不断的选取与集合V的所有邻接边中权值最小的边和顶点加入该集合,直到集合V中的顶点数量等于网的顶点数。
4-2 克鲁斯卡尔算法
基本思想:先把所有的边按照权值排序,然后从权值最小的边开始遍历所有的边:如果该边加入集合中不构成环,则将该边以及其邻接的顶点一起加入;否在舍弃这一条边。所有的边遍历完后,集合中的顶点和边就是原图的最大生成树。
该算法为了能够方便的对边按照权值进行排序,对图采用了边集数组的存储方式,边集数组的数据元素结构定义如下:
typedef struct {
int begin;
int end;
int weight;
} Edge;
如何判断一个新的顶点和边加入不会形成回路:并查集
克鲁斯卡尔的伪代码实现:
void MiniSpanTree_Krukal(MGraph G)
{
int i, n, m;
Edge edges[MAXEDGE];
int parent[MAXVEX];
··· //边集数组排序
//初始化并查集
for(int i = 0; i < G.numVertexes; i++)
parent[i] = i;
//循环遍历每一条边
for(int i = 0; i < G.numEdges; i++)
{
n = Find(parent, edges[i].begin); //找到begin所在集合的代表元素
m = Find(parent, edges[i].end); //找到end所在集合的代表元素
if(n != m)
{
// 并查集合并
parent[n] = m;
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
// 并查集查找
int Find(int *parent, int f)
{
while(f != parent[f])
f = parent[f];
return f;
}
复杂度分析: Find()函数的时间复杂度由e决定,时间复杂度为loge,加上外层的For循环,总的时间复杂度为eloge。
两种寻找最小生成树的方法的差别:克鲁斯卡尔算法主要是针对边来展开的,对于稀疏图来说有很大的优势;普里姆算法是从点出发考虑的,更适合稠密图。
5、最短路径
5-1 迪杰斯特拉算法
迪杰斯特拉算法能够方便的求得网中的某一个源点 v 0 v_0 v0到其余各个顶点的最短路径,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
迪杰斯特拉算法的最终输出可以认为是两个数组:
- d i s t a n c e [ G . n u m V e r t e x ] distance[G.numVertex] distance[G.numVertex]: d i s t a n c e [ v i ] distance[v_i] distance[vi]表示从顶点 v 0 v_0 v0到 v i v_i vi的最短路径长度
- p a t h [ G . n u m V e r t e x ] path[G.numVertex] path[G.numVertex]: p a t h [ v i ] path[v_i] path[vi]表示顶点 v i v_i vi的最短路径中 v i v_i vi的前驱顶点
迪杰斯特拉算法的限制:图中不可以有负的权值存在
迪杰斯特拉算法的关键在于如何更新上面所说的这两个数组:
5-2 洛伊德算法
弗洛伊德算法可以方便求得图中的任一顶点到其它所有顶点的最短路径。时间复杂度为
O
(
n
3
)
O(n^3)
O(n3)。
6、拓补排序
6-1 什么是AOV网?
- 一个有向无环图
- 顶点表示活动
- 弧表示活动之间的优先关系
6-2 什么是拓补序列?
AOV网的一个顶点序列,该顶点序列满足:包含网中所有顶点,并且若网中存在从 v i v_i vi到 v j v_j vj的弧,则在顶点序列中 v i v_i vi一定排在 v j v_j vj的前面。
6-3 拓补排序算法
- 从图中选择一个入度为0的顶点并输出
- 删除该顶点以及从该顶点出发的所有边
- 重复上面两步,直到所有顶点都被删除或者没有入度为0的顶点(如果是这种情况就说明图中有环了)
6、关键路径
略