图的基本概念
图分为有向图和无向图
弧:类似于树中的边,不过是有方向的边
入度和出度:略
有向完全图:具有n(n - 1)条边的有向图
无向完全图:具有n(n - 1) / 2条边的无向图
路径:相邻顶点序偶所构成的序列
路径长度:路径上面边的个数
简单路径:序列中不重复出现的路径
回路:路径中第一个顶点和最后一个顶点相同
连通图:图中任意两个顶点都连通
极大连通子图:连通图自身就是极大连通子图;非连通图中能包含所有顶点的边构成的连通子图
生成树:包含必不可少的边
权:略
邻接矩阵(重点)
图的顺序存储结构
邻接矩阵中,数组下标表示顶点,0表示顶点不相连,1表示顶点相连
一般将行视为出,列视为入(有向图)
无向图中,邻接矩阵是对称的;对应顶点的度即其所在行 / 列元素之和;矩阵中1的个数是图边数的两倍
有向图中,矩阵中1的个数是图的边数;对应行的元素之和为出度,对应列的元素之和为入度
对于无权图而言,主对角线上的元素统一是0
对于有权图而言,主对角线上的元素统一是0,其他原本为0的设置为∞
// 邻接矩阵的定义
typedef struct{
int edges[maxsize][maxsize];
int n , e; // n代表顶点数,e代表边数
int vex[maxsize]; // 存放结点信息
}Graph;
邻接表(重点)
图的链式存储结构
每个顶点的邻接顶点信息都采用单链表的形式存储,所有的顶点构成一张邻接表
// 边表
typedef struct ARC{
int adjvex; // 此边指向的顶点位置
struct ARC *nextarc; // 指向下一条边的指针
}ARC;
// 顶点表
typedef struct{
int data; // 顶点信息
ARC *firstarc; // 指向第一条边的指针
}VNode;
// 邻接表
typedef struct{
VNode adj[maxsize];
int n , e; // n代表顶点数,e代表边数
}Graph;
深度优先搜索遍历DFS(重点)
类似于树的先序遍历
基本思想:
<1>先访问出发点v,并标记为已被访问过;
<2>选取v的任一邻接点w,访问并标记为已访问过;
<3>再选取w的任一邻接点,重复以上操作;
<4>当此顶点所有的邻接点都被访问时,依次退回到最近访问的顶点,重复这一过程。
// 深度优先遍历
bool visited[maxsize]; // 标记数组
void DFS(Graph G , int v){
visit(v); // 访问顶点v
visited[v] = true; // 标记顶点v
for(int w = firstNeighbor(G , v) ; w >= 0 ; w = nextNeighbor(G , v , w))
if(!visited[w])
DFS(G , w); // 递归访问下一未到达的邻接点
}
void DFSTravel(Graph G){
for(int v = 0 ; v < G.maxvex ; ++v){
visited[v] = false; // 初始化标记数组
}
for(int v = 0 ; v < G.maxvex ; ++v){
if(!visited[v])
DFS(G , v); // 对图中所有顶点都要DFS一遍
}
}
广度优先搜索遍历BFS(重点)
类似于树的层次遍历,需要借助一个队列
基本思想:
<1>取一个顶点访问,入队,标记该顶点已经被访问过;
<2>队列不空则循环执行:出队,将该出队顶点所有未被访问的顶点依次访问,标记,并入队;
<3>队列空则跳出循环,BFS遍历结束。
// 广度优先遍历
// 广度优先遍历类似于树的层次遍历,需要借助一个辅助队列Q
bool visited[maxsize]; // 标记数组
void BFS(Graph G , int v){
visit(v); // 访问顶点
visited[v] = true;
Enqueue(Q , v); // 顶点v入队
while(!isEmpty(Q)){
Dequeue(Q , v); // 顶点v出队
for(int w = firstNeighbor(G , v) ; w >= 0 ; w = nextNeighbor(G , v , w)){
if(!visiter[w]){ // 检查v的所有邻接点
visit(w); // 访问顶点w
visited[w] = true;
Enqueue(Q , w);
}
}
}
}
void BFSTravel(Graph G){
for(int i = 0 ; i < G.maxvex ; ++i)
visited[i] = false;
Init(Q); // 初始化辅助队列Q
for(int i = 0 ; i < G.maxvex ; ++i){
if(!visited[v]) // 遍历图G中所有的顶点v
BFS(G , i);
}
}
例题1:设计算法,求出带权无向连通图G中距离顶点v最远的一个顶点。
// 由BFS算法可知,BFS最后一个遍历的顶点一定是距离当前顶点v最远的一个顶点
int BFS(Graph G , int v){
int w;
bool visited[maxsize]; // maxsize是一个宏定义的常数,值为图G的顶点数
for (int i = 0 ; i < maxsize ; ++i) // 初始化标记数组
visited[i] = false;
Init(Q); // 初始化一个队列
visited[v] = true;
Enqueue(Q , v); // v被访问且入队
while(!isEmpty(Q)){
Dequeue(Q , v); // 顶点v出队
for(w = firstNeighbor(G , v) ; w >= 0 ; w = nextNeighbor(G , v , w)){
if(!visiter[w]){ // 检查v的所有邻接点
visited[w] = true;
Enqueue(Q , w);
}
}
}
return w; // 当队列为空时,w保存了最后一个被访问的顶点
}
例题2:设计算法,判断无向图G是否是一棵树,是则返回1,否则返回0。
// 如果一个无向图是树,则其一定是有n - 1条边的连通图
// n - 1条边通过图的信息确定,是否连通看一次DFS能否遍历所有顶点作为依据
void DFS(Graph G , int v , int &vex , int &edge){
int w;
int visited[maxsize]; // maxsize是一个宏定义的常数,值为图G的顶点数
for(int i = 0 ; i < maxsize ; ++i) // 初始化标记数组
visited[i] = false;
visited[v] = true;
++vex; // 顶点数自增1
for(w = firstNeighbor(G , v) ; w >= 0 ; w = nextNeighbor(G , v , w)){
++edge; // 边数自增1
if(!visited[w])
DFS(G , w); // 递归访问下一未到达的邻接点
}
}
int isTree(Graph G){
int vex = 0;
int edge = 0;
DFS(G , 1 , vex , edge); // 进行一次DFS即可
//此时若顶点数为n且边数为n - 1,则返回1,否则返回0
if(vex == maxsize && (maxsize - 1) == (edge / 2))
return 1;
else
return 0;
}
最小生成树(重点)
下面给出两种构建最小生成树的算法:Prim算法和Kruskal算法。
(1)Prim算法
算法思想:
<1>任取一个顶点,视作一棵树,从这棵树相邻的边中选一条最短的边,将边以及相连的顶点并入这棵树中;
<2>再从这棵树相邻的边中选一条最短的边,和上述一样的操作;
<3>重复以上步骤,直到所有的顶点都被并入该树中结束,此时这棵树即最小生成树。
Prim算法时间复杂度:邻接矩阵存储结构下为 O(n的平方)
(2)Kruskal算法
算法思想:
<1>将图中所有的边按权值大小排序;
<2>从最小的边开始并入树中,如果构成回路则不并入;
<3>重复以上步骤,直到所有的边都已经被检测完为止。
Kruskal算法时间复杂度:取决于选取的排序算法的时间性能,因此适用于稀疏图
最短路径(重点)
下面给出两种寻找最短路径的算法:Dijkstra算法和Floyd算法。
(1)Dijkstra算法
基于贪心算法,求出特定顶点到其余各顶点的最短路径。注意,Dijkstra算法并不适用于图边上带有负权值时的情况
算法思想:
<1>先将出发点的相邻边进行比较,最短的边被划入,并将这个点算进最短路径;
<2>从这个最新划入的点开始,比较其相邻边,这里比较 A -> C 和 A -> B-> C的距离,最短的则划入最短路径中;
<3>重复上述步骤。
Dijkstra算法计算完成后,会形成了三个表,分别记录:
<1>是否已访问;
<2>出发点到对应顶点的距离;
<3>最短路径。
(2)Floyd算法
求出任意一对顶点间的最短路径
算法思想:三重循环,k为中转点,v为起点,w为终点。如果D[v] [k] + D[k] [w]为更小的长度,则将D[v] [k] + D[k] [w]覆盖掉D[v] [w]中。
算法通过不断的更新D1,D2,…等表,最终得出一个可以判断两两顶点最短路径的表。
建表规律:沿对角线元素依次建表,每个表无穷元素和对角元素填自己,剩下的则比较相交走小一点还是原本的值小一点,保留最小的值即可
拓扑排序
AOV网:反映一个工程各个活动之间的先后次序关系的有向图
AOV网没有回路
对于有向无环图G,拓扑排序后,任意一对顶点u和v,只要存在u到v的路径,则拓扑排序中u一定在v的前面
算法思想:
<1>从有向图中选一个没有前驱的顶点输出;
<2>删除此顶点,并删除从该顶点出发的所有边;
<3>重复以上步骤,直到图中不存在没有前驱的顶点为止。
注意:拓扑排序可能不唯一
逆拓扑排序
拓扑排序考虑的是顶点的入度问题
逆拓扑排序考虑的是顶点的出度问题
算法思想:
<1>在网中选择一个没有后继的顶点输出;
<2>删除此顶点,并删除所有到达该顶点的边;
<3>重复上述步骤,直到网中不存在出度为0的顶点为止。
关键路径
AOE网(Activity On Edge):边表示活动,顶点表示结束
关键路径:边上活动相加最大的一条路径。