数据结构-图 2021/8/12 22:51
图的储存结构
1.邻接矩阵法
#include <iostream>
using namespace std;
typedef char VertexType;
typedef int EdgeType;
#define MaxVertexNum 100//顶点数目的最大值
#define INFINITY 最大的int值 //宏定义常量无穷
typedef struct {
VertexType Vex[MaxVertexNum];
EdgeType Edge[MaxVertexNum][MaxVertexNum];
int vexnum, arcnum;//图的顶点数和弧数
}MGraph;
//邻接矩阵适合储存稠密图
2.邻接表法
#include <iostream>
using namespace std;
typedef char VertexType;
#define MaxVertexNum 100//顶点数目的最大值
#define INFINITY 最大的int值 //宏定义常量无穷
//邻接表类似于树的孩子表示法
typedef struct {//用邻接表储存的图
AdjList vertices;
int vexnum, arcnum;
}ALGraph;
typedef struct VNode{//顶点
VertexType data;
ArcNode* first;
}VNode,AdjList[MaxVertexNum];
typedef struct ArcNode {//弧
int adjvex;//该弧指向哪个顶点
struct ArcNode* next;//指向下一条弧的指针
//Infotype info //边权值
}ArcNode;
//用领接表表示的图删除结点不容易,因为有两份冗余的数据
//为了解决这个问题,有了十字链表(有向图)和临接多重表(无向图)
/*图的基本操作:
Adjacent(G,x,y):判断图G是否存在边<x,y>或(x,y)
邻接矩阵:判断矩阵对应位置是否为1; 领接表:从顶点x依次往后找
Neighbors(G,x):列出图G中与结点x邻接的边
邻接矩阵:判断矩阵对应行/列是否为1; 领接表:从顶点x依次往后找全部
InsertVertex(G,x):在图G中插入顶点x
邻接矩阵:在矩阵外围增加行和列 邻接表:在顶点表中插入新的顶点
DeleteVertex(G,x):从图G中删除顶点x
邻接矩阵:在矩阵中把顶点对应的行和列删除,在顶点表中将该顶点标志值0(标记该点已删除) 邻接表:在顶点表中把该顶点删除,同时遍历所有结点的弧,将该结点指向的和指向该结点的弧删除
AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边
邻接矩阵:在矩阵中将对应位置标为1即可 邻接表:在结点后边的弧链表中插入该弧
RemoveEdge(G,x,y):若无向边(x,y)或有向边<x,y>存在,则从图G中删除该边
和上面AddEdge相似的逆操作
FirstNeighbor(G,x):求图中顶点x的第一个邻接点
邻接矩阵:将矩阵中结点对应行中第一个值为1的顶点号输出 邻接表:查看该结点弧链表第一条弧即可
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号
Get_edge_value(G,x,y):获取图G中边(x,y)或<x,y>对应的权值
Set_edge_value(G,x,y,v):设置图G中边(x,y)或<x,y>对应的权值为v。这条和上一条的关键都在于找顶点
*/
图的广度优先遍历
#define MAX_VERTEX_NUM 100
bool visited[MAX_VERTEX_NUM];//访问标记数组
//伪代码
void BFSTraverse(Graph G) {//对图G进行广度优先遍历
for (i = 0; i < G.vexnum; ++i)
visited[i] = false;//访问标记数组初始化
InitQuene(Q);//初始化辅助队列Q
for (i = 0; i < G.vexnum; ++i)
if (!visited[i])//对每个连通分量用一次BFS
BFS(G, i);
}
//广度优先遍历
void BFS(Graph G, int v) { //从顶点v出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v] = true; //对v做已访问标记
Enquene(Q, v); //顶点v入队列Q
while (!isEmpty(Q)) {
DeQuene(Q, v); //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0:w=NextNeighboe(G,v,w))
//检测v所有的邻接点
if (!visited[w]) {//w为v的尚未访问的邻接顶点
visit(w);//访问顶点w
visited[w] = true;//对w做已访问标记
EnQuene(Q, w);//顶点w入队列
}
}
}
//要会构造广度优先序列、广度优先生成树
图的深度优先遍历
#define MAX_VERTEX_NUM 100
bool visited[MAX_VERTEX_NUM];//访问标记数组
//伪代码
void BFSTraverse(Graph G) {//对图G进行深度优先遍历
for (i = 0; i < G.vexnum; ++i)
visited[i] = false;//访问标记数组初始化
for (i = 0; i < G.vexnum; ++i)
if (!visited[i])//对每个连通分量用一次BFS
DFS(G, i);
}
//广度优先遍历
void BFS(Graph G, int v) { //从顶点v出发,深度优先遍历图G
visit(v); //访问初始顶点v
visited[v] = true; //对v做已访问标记
for (w = FirstNeighbor(G, v); w >= 0:w = NextNeighboe(G, v, w))
if (!visited[w]) {//w为v的尚未访问的邻接顶点
DFS(Q, w);
}
}
//要会构造深度优先序列、深度优先生成树
最小生成树
最小生成树:各个结点都可以连通但是权值最小
1.Prim算法:(普里姆算法)适用于边稠密
从某结点开始构建生成树,每次将代价最小的新结点纳入生成树,直到所有结点都纳入为止。
2.Kruskal算法(克鲁斯卡尔):适用于边稀疏
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通.
最短路径
1.广度优先算法求无权图最短路径:(谁先碰到这个点则该路径就是最短路径)
2.Dijkstra迪杰斯特拉算法:
定义三个数组:
标记各顶点是否已找到最短路径final[],初始化为false
当前最短路径的长度dist[],初始化为无穷大
当前最短路径上的前驱path[],初始化为-1
流程:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i]true,
检查所有邻接自Vi的顶点,若其final值为false,经过Vi的路径比当前最短路径要短,则更新dist和path值
时间复杂度O(n^2)
Dijkstra算法不适合有负权值的带权图
3.Floyd算法:
定义两个二维数组A和Path
A用来存储两个点互相方向的最短路径
Path用来储存两点之间最短路径的倒数第二个点
流程:
1.若不允许在其他顶点中转,求出最短路径和path
2.若允许在V0中转,求出最短路径和path,能够得到更小的路径则更新两矩阵
3.若允许在V1中转……若允许在Vn-1中转……
//核心代码(伪):
for (int k = 0; k < n; k++) { 考虑以Vk为中转点
for (int i = 0; i < n; i++) {//遍历整个矩阵,i为行号,j为列号
for (int j = 0; j < n; j++) {
if (A[i][j] > A[i][k] + A[k][j]) {//以Vk为中转点的路径更短
A[i][j] = A[i][k] + A[k][j];//更新最短路径长度
path[i][j] = k;//更新中转点
}
}
}
}
时间复杂度O(n^3)
弗洛伊德算法可以解决带负权值的图(但解决不了负权回路的图,这种图也没有最短路径)
有向无环图
/*
有向无环图:一个有向图中不存在环,简称DAG图
1.把各个操作数不重复的排成一排
2.标出各个运算符的生效顺序(先后顺序有点出入无所谓)
3.按顺序加入运算符,注意分层
4.操作数不会重复,检查操作符即可。从底向上逐层检查同层的运算符是否可以合体
AOV网(用顶点表示活动的网)有向边<Vi,Vj>表示活动i先于j进行
拓扑排序的实现:
1.从AOV网中选择一个没有前驱(入度为0)的顶点并输出
2.从网中删除该结点和所有以它为起点的有向边
3.重复1、2直到当前AOV网为空或当前网中不存在无前驱的顶点为止
每个AOV网都有一个或多个拓扑排序序列
*/
#define MaxVertexNum 100 //图中顶点的最大值
typedef int VertexType;
typedef struct ArcNode { //边表结点
int adjvex; //该弧所指向的顶点的位置
struct ArcNode* nextarc;//指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode {//顶点表结点
VertexType data;//顶点信息
ArcNode* firstarc;//指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct {
AdjList Vertices;//邻接表
int vexnum, arcnum;//图的顶点数和弧数
}Graph;
bool TopologicalSort(Graph G) {
InitStack(S);//栈的初始化
for (int i = 0; i < G.vexnum; i++)
if (indegree[i] == 0)
Push(S, i);//将所有入度为零的顶点进栈
int count = 0;//计数,记录当前已经输出的顶点数
while (!IsEmpty(S)) {//栈不空,则存在入度为0的顶点
Pop(S, i);//栈顶元素出栈
print[count++] = i;//输出顶点i
for (p = G.Vertices[i].firstarc; p; p = p->nextarc) {
//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈s
v = p->adjvex;
if (!(--indegree[v]))
Push(S, v);//入度为0,则入栈
}
}
if (count < G.vexnum)
return false; //排序失败,有向图中有回路
else
return true;//拓扑排序成功
}
//以上为求拓扑排序序列的算法,同样的思想我们也可以求逆拓扑排序序列,同时我们要认识到使用不同的储存结构会对算法时间复杂度有影响(产出指向顶点的边的时候,邻接表需要遍历整个表)
//逆拓扑排序的实现(DFS算法)思想是在深度优先搜索退栈的时候打印顶点
void DFSTraverse(Graph G) {//对图G进行深度优先遍历
for (v = 0; v < G.vexnum; ++v)
visited[v] = false;//初始化已访问标记数据
for (v = 0; v < G.vexnum; ++v)//本代码从v=0开始遍历
if (!visited[v])
DFS(G, v);
}
void DFS(Graph G, int v)//从顶点v出发深度优先遍历图G
{
visited[v] = True;//设已访问标记
for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighor(G, v, w))
if (!visited[w])//w为u的尚未访问的邻接顶点
DFS(G, w);
print(v);//输出顶点
}
/*
AOE网:带权有向图中,以顶点表示事件,以有向边表示活动,
以边上权值表示完成该活动的开销(如完成活动所需的时间),
称之为用边表示活动的网络
关键路径:从源点到汇点的有向路径可能有很多条,所有路径中,具有最大路径长度的路径称为关键路径
关键活动:关键路径上的活动称为关键活动
要会求事件的最早发生时间、事件的最迟发生时间、活动的最早发生时间、活动的最迟发生时间、活动的时间余量
*/