首先我们需要一个图的数据结构
#include "stdio.h"
#include "stdlib.h"
#define MAX 32767
typedef struct Graph{
char* vexs;
int** arcs;
int vexNum;
int arcNum;
}Graph;
为了方便后续算法的使用,我们用MAX来表示两个节点之间没有连接
Graph* initGraph(int vexNum){
Graph* g = (Graph*)malloc(sizeof(Graph));
g->vexs = (char*)malloc(sizeof(char)*vexNum);
g->arcs = (int**)malloc(sizeof(int*)*vexNum);
for (int i=0;i<vexNum;i++){
g->arcs[i] = (int*)malloc(sizeof(int*)*vexNum);
}
g->vexNum = vexNum;
g->arcNum = 0;
return g;
}
void createGraph(Graph* g,char* vexs,int* arcs){
for (int i=0;i<g->vexNum;i++){
g->vexs[i] = vexs[i];
for (int j=0;j<g->vexNum;j++){
g->arcs[i][j] = *(arcs+i*g->vexNum+j);
if (g->arcs[i][j]!=0&&g->arcs[i][j]!=MAX){
g->arcNum++;
}
}
}
g->arcNum/=2;
}
这是初始化图和创建图函数,这样我们可以使用下面的方式来创建图
int main(){
Graph* g = initGraph(4);
int arr[16] = {
0,4,6,9,
4,0,MAX,MAX,
6,MAX,0,2,
9,MAX,2,0
};
createGraph(g,"ABCD",(int*)arr)
}
普利姆算法和克鲁斯卡尔算法
普利姆算法和克鲁斯卡尔算法是获取图的最小生成树的算法,两者都用到了MST性质,图的MST性质是:考虑图G的点集V的非空子集X,如果点u属于X,点v属于V\X(集合V对集合X的差集),且边(u,v)是权值最小的边,则边(u,v)是图G的最小生成树的一条边
typedef struct Edge{
char vex; // 记录U集合中最小边的起始节点
int weight; // 记录最小边的权值
}Edge;
Edge* initEdge(Graph* g,int index){
// 初始化edge结构体,将edge数组都赋值为开始节点的字符和边的权重
Edge* edge = (Edge*)malloc(sizeof(Edge)*g->vexNum);
for (int i=0;i<g->vexNum;i++){
edge[i].vex = g->vexs[index];
edge[i].weight = g->arcs[index][i];
}
return edge;
}
int getminEdge(Edge* edge,Graph* g){
// 找当前edge数组中权值最小的数据点对应的索引 edge数组的索引对应于图中节点的索引
int index,min=MAX;
for (int i=0;i<g->vexNum;i++){
if (edge[i].weight!=0&&min>edge[i].weight){
min = edge[i].weight;
index = i;
}
}
return index;
}
void prim(Graph* g,int index){
// prim算法输出最小生成树
int minIndex;
Edge* edge = initEdge(g,index);
for (int i=0;i<g->vexNum-1;i++){
minIndex = getminEdge(edge,g);
// 可以在这一行修改操作,进而导出最小生成树
printf("v%c->v%c ,weight = %d\n",edge[minIndex].vex,g->vexs[minIndex],edge[minIndex].weight);
// 更新edge数组
edge[minIndex].weight = 0; // 当前所在的节点权值归零(标记之后不再考虑)
for (int j=0;j<g->vexNum;j++){
if (g->arcs[minIndex][j]<edge[j].weight){
edge[j].weight = g->arcs[minIndex][j];
edge[j].vex = g->vexs[minIndex];
}
// edge数组当中最终保留的是到j节点最短路径的起始节点(所以如果比之前还要大,就不更改)
}
}
}
首先是普利姆算法,普利姆算法可以理解为找点法:从起点开始,依次寻找权值最小的边,并且将这条边的终点放入集合X,然后将这条边的终点作为下次找点的起点。当集合X等于点集V的时候,就得到了以某个点为起点的最小生成树。
typedef struct Line{ // 记录边的结构体
int start;
int end;
int weight;
}Line;
Line* initLine(Graph* g){ // 读取一下图的每一条边
int index = 0;
Line* line = (Line*)malloc(sizeof(Line)*g->arcNum);
for (int i=0;i<g->vexNum;i++){
for (int j=i+1;j<g->vexNum;j++){
if (g->arcs[i][j]!=MAX){
line[index].start = i;
line[index].end = j;
line[index].weight = g->arcs[i][j];
index++;
}
}
}
return line;
}
void sortLine(Line* line,Graph* g){ // 对边排序
Line tem;
for (int i=0;i<g->arcNum-1;i++){
for (int j=0;j<g->arcNum-1-i;j++){
if (line[j].weight>line[j+1].weight){
tem = line[j];
line[j] = line[j+1];
line[j+1] = tem;
}
}
}
}
void krustkal(Graph* g){
int* connected = (int*)malloc(sizeof(int)*g->vexNum);
for (int i=0;i<g->vexNum;i++){
connected[i] = i;
}
Line* line = initLine(g);
sortLine(line,g);
for (int j=0;j<g->arcNum;j++){
int start = connected[line[j].start];
int end = connected[line[j].end];
if (start!=end){
printf("v%c-->v%c weight = %d\n",g->vexs[line[j].start],g->vexs[line[j].end],line[j].weight);
// 这个循环只是一个连通分量的校正
for (int k=0;k<g->vexNum;k++){
if (connected[k]==end){
connected[k] = start;
}
}
}
}
}
克鲁斯卡尔算法可以理解为找边法:将边的数据记录下来,然后按照权重排序,再依次联通最小的边,当生成树连接到每个点时,我们得到了最小生成树。为了方便编写,我们这里使用冒泡排序。
普利姆算法和克鲁斯卡尔算法在广义上是一种贪心算法,但是可以用数学证明,这种贪心算法是可以得到图的最小生成树的。
迪杰斯特拉算法和弗洛伊德算法
迪杰斯特拉算法和弗洛伊德算法是获取单源最小路径。
int getMin(int* distance,int* status,Graph* g){
int res = MAX;
int index;
for (int i=0;i<g->vexNum;i++){
if (!status[i]&&distance[i]<res){
res = distance[i];
index = i;
}
}
return index;
}
// dijkstra算法获取单源最小路径
void dijkstra(Graph* g,int index){
int* status = (int*)malloc(sizeof(int)*g->vexNum); // 检查当前节点最短路径是否达到
int* pre = (int*)malloc(sizeof(int)*g->vexNum); // 存储到达当前节点的最短路径的前驱节点
int* distance = (int*)malloc(sizeof(int)*g->vexNum); // 计算路径长度
for (int i=0;i<g->vexNum;i++){
status[i] = i==index;
if (g->arcs[index][i]>0&&g->arcs[index][i]!=MAX){
pre[i] = index;
}
else {
pre[i] = -1;
}
distance[i] = g->arcs[index][i];
}
for (int i=0;i<g->vexNum;i++){
int index = getMin(distance,status,g);
status[index] = 1;
for (int j=0;j<g->vexNum;j++){
if (!status[j]&&distance[index]+g->arcs[index][j]<distance[j]){
distance[j] = distance[index] + g->arcs[index][j];
pre[j] = index;
}
}
}
for (int k=0;k<g->vexNum;k++){
printf("status = %d pre = %d distance = %d\n",status[k],pre[k],distance[k]);
}
}
迪杰斯特拉算法更像是从结尾出发,倒推出最小路径,见注释使用三个数组,如果循环过程中的新路径比当前获取的新路径更短,则进行替换;如果当前节点的最短路径未获取,则我们将它作为最小路径。
void floyd(Graph* g){
// 又叫 3 for 算法
int distance[g->vexNum][g->vexNum];
int pre[g->vexNum][g->vexNum];
// 初始化distance数组和pre数组
for (int i=0;i<g->vexNum;i++){
for (int j=0;j<g->vexNum;j++){
distance[i][j] = g->arcs[i][j];
if (g->arcs[i][j]>=0&&g->arcs[i][j]!=MAX){
pre[i][j] = i;
}
else {
pre[i][j] = -1;
}
}
}
for (int i=0;i<g->vexNum;i++){
for (int j=0;j<g->vexNum;j++){
for (int k=0;k<g->vexNum;k++){
if (distance[j][k]>distance[j][i]+distance[i][k]){
distance[j][k] = distance[j][i]+distance[i][k];
pre[j][k] = pre[i][k];
}
}
}
}
for (int i=0;i<g->vexNum;i++){
for (int j=0;j<g->vexNum;j++){
printf("v%c -- v%c distance = %d\n",g->vexs[i],g->vexs[j],distance[i][j]);
}
}
}
弗洛伊德算法又叫3for算法,因为使用3个for循环就可以解决。我们依次将每个节点作为插入节点,并且使用一个pre数组来记录当前路径的倒数第二个节点,用distance数组来记录最短路径。如果新的路径比当前已知的最短路径短,则更新distance数组和pre数组,否则保留。这种算法考虑了每个节点之间的最短路径,有暴力算法之称。
回顾往期:栈,队列,二叉树的实现和基本方法