1.图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成的,通常表示为G(V,E)
G:graph V:vertex E:edge
需要注意的地方:
线性表 | 树 | 图 | |
数据元素 | 元素 | 结点 | 顶点 |
没有结点 | 空表 | 空树 | 必须有结点,边集可为空 |
关系 | 相邻数据元素之间具有线性关系 | 相邻两层结点之间具有层次关系 | 任意两个顶点之间都可能有关系,逻辑关系用边来表示 |
各种定义:
无向图 | 有向图 | |
边 | 无向边(Edge):顶点之间的边没有方向,称作无向边,用无序偶对 | 有向边(Arc):若从 |
图 | 无向图(undirected graphs):图中任意两个顶点之间的边都是无向边 | 有向图(directed graphs):图中任意两个顶点之间的边都是有向边 |
简单图 | 不存在顶点到自身的边,且同一条边不重复出现![]() | |
完全图 | 无向完全图:任意两个顶点之间都存在边,n个顶点有 | 有向完全图:任意两个顶点之间都存在方向相反的两条边,n个顶点有 |
稀疏图和稠密图 | 有很少条边或弧的图称作稀疏图,反之为稠密图 | |
权(Weight) | 与图的边或弧相关的数 | |
网(Network) | 带权的图 | |
子图(Subgraph) | 如果一个图的顶点和边均在另一个大图内存在就把这个图称作大图的子图 | |
邻接点(Adjacent) | 对于无向图若两个顶点之间有联系,这两个顶点互为邻接点 | 弧尾邻接到弧头,弧头邻接自弧尾 |
边(弧)和顶点 | 两个顶点之间的边依附于(incident)这两个顶点,或边与顶点相关联 | 边与顶点相关联 |
顶点的度(Degree) |
和顶点相连的边的数目 |
以顶点为头的弧的数目称作入度(InDegree) 以顶点为尾的弧的数目称作出度(OutDegree) |
路径(Path) | 无向图中从顶点a到b的路径是一个顶点序列 | 有向图的路径必须有向,两个顶点之间可能不存在路径 |
路径长度 | 路径上边或者弧的数量 | |
回路或环(Cycle) | 第一个顶点和最后一个顶点相同的路径 | |
简单回路,路径,环 | 除了头尾顶点可以重复其他的顶点均不能重复 | |
连通图 | 图中任意两个顶点都是连通的 | 对于每一对顶点ab,从a到b和从b到a都有路径称为强连通图 |
连通分量 |
无向图中的极大连通子图称作连通分量,需要满足: 1.要是子图 2.子图要连通 3.连通子图内含有极大顶点数 4.具有极大顶点数的连通子图包含依附于这些顶点的所有边 5.非连通图也可以有连通分量 | 有向图中的极大强连通子图称作有向图的强连通分量 |
生成树 |
连通且n个顶点n-1条边叫生成树 | 有向图中一个顶点入度为0(根结点),其余顶点入度为1(父母只有一个)则是有向树,一个有向图由若干有向树构成生成森林 |
2.图的存储结构
1.邻接矩阵(二维数组存储)
对于无向图:若两点有连线就在表格行列相交处记1
对于有向图:行代表弧头,列代表弧尾,竖着看,从A开始做弧头,看那些弧指向A,标记其弧尾顶点
2.邻接表(数组+链表)
对于无向图:记录每个顶点相连的顶点即可
对于有向图:记录的只能是出去指向的顶点(出的第一个顶点)
注意:表头结点包括数据域和指针域(第一个边表结点的地址),边表结点包括边指向的顶点的数组下标,数据域(权),指针域(指向下一个边表结点)
3.十字链表
也就是邻接表和逆邻接表的结合,利用了每一个箭头都有出和入,记录的所有入的和所有出的相同,也就是邻接表和逆邻接表的链表结点个数相同,那就可以在创建邻接表的时候顺手就创建完逆邻接表使得更快了解入度以及出度的情况
第一步:先完成邻接表
填写出去到的结点,完成邻接表
完成逆邻接表,补全剩余的指针位置
v0有两个入边,所以在v0的headline指向v2后再无v0的入边顶点,所以其headline为空
4.邻接多重表
这个是针对于无向图而言的
第一步:先画出基本的结构,包括顶点数组由数据域和指针域组成,然后是边表结点包括边首尾的两个顶点和两个指针,分别指向与前面顶点连接的边
第二部:连线
技巧:比如从v0出发就去找0后面指针是空的,然后连起来,直到找到最后一个时,标记空指针
为什么:因为就是为了把和0相连的所有边串起来
所以有5条边,理论上应该有10个指针是用上的,4个指针是空的,图符合猜想
5.边集数组
边集数组是由两个一维数组组成的一个记录顶点信息,一个存储边的信息,其中边数组由起点,终点和权三个要素组成,是主要关注边的集合。例如:
v0 |
v1 | v2 |
begin | weight | end | |
edge1 | 0 | 2 | 1 |
edge2 | 1 | 3 | 2 |
edge3 | 2 | 4 | 0 |
3.图的遍历
1.深度优先搜索
如果有图:先选定一个顶点,开始前规定多个顶点可选如何选的规则,比如每次都走右手边
然后一路走到底,直到前面的点走过或没点可走,然后回退走其他路。
例:
A->
BCD可选但是右手规则选B
A->B
A->B->E->F->C
无路可走,回退一直到A
可选BCD,但是BC已走过,选D
A->B->E->F->C->D
D有GH可走,根据右手规则,选择G
A->B->E->F->C->D->G
A->B->E->F->C->D->G->I->H
如果有邻接表:
v1出发,然后到v2,然后继续走
遇到走过的就跳过
遇到走不通的要回退
以此类推直到遍历完所有点
2.广度优先搜索
如果有图:按照层次找
就是找某个点离起点有几步,把步数相同的分到一起
如果有邻接表:
用队列思想,出去一个再把和这个点连接且为未访问过的点放进来,直到所有点都入队列
v1入队 | v1 |
v1出队,相连的入队 | v2 v3 v4 |
v2出队,相连的入队 | v3 v4 v7 v6 v5 |
全部已入队 | |
出队,顺序是 | 1 2 3 4 7 6 5 |
4.最小生成树
解决带权值的最短路径问题例如:在城市之间架设通信网使得总长度最
1.普里姆算法(Prime)
操作:先选定一个顶点开始,再选择和顶点相连且没被选过的最短的路径
以下图为例
假设选定A,有长度6,7的边可选,这里选择短的6
现在AB两点可选的有四条边,这里选最短的7
现在ABD可选的边有6条,同样选最短的5
现在再从可选的边选最小的4完成连线
有点像贪心算法,每次都从可选的东西里面选最好的,从所有线里面选择最小的,使得全局收益最大
2.克鲁斯卡尔算法(Kurskal)
克鲁斯卡尔算法是基于贪心思想,每次选取权值最小且不与已有边构成环的边来构造生成树。
为什么不允许环生成:
如果允许生成环,那么贪心策略选取边的依据就会被破坏,无法保证最终构造出的是全局最优的最小生成树。
例:
此时最短的是EC边,长度:4
此时最短的是ED边,长度:
看起来6最短但是形成了环,不符合
(X)
(V)
最后连AD完成连线
点少边多选普里姆算法
边少点多选克鲁斯卡尔算法
5.最短路径
1.迪杰斯特拉算法(Dijkstra)
详细解释:
核心:每次都从已知的边里面选择最短的使得最后总的结果是最优解
eg:假定从s进入
现在s到2,6,7长度已知,选择最短的9,此时2已经找到了最短路径,再引入与2相连且未引入的边,这样是一个循环
下一步把和2相连的3引入,那么s到3的距离也已知了,应该是33,现在已知s到3,6,7的长度分别是33,14,15,选择最短的14,做完之后6就已知了,再把和6相连的点连接上,更新当前其到出发点的最短距离,以此类推。
特别注意,当某一个点到原点的距离变小了需要及时更新,例如s->6->5的距离是44,但是s->6->3->5的距离是34,那出发点到5的最短距离就需要更新
关于计算机算法实现:
类似于手动实现,需要一个表格来记录当前所有点到出发点的最短距离,不知道记作无穷,因为一开始只知道与起点相连的边,其余点与出发点的距离都是无穷大,当后期有更短的距离时再更新,再从所有距离里面选择最短的
2.弗洛伊德算法(Floyd)
弗洛伊德算法相比迪杰斯特拉算法简洁很多,只需要三个循环就能实现,但是代价是三个循环导致时间复杂度很高(O(n^3))
弗洛伊德算法的核心是中转点
如果是A直接走到C,长度是5,但是借助中转点B,最短长度就是3
第一层循环:作用是换中转点,记作k
第二层循环:作用是从谁出发,记作v
第三层循环:作用是到达谁,记作w
循环内部执行代码:
1.更新从谁出发(v)经过中转点(k)到达终点(w)的长度
2.更新从谁出发(v)到达终点(w)所需要经过的中转点(k)
通过不断循环找所有两点间最短路径,最终找到目标两点间的最短路径
6.拓扑排序
1.AOV网与拓扑排序
(Activity On Vertex Network)有向图为顶点表示活动的网
特点是顶点的执行有先后顺序,常用于判定工程的可行性以及确定各项活动在工程中的执行顺序
比如:
拓扑排序就是排出事件执行的先后顺序
需要注意的点:
1.aov网不能有回路,否则始终会卡在回路里面
2.只有当开启一个事件所有的预备条件都完成时才可以开启这个事件,不可以调换顺序
3.拓扑排序最终的结果不唯一,比如拍电影需要请演员和准备剧本,但是这两个事件只需要在拍电影之前完成就行没有特别的顺序要求所以顺序可以调换
如何拓扑排序?
把入度为0的点拿出来,然后更新入度,再重复步骤直到完成事件
2.AOE网与关键路径
有什么用?
确定完成整个工程至少需要多少时间,那些活动时影响工程进度的关键
如何找到关键路径?
step1:找所有事件的最早开始时间(正向选最晚)
比如完成3需要先完成v1和v2,只有当两个事件都完成才能开启v3,这是最早开始时间,再早一点无法完成2->3的过程,无法开始v3
step2:找所有事件的最晚开始事件(逆向选最早)
汇点最晚等于最早,因为这个事情在汇点就已经完成了
那往回推导v6最迟应该在25开始工作,最多拖延1,否则会使得汇点最终完成时间推迟
同样的对于v4,最迟应该在15开始才能保证v7在规定时间开始,在16前开始才能保证v6在规定时间开始,那么为了二者都满足应该在15开始,因此15就是最晚发生时间这样整体节奏就不会被拖延
step3:找到最早开始与最晚开始时间相同的事件,说明这个事件很重要,时间不能变,没有余量
连线就是关键路径,所选的点就是关键活动
7.代码区
1.无向图邻接矩阵
一个大结构体,包含一维数组存顶点,二维数组存储边,以及边和顶点的个数。
其中初始化,输入顶点的名字放入一维数组,再用两层for循环将所有边赋值为infinity表示无连接。
赋值时由于是无向图,输入一次数据即可,再用对称性质赋值另外一处数据
G->arc[x][y] = G->arc[y][x]
有向图只能一条一条赋值,注意入和出的方向,行出列入,其他均相同
#include <iostream>
using namespace std;
typedef char VertexType;//顶点类型
typedef int EdgeType;//边类型
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numNodes, numEdges;
}MGraph;
void CreateMGraph(MGraph* G) {
int x, y,w;
cout << "输入顶点数和边数:" << endl;
cin >> G->numNodes >> G->numEdges;
cout << "输入顶点:" << endl;
for (int i = 0; i < G->numNodes; i++)
{
cin >> G->vexs[i];
}
cout << endl;
for (int i = 0; i < G->numNodes; i++)//初始化边集数组
{
for (int j = 0; j < G->numNodes; j++)
{
G->arc[i][j] = INFINITY;
}
}
for (int i = 0; i < G->numEdges; i++)//赋值边集数组
{
cout << "输入边(vi,vj)上的下标i,下标j和权w:" << endl;
cin >> x >> y >> w;/*输入边(vi,vj)上的权w */
G->arc[x][y] = w;
G->arc[x][y] = G->arc[y][x]; /* 因为是无向图,矩阵对称 */
}
}
int main() {
MGraph G;
CreateMGraph(&G);
return 0;
}
2.无向图邻接表
代码分为三个部分,创建结构体,创建顶点数组,创建边表
这三个结构体有嵌套关系包含最大的是第三个结构体,包含顶点数组,顶点和边的总数。而顶点数组又包含了指向边链表的指针,边链表又含有边结构体。
其次就是赋值,先处理顶点不要管边,顶点赋值后全部都指向NULL
然后处理边,由于是无向图,处理时输入一条边要用两次,先分配内存,写序号
e = (EdgeNode*)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex = j; /* 邻接序号为j */
然后修改指针,先改后面的,再改前面的
e->next = G->adjList[i].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[i].firstedge = e; /* 将当前顶点的指针指向e */
完整代码:
#include<iostream>
using namespace std;
typedef char VertexType;//顶点类型
typedef int EdgeType;//边类型
#define MAXVEX 100
#define INFINITY 65535
typedef struct EdgeNode {
int adjvex;
EdgeType info;
struct EdgeNode* next;
}EgdeNode;
typedef struct VertexNode {
VertexType data;
EgdeNode* firstedge;
}VertexNode,AdjList[MAXVEX];
typedef struct {//存储总的数据,包括顶点数组和顶点数和边数
AdjList adjList;
int numNodes, numEdges;
}GraphAdjList;
void CreateALGraph(GraphAdjList *G){
int i, j, k;
EdgeNode* e;
cout << "输入顶点数和边数:" << endl;
cin >> G->numNodes >> G->numEdges;
cout << "输入顶点信息:" << endl;
for (i = 0; i < G->numNodes; i++) /* 读入顶点信息,建立顶点表 */
{
cin >> G->adjList[i].data;/* 输入顶点信息 */
G->adjList[i].firstedge = NULL; /* 将边表置为空表 */
}
for (k = 0; k < G->numEdges; k++)/* 建立边表 */
{
cout << "输入边(vi,vj)上的顶点序号:" << endl;
cin >> i >> j;/* 输入边(vi,vj)上的顶点序号 */
e = (EdgeNode*)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex = j; /* 邻接序号为j */
e->next = G->adjList[i].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[i].firstedge = e; /* 将当前顶点的指针指向e */
e = (EdgeNode*)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex = i; /* 邻接序号为i */
e->next = G->adjList[j].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[j].firstedge = e; /* 将当前顶点的指针指向e */
}
}
int main(void)
{
GraphAdjList G;
CreateALGraph(&G);
return 0;
}
3.邻接矩阵完成深度优先搜索(DFS)
#include <iostream>
using namespace std;
typedef char VertexType;//顶点类型
typedef int EdgeType;//边类型
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numNodes, numEdges;
}MGraph;
void CreateMGraph(MGraph* G) {
int x, y,w;
cout << "输入顶点数和边数:" << endl;
cin >> G->numNodes >> G->numEdges;
cout << "输入顶点:" << endl;
for (int i = 0; i < G->numNodes; i++)
{
cin >> G->vexs[i];
}
cout << endl;
for (int i = 0; i < G->numNodes; i++)//初始化边集数组
{
for (int j = 0; j < G->numNodes; j++)
{
G->arc[i][j] = INFINITY;
}
}
for (int i = 0; i < G->numEdges; i++)//赋值边集数组
{
cout << "输入边(vi,vj)上的下标i,下标j和权w:" << endl;
cin >> x >> y >> w;/*输入边(vi,vj)上的权w */
G->arc[x][y] = w;
G->arc[x][y] = G->arc[y][x]; /* 因为是无向图,矩阵对称 */
}
}
int visited[MAXVEX],times=0; /* 访问标志的数组 */
void DFS(MGraph G, int i)
{
int j;
visited[i] = 1;
printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */
times++;
if (times==G.numNodes){
return;
}
for (j = 0; j < G.numNodes; j++)
if (G.arc[i][j] < INFINITY && G.arc[i][j] != 0 && !visited[j])
DFS(G, j);/* 对为访问的邻接顶点递归调用 */
}
/* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G)
{
int i;
for (i = 0; i < G.numNodes; i++)
visited[i] = 0; /* 初始所有顶点状态都是未访问过状态 */
for (i = 0; i < G.numNodes; i++)
if (!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
DFS(G, i);
}
int main() {
MGraph G;
CreateMGraph(&G);
DFSTraverse(G);
return 0;
}
4.邻接矩阵完成广度优先搜索(BFS)
#include <iostream>
#include<queue>
using namespace std;
typedef char VertexType;//顶点类型
typedef int EdgeType;//边类型
#define MAXVEX 100
#define INFINITY 65535
typedef struct {
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numNodes, numEdges;
}MGraph;
void CreateMGraph(MGraph* G) {
int x, y,w;
cout << "输入顶点数和边数:" << endl;
cin >> G->numNodes >> G->numEdges;
cout << "输入顶点:" << endl;
for (int i = 0; i < G->numNodes; i++)
{
cin >> G->vexs[i];
}
cout << endl;
for (int i = 0; i < G->numNodes; i++)//初始化边集数组
{
for (int j = 0; j < G->numNodes; j++)
{
G->arc[i][j] = INFINITY;
}
}
for (int i = 0; i < G->numEdges; i++)//赋值边集数组
{
cout << "输入边(vi,vj)上的下标i,下标j和权w:" << endl;
cin >> x >> y >> w;/*输入边(vi,vj)上的权w */
G->arc[x][y] = w;
G->arc[x][y] = G->arc[y][x]; /* 因为是无向图,矩阵对称 */
}
}
int visited[MAXVEX],times=0; /* 访问标志的数组 */
void BFSTraverse(MGraph G) {
queue<char> Q;
for (int i = 0; i < G.numNodes; i++){
visited[i] = 0;
}
for (int i = 0; i < G.numNodes; i++)
{
if (!visited[i]) {
visited[i] = 1;
printf("%c ", G.vexs[i]);
Q.push(i);
while (!Q.empty()) {
i = Q.front();
Q.pop();
for (int j = 0; j < G.numNodes; j++)
{
if (G.arc[i][j] < INFINITY && !visited[j]) {
visited[j] = 1;
printf("%c ", G.vexs[j]);
Q.push(j);
}
}
}
}
}
}
int main() {
MGraph G;
CreateMGraph(&G);
BFSTraverse(G);
return 0;
}
5.邻接表完成深度优先搜索(DFS)
本质上还是递归,一条路走到底,如果是连通图可以直接走完,走到死路了再回头找其他路(跳出当前层回到上一层)
Boolean visited[MAXSIZE]; /* 访问标志的数组 */
/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
p = GL->adjList[i].firstedge;
while(p)
{
if(!visited[p->adjvex])
DFS(GL, p->adjvex);/* 对为访问的邻接顶点递归调用 */
p = p->next;
}
}
/* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL)
{
int i;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE; /* 初始所有顶点状态都是未访问过状态 */
for(i = 0; i < GL->numVertexes; i++)
if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
DFS(GL, i);
}
6.邻接表完成广度优先搜索(BFS)
1.有一个双层while循环,其中外层循环作用是检查此时队列是否为空,如果为空就把没用过的顶点数组中的结点按照顺序放入
2.内层还有一个while作用是把顶点元素后面跟着的所有没用过的点放入(和顶点相连且没用过)
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i]=TRUE;
printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
EnQueue(&Q,i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p = GL->adjList[i].firstedge; /* 找到当前顶点的边表链表头指针 */
while(p)
{
if(!visited[p->adjvex]) /* 若此顶点未被访问 */
{
visited[p->adjvex]=TRUE;
printf("%c ",GL->adjList[p->adjvex].data);
EnQueue(&Q,p->adjvex); /* 将此顶点入队列 */
}
p = p->next; /* 指针指向下一个邻接点 */
}
}
}
}
}
【补】7.邻接矩阵转邻接表(参考《大话数据结构》)
细节提示:
1.此处结构体内多了一个first in表示入度数量
2.双层嵌套循环外层i从0开始,内层由大到小,并且使用头插法,目的是使得最后的边表行顺序是从小到大(从大的数开始插,把小的数插在前面,使得总的顺序是从小到大,和直接创建邻接表时的方式略有不同)
/* 利用邻接矩阵构建邻接表 */
void CreateALGraph(MGraph G,GraphAdjList *GL)
{
int i,j;
EdgeNode *e;
*GL = (GraphAdjList)malloc(sizeof(graphAdjList));
(*GL)->numVertexes=G.numVertexes;
(*GL)->numEdges=G.numEdges;
for(i= 0;i <G.numVertexes;i++) /* 读入顶点信息,建立顶点表 */
{
(*GL)->adjList[i].in=0;
(*GL)->adjList[i].data=G.vexs[i];
(*GL)->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(i=0;i<G.numVertexes;i++) /* 建立边表 */
{
for(j=G.numVertexes-1;j>=0;j--)
{
if (G.arc[i][j]==1)
{
e=(EdgeNode *)malloc(sizeof(EdgeNode));
//下面6句代码仅仅只是为了与图书中的206页图匹配,让生成的队列符合书中图示。
//实际构建无需这样,只需理解当前就是构建一个图结构的邻接表即可
if (i==1 && j==8)
e->adjvex=6;
else if (i==1 && j==6)
e->adjvex=8;
else
e->adjvex=j; /* 邻接序号为j */
//正常代码下如下
//e->adjvex=j; /* 邻接序号为j */
e->next=(*GL)->adjList[i].firstedge; /* 将当前顶点上的指向的结点指针赋值给e */
(*GL)->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
(*GL)->adjList[j].in++;
}
}
}
}
8.普里姆算法最小生成树(图摘自《大话》p208)
注意:
1.adjvex数组作用是保存相关顶点下标,记录离当前顶点最近的点,没更新的不算
例如初始化时,所有点都是记录0表示0是离这些点最近的点
初始化 adjvex
为 0
-
表示初始顶点的关联顶点:
adjvex
数组记录生成树中与每个未加入顶点最近的顶点。初始化时,所有顶点的最近顶点都设置为第一个顶点v0
,因为生成树从v0
开始构建,初始状态下,v0
是唯一已加入生成树的顶点,其他顶点的最近顶点自然都是v0
。
然后添加1,把与1相关的点放入,这时,数据记录为001000101
后面放入5,按道理来说6和4都应该被记录为5,但是前面6的下标已经是1了,1和5冲突需要看看谁近一点了,由于1近,因此6的下标还是记1,总的是001050101了
那么这个表有什么用呢,如果没有这个表我们就不知道选出的最小边连接的到底是谁,例如找出001050101后,这时我们找到的最小边12,对应顶点8,再查表发现8连接的是1,那么输出就是(1,8)
2.lowcost数组作用是标记是否已经加入生成树,这个作用是避免已经加入生成树的顶点被重复选取,比如5可以连接064,但是0已经选过了,不然还会被再选一次,记录为0表示已加入,不选择
3.总的来说代码包含三个部分
初始化部分要设置两个数组,让所有点的连接点都记作0,刷新和v0顶点有关系的顶点相连的边的权值
循环lowcost数组找到最小值对应的点并输出
刷新lowcost数组将刚才输出的点记0,刷新adjvex把与新点相连的点引入
/* Prim算法生成最小生成树 */
void MiniSpanTree_Prim(MGraph G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /* 保存相关顶点下标 */
int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */
lowcost[0] = 0;/* 初始化第一个权值为0,即v0加入生成树 */
/* lowcost的值为0,在这里就是此下标的顶点已经加入生成树 */
adjvex[0] = 0; /* 初始化第一个顶点下标为0 */
for(i = 1; i < G.numVertexes; i++) /* 循环除下标为0外的全部顶点 */
{
lowcost[i] = G.arc[0][i]; /* 将v0顶点与之有边的权值存入数组 */
adjvex[i] = 0; /* 初始化都为v0的下标 */
}
for(i = 1; i < G.numVertexes; i++)
{
min = GRAPH_INFINITY; /* 初始化最小权值为∞, */
/* 通常设置为不可能的大数字如32767、65535等 */
j = 1;k = 0;
while(j < G.numVertexes) /* 循环全部顶点 */
{
if(lowcost[j]!=0 && lowcost[j] < min)/* 如果权值不为0且权值小于min */
{
min = lowcost[j]; /* 则让当前权值成为最小值 */
k = j; /* 将当前最小值的下标存入k */
}
j++;
}
printf("(%d, %d)\n", adjvex[k], k);/* 打印当前顶点边中权值最小的边 */
lowcost[k] = 0;/* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */
for(j = 1; j < G.numVertexes; j++) /* 循环所有顶点 */
{
if(lowcost[j]!=0 && G.arc[k][j] < lowcost[j])
{/* 如果下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
lowcost[j] = G.arc[k][j];/* 将较小的权值存入lowcost相应位置 */
adjvex[j] = k; /* 将下标为k的顶点存入adjvex */
}
}
}
}
9.克鲁斯卡尔算法
解释疑惑点:
1.关于简单的排序,原理应该是冒泡排序,把结构体看成一个整体,用sort函数和交换函数来交换,例如5 4 3 2 1,i=0,j=1,那么5和4比5大,4往前移动,变成4 5 3 2 1,以此类推变成1 5 4 3 2,这样最小的移到最前面,最多移动n-1次,这样实现了排序
2.关于方向和parent数组,虽然是无向图但是仍然要考虑头尾,生成树顾名思义就是一棵树,比如v4出v7入那么就有parent[4]=7,代表4的父母结点是7,而7对应0表示7是根结点
为什么这么做,因为parent作用就是判断是否具有闭合回路,如果两个点向上找到了同一个结点,那必然构成闭合回路,不符合最小生成树定义
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
#define MAXEDGE 20
#define MAXVEX 20
#define GRAPH_INFINITY 65535
typedef struct
{
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
typedef struct
{
int begin;
int end;
int weight;
}Edge; /* 对边集数组Edge结构的定义 */
/* 构件图 */
void CreateMGraph(MGraph *G)
{
int i, j;
/* printf("请输入边数和顶点数:"); */
G->numEdges=15;
G->numVertexes=9;
for (i = 0; i < G->numVertexes; i++)/* 初始化图 */
{
for ( j = 0; j < G->numVertexes; j++)
{
if (i==j)
G->arc[i][j]=0;
else
G->arc[i][j] = G->arc[j][i] = GRAPH_INFINITY;
}
}
G->arc[0][1]=10;
G->arc[0][5]=11;
G->arc[1][2]=18;
G->arc[1][8]=12;
G->arc[1][6]=16;
G->arc[2][8]=8;
G->arc[2][3]=22;
G->arc[3][8]=21;
G->arc[3][6]=24;
G->arc[3][7]=16;
G->arc[3][4]=20;
G->arc[4][7]=7;
G->arc[4][5]=26;
G->arc[5][6]=17;
G->arc[6][7]=19;
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/* 交换权值 以及头和尾 */
void Swapn(Edge *edges,int i, int j)
{
int temp;
temp = edges[i].begin;
edges[i].begin = edges[j].begin;
edges[j].begin = temp;
temp = edges[i].end;
edges[i].end = edges[j].end;
edges[j].end = temp;
temp = edges[i].weight;
edges[i].weight = edges[j].weight;
edges[j].weight = temp;
}
/* 对权值进行排序 */
void sort(Edge edges[],MGraph *G)
{
int i, j;
for ( i = 0; i < G->numEdges; i++)
{
for ( j = i + 1; j < G->numEdges; j++)
{
if (edges[i].weight > edges[j].weight)
{
Swapn(edges, i, j);
}
}
}
printf("权排序之后的为:\n");
for (i = 0; i < G->numEdges; i++)
{
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
/* 查找连线顶点的尾部下标 */
int Find(int *parent, int f)
{
while ( parent[f] > 0)
{
f = parent[f];
}
return f;
}
/* 生成最小生成树 */
void MiniSpanTree_Kruskal(MGraph G)
{
int i, j, n, m;
int k = 0;
int parent[MAXVEX];/* 定义一数组用来判断边与边是否形成环路 */
Edge edges[MAXEDGE];/* 定义边集数组,edge的结构为begin,end,weight,均为整型 */
/* 用来构建边集数组并排序********************* */
for ( i = 0; i < G.numVertexes-1; i++)
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<GRAPH_INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
/* ******************************************* */
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0; /* 初始化数组值为0 */
printf("打印最小生成树:\n");
for (i = 0; i < G.numEdges; i++) /* 循环每一条边 */
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) /* 假如n与m不等,说明此边没有与现有的生成树形成环路 */
{
parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中。 */
/* 表示此顶点已经在生成树集合中 */
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
int main(void)
{
MGraph G;
CreateMGraph(&G);
MiniSpanTree_Kruskal(G);
return 0;
}