图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
- 在图中数据元素称之为顶点。
- 在图结构中不允许没有顶点,若V是顶点的集合,则强调顶点集合V有穷非空。
- 在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
各种图定义
无向边:若顶点 vi 到 vj 之间的边没有方向,则称这条边,用无序偶对(vi, vj)来表示。
G = (V1, {E1}) V1 = {A, B, C, D} E1 = {(A, B), (B, C), (C, D), {D, A}, (A, C)}
有向边:若从顶点 vi 到 vj 的边有方向,则称这条边为有向边,也称为弧。用有序偶<vi, vj>来表示,vi为弧尾,vj为弧头。
G = (V1, {E1}) V1 = {A, B, C, D} E1 = {<A, D>, <B, A>, <C, A>, <B, C>}
若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全 图。含有n个顶点的无向完全图有n × (n - 1) / 2条边。
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n × (n - 1)条边。
有很少条边或弧称为稀疏图,反之称为稠密图。
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权。这种带权的图通常称为网。
路径的长度时路径上的边或弧的数目。
第一个顶点到最后一个顶点相同的路径称为回路或环。序列中顶点不重复的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
图的存储结构
邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组存储图中的边或弧的信息。
typedef char VertexType; // 顶点类型可自定义
typedef int EdgeType; // 边上的权值类型可自定义
#define MAXVEX 100 // 最大顶点数
typedef struct {
VertexType vexs[MAXVEX]; // 顶点表
EdgeType arc[MAXVEX][MAXVEX]; // 邻接矩阵,可看作边表
int numVertexes, numEdges; // 图中当前的顶点数和边数
}MGraph;
/**
建立无向网图的邻接矩阵表示
@param G 邻接矩阵
*/
void CreateMGraph(MGraph *G) {
int i, j, w;
printf("输入顶点数和边数:\n");
scanf("%d, %d", &G->numVertexes, &G->numEdges); //输入顶点数和边数
for (int i = 0; i < G->numVertexes; i++) { //读入顶点信息,建立顶点表
scanf("%c", &G->vexs[i]);
}
for (int i = 0; i < G->numVertexes; i++) {
for (int j = 0; j < G->numVertexes; j++) {
G->arc[i][j] = INFINITY; //邻接矩阵初始化
}
}
for (int k = 0; k < G->numEdges; k++) { //读入numEdges条边,建立邻接矩阵
printf("输入边(vi, vj)上的下标i,下标j和权w:\n");
scanf("%d, %d, %d", &i, &j, &w); //输入边(vi, vj)上的权 w
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //因为是无向图,矩阵对称
}
}
邻接表
typedef struct EdgeNode { //边表结点
int adjvex; //邻接点域,存储该结点对应的下标
EdgeType weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode { //顶点表结点
VertexType data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode, AdjList[MAXVEX];
typedef struct {
AdjList adhList;
int numVertexes, numEdges; // 图中当前的顶点数和边数
}GraphAdjList;
/**
建立图的邻接表结构
@param G 邻接表
*/
void CreateALGraph(GraphAdjList *G) {
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d, %d", &G->numVertexes, &G->numEdges);
for (i = 0; i < G->numVertexes; i++) {
scanf("%c", &G->adhList[i].data);
G->adhList[i].firstedge = NULL;
}
for (k= 0; k < G->numEdges; k++) {
printf("输入边(vi, vj)上的顶点序号:\n");
scanf("%d, %d", &i, &j);
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //生成边表结点
e->adjvex = j; //邻接序号为j
e->next = G->adhList[i].firstedge; //将e指针指向当前顶点指向的结点
G->adhList[i].firstedge = e; //将当前顶点的指针指向e
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //生成边表结点
e->adjvex = i; ///邻接序号为i
e->next = G->adhList[i].firstedge;
G->adhList[i].firstedge = e;
}
}
十字链表(有向图)
把邻接表与逆邻接表结合起来
边表结点结构
tailvex | headvex | headlink | tailink |
---|
邻接多重表(无向图)
边表结点结构
ivex | ilink | jvex | jlink |
---|
图的遍历
深度优先遍历
也称为深度优先搜索,简称为DFS。
它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径想通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
typedef int boolean; //boolean是布尔类型,其值是ture或false
boolean visited[MAXVEX]; //访问标志的数组
/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph G, int i) {
visited[i] = true;
printf("%c", G.vexs[i]);
for (int j = 0; j < G.numVertexes; j++) {
DFS(G, j);
}
}
/* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G) {
for (int i = 0; i < G.numVertexes; i++) {
visited[i] = false; //初始所有顶点状态都是未访问过状态
}
for (int i = 0; i < G.numVertexes; i++) {
if (!visited[i]) { //对未访问的顶点调用DFS,若是连通图,只会执行一次
DFS(G, i);
}
}
}
/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i) {
EdgeNode *p;
visited[i] = true;
printf("%c", GL.adhList[i].data);
p = GL.adhList[i].firstedge;
while (p) {
if (!visited[p->adjvex]) {
DFS(GL, p->adjvex);
}
p = p->next;
}
}
/* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL) {
for (int i = 0; i < GL.numVertexes; i++) {
visited[i] = false;
}
for (int i = 0; i < GL.numVertexes; i++) {
if (!visited[i]) {
DFS(GL, i);
}
}
}
广度优先遍历
也称为广度优先搜索,简称BFS。
/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G) {
Queue Q;
for (int i = 0; i < G.numVertexes; i++) {
visited[i] = false;
}
InitQueue(&Q);
for (int i = 0; i < G.numVertexes; i++) {
if (!visited[i]) {
visited[i] = true;
printf("%c", G.vexs[i]);
EnQueue(&Q, i);
while (!QueueEmpty(Q)) {
DeQueue(&Q, &i);
for (int j = 0; j < G.numVertexes; j++) {
if (G.arc[i][j] == 1 && !visited[j]) {
visited[j] = true;
printf("%c", G.vexs[j]);
DeQueue(&Q, &j);
}
}
}
}
}
}
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL) {
EdgeNode *p;
Queue Q;
for (int i = 0; i < GL.numVertexes; i++) {
visited[i] = false;
}
InitQueue(&Q);
for (int i = 0; i < GL.numVertexes; i++) {
if (!visited[i]) {
visited[i] = true;
printf("%c", GL.adhList[i].data);
EnQueue(&Q, i);
while (!QueueEmpty(Q)) {
DeQueue(&Q, &i);
p = GL.adhList[i].firstedge;
while (p) {
if (!visited[p->adjvex]) {
visited[p->adjvex] = true;
printf("%c", GL.adhList[p->adjvex].data);
EnQueue(&Q, i);
}
p = p->next;
}
}
}
}
}
最小生成树
构造连通网的最小代价生成树称为最小生成树。
经典算法有Prim算法和Kruskal算法。
Prim算法
void MinSpanTree_Prim(MGraph G) {
int min, i, j, k;
int adjvex[MAXVEX]; //保存相关顶点下标
int lowcost[MAXVEX]; //保存相关顶点顶点间边的权值
lowcost[0] = 0; //初始化第一个权值为0,在这里j就是此下标的顶点已经加入生成树
adjvex[0] = 0; //初始化第一个顶点下标为0
for (i = 0; i < G.numVertexes; i++) { //循环除下标为0外的全部顶点
lowcost[i] = G.arc[0][i]; //将0顶点与之有边的权值存入数组
adjvex[i] = 0; //初始化
}
for (i = 0; i < G.numVertexes; i++) {
min = INFINITY;
j = 1;
k = 0;
while (j < G.numVertexes) { //循环全部顶点
if (lowcost[j] != 0 && lowcost[j] < min) {
min = lowcost[j]; //z让当前权值成为最小值
k = j; //将当前最小值的下标存入k
}
j++;
}
printf("(%d, %d)", adjvex[k], k);
lowcost[k] = 0; //将当前顶点的权值设为0,表示此顶点d已经完成任务
for (i = 0; i < G.numVertexes; i++) { //循环全部顶点
if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) { //若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值
lowcost[j] = G.arc[k][j]; //将较小权值存入
adjvex[j] = k; // //将下标为k的顶点存入
}
}
}
}
时间复杂度为O(n2)
Kruskal算法
/* 查找连线顶点的尾部下标 */
int Find(int *parent, int f) {
while (parent[f] > 0) {
f = parent[f];
}
return f;
}
void MinSpanTree_Kruskal(MGraph G) {
int i, begin, end;
Edge edges[MAXVEX]; //定义边集数组
int parent[MAXVEX]; //定义一数组用来判断边与边是否形成环路
for (i = 0; i < G.numVertexes; i++) {
parent[i] = 0;
}
for (i = 0; i < G.numEdges; i++) {
begin = Find(parent, edges[i].begin);
end = Find(parent, edges[i].end);
if (begin != end) { //如果begin与end不等,说明此边没有与现有生成树形成环路
parent[begin] = end; //将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经咋生成树集合中
printf("(%d, %d) %d,", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
时间复杂度为O(e㏒e)
Kruskal算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而Prim算法对于稠密图,即边数非常多的情况会更好。
最短路径
对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。
Dijkstra算法
#define MAXVEX 9 //最大顶点数
typedef int Patharc[MAXVEX]; //用于存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX]; //用于存储到各点最短路径的权值和
// P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *P) {
int v, w, k = 0, min = 0;
int final[MAXVEX];
ShortPathTable *D = NULL;
for (v = 0; v < G.numVertexes; v++) { //初始化数据
final[v] = 0;
(*D)[v] = G.arc[v0][v];
(*P)[v] = 0;
}
(*D)[v0] = 0;
final[v0] = 1; //这两句都是v0-v0的路径
for (v = 1; v < G.numVertexes; v++) {
min = INFINITY;
for (w = 0; w < G.numVertexes; w++) {
if (!final[w] && (*D)[w] < min) {
k = w;
min = (*D)[w]; //w顶顶啊离v0顶点更近
}
}
final[k] = 1; //将目前找到的最近的顶点为1
for (w = 0; w < G.numVertexes; w++) { //修正当前最短路径及距离
if (!final[w] && (min + G.arc[k][w] < (*D)[w])) { //如果经过v顶点的路径比现在这条路径的路径长度短的话
(*D)[w] = min + G.arc[k][w]; //修改当前路径长度
(*P)[w] = k;
}
}
}
}
Floyd算法
如果要求所有顶点至所有顶点的最短路径问题时,可以使用Floyd
typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D) {
int v, w = 0, k;
for (v = 0; v < G.numVertexes; v++) {
(*D)[v][w] = G.arc[v][w]; //D[v][w]值即为对应点间的权值
(*P)[v][w] = w; //初始化P
}
for (k = 0; k < G.numVertexes; k++) {
for (v = 0; v < G.numVertexes; v++) {
for (w = 0; w < G.numVertexes; w++) {
if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w]) { //如果经过下标为k顶点路径比原两点间l路径更短
(*D)[v][w] = (*D)[v][k] + (*D)[k][w]; //将当前两点间权值设为更小的一个
(*P)[v][w] = (*P)[v][k]; //路径设置经过下标为k的顶点
}
}
}
}
}
拓扑排序
无环图
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,称为AOV网。
对一个有向图构造拓扑序列的过程。构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环的AOV网了;如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环,不是AOV网。
typedef struct EdgeNode { //边表结点
int adjvex; //邻接j点域,存储该顶点对应的下标
int weight; //用于存储权值,对于非网图可以x不需要
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode { //顶点表结点
int in; //顶点入度
int data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode, AdjList[MAXVEX];
typedef struct {
AdjList adjList;
int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;
Status TopologicalSort(GraphAdjList GL) {
EdgeNode *e;
int i, k, gettop;
int top = 0; //用于栈指针下标
int count = 0; //用于d统计输出顶点的个数
int *stact; //建栈存储输入度为0的顶点
stact = (int*)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++) {
if (GL->adjList[i].in == 0) {
stact[++top] = i; //将入度为0的顶点入栈
}
}
while (top != 0) {
gettop = stact[--top]; //出栈
printf("%d ->", GL->adjList[gettop].data);
count++;
for (e = GL->adjList[gettop].firstedge; e; e = e->next) { //对此顶点弧表遍历
k = e->adjvex;
if (!(--GL->adjList[k].in)) { //将K号顶点邻接i点的入度为1
stact[++top] = k; //若为0则入栈,以便于下次循环输出
}
}
}
if (count < GL->numVertexes) { //如果count小于顶点树,说明存在环
return ERROR;
} else {
return OK;
}
}
关键路径
在一个表示工程的带权有向图中,用顶点表示时间,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称之为AOE网。
路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动。
int *etv, *ltv; //时间最早发生时间和最迟发生时间数组
int *stack2; //用于存储拓扑序列的栈
int top2; //t用于stack2的指针
Status TopologicalSort(GraphAdjList GL) {
EdgeNode *e;
int i, k, gettop;
int top = 0; //用于栈指针下标
int count = 0; //用于d统计输出顶点的个数
int *stact; //建栈存储输入度为0的顶点
stact = (int*)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++) {
if (GL->adjList[i].in == 0) {
stact[++top] = i; //将入度为0的顶点入栈
}
}
top2 = 0;
etv = (int*)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++) {
etv[i] = 1;
}
stack2 = (int*)malloc(GL->numVertexes * sizeof(int));
while (top != 0) {
gettop = stact[--top]; //出栈
printf("%d ->", GL->adjList[gettop].data);
count++;
stack2[++top2] = gettop; //将弹出的顶点序号压入拓扑序列的栈
for (e = GL->adjList[gettop].firstedge; e; e = e->next) { //对此顶点弧表遍历
k = e->adjvex;
if (!(--GL->adjList[k].in)) { //将K号顶点邻接i点的入度为1
stact[++top] = k; //若为0则入栈,以便于下次循环输出
}
if (etv[gettop] + e->weight > etv[k]) { //求各顶点时间最早发生时间值
etv[k] = etv[gettop] + e->weight;
}
}
}
if (count < GL->numVertexes) { //如果count小于顶点树,说明存在环
return ERROR;
} else {
return OK;
}
}
void CriticalPath(GraphAdjList GL) {
EdgeNode *e;
int i, gettop, k, j;
int ete, lte;
TopologicalSort(GL);
ltv = (int*)malloc(GL->numVertexes * sizeof(int));
for (i = 0; i < GL->numVertexes; i++) {
ltv[i] = etv[GL->numVertexes];
}
while (top2 != 0) {
gettop = stack2[top2--]; //将拓扑序列出栈,后进先出
for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
k = e->adjvex;
if (ltv[k] - e->weight < ltv[gettop]) { //求各顶点事件最晚发生时间
ltv[gettop] = ltv[k] - e->weight;
}
}
}
for (j = 0; j < GL->numVertexes; i++) {
for (e = GL->adjList[i].firstedge; e; e = e->next) {
k = e->adjvex;
ete = etv[j]; //活动最早发生时间
lte = ltv[k] - e->weight; //活动最晚发生时间
if (ete == lte) { //若相等jj就在关键路径上
printf("<v%d, v%d> length: %d", GL->adjList[i].data, GL->adjList[k].data, e->weight);
}
}
}
}
网友评论