不同于线性表的一对一和树型结构的一对多的简单结构,图结构是一种元素多对多的复杂结构。
这里主要介绍:
对于无向图:
下面使用
C语言
来描述数据结构
先把最小单位定义一下:
typedef char[4] Vertex;// 顶点信息
typedef int Weight;// 权重
typedef int Number;// 数值类
邻接矩阵
typedef Weight** Matrix;// 矩阵
typedef struct GMatrix {// 邻接矩阵存储结构
Vertex* data;// 一维数组存放顶点信息
Number n;// 顶点数
Matrix M;// 二维数组
};
边数相对顶点较少
的图可以极大程度的节省存储空间,避免浪费邻接列表
typedef struct UGLBox {// 无权邻接表项
Number v;//Vertex 顶点索引
UGLBox* next;// 指向下一无权表项
};
typedef struct WGLBox {// 有权邻接表项
Number v;//Vertex 顶点索引
Weight w;
WGLBox* next;// 指向下一有权表项
};
typedef struct GLHead {// 邻接列表头
Vertex data;// 顶点名称
void* B;//Box 表项
};
typedef struct GList {// 邻接列表存储结构
Number len;// 表长
GLHead* Head;// 指向表头数组
};
针对有向图还可以改进为十字链表
十字链表 >folded
typedef struct GOBox {// 十字链表项
Number tailV;
Number headV;
GOBox* tail;
GOBox* head;
};
typedef struct GOHead {// 十字链表头
Vertex data;
GOBox* tail;
GOBox* head;
};
typedef struct GOList {// 十字链表存储结构
Number len;
GOHead* Head;
};
同样的,针对无向图也可以改进为邻接多重表,减少增减表项时的开支
邻接多重表 >folded
typedef struct GMBox {
Number tailV;
GMBox* tail;
Number headV;
GMBox* head;
};
typedef struct GMHead {
Vertex data;
GMBox* first;
};
typedef struct GMList {
Number len;
GMHead* Head;
};
typedef struct Edge {
Number begin;
Number end;
Weight w;
};
typedef struct Edges {
Number len;
Vertex* v;
Edge* e;
};
最后还可以把输入数据规范化
>folded
typedef struct UEdge {// 无权边
Number t;//tailVertex
Number h;//headVertex
};
typedef struct WEdge {// 有权边
Number t;//tailVertex
Number h;//headVertex
Weight w;
};
typedef struct GInput {// 图输入
Vertex* data;// 顶点
Number n;// 顶点数
void* Edges;// 边的关系
Number len;// 边的关系的数量
};
按照右手原则,每次选择上一顶点的最右边的下一顶点,走过一个顶点标记一个顶点,不能走被标记过的顶点,一条路走到黑,直到无路可走,然后回溯。 这个就是先走到最大深度,不能再深入后,再返回到有支路可走的顶点继续深入到最下面。
马踏棋盘算法
马踏棋盘算法,也称骑士周游问题。在一个 8x8 的国际象棋棋盘上,用一个马按照马步(即走日字,同中国象棋的马的走法)跳遍整个棋盘,要求每个格子都只跳一次,最后回到出发点。 问题分析
通过对位移参数 1 和 2,y 轴对称,y=x 对称,y=-x 对称三步可以列出所有可能的下一步位置。(或者直接手写 8 个坐标偏移)
代码
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define X 8
#define Y 8
int chess[Y][X];
int Tag;// 遍历顺序
bool isFind;
int solveCount;
// 定义两个数据结构,分别用来存放坐标信息和权值信息
typedef struct xybox {
int x;
int y;
};
typedef struct weightbox {
int len;
xybox xy[8];
int w[8];
};
// 下一个可能跳的坐标
inline xybox nextxy(int x,int y,int count,xybox* data) {
xybox xy;
xy.x = x + data[count].x;
xy.y = y + data[count].y;
return xy;
}
// 建立马跳的格子的增量数据,总共 8 种情况 返回指针指向 xybox [8]
xybox* buildData() {
int i;
xybox* data = (xybox*)malloc(8 * sizeof(xybox));
data[0].x = -1;data[0].y = -2;
data[1].x = 1; data[1].y = -2;
//y=x 镜像处理
for (i = 2; i < 4; i++) {
data[i].x = data[i - 2].y;
data[i].y = data[i - 2].x;
}
//y=-x 镜像处理
for (i = 4; i < 8; i++) {
data[i].x = -data[i - 4].y;
data[i].y = -data[i - 4].x;
}
return data;
}
// 遍历棋盘,深度优先,使用贪心算法优化
bool traverse(int x,int y,xybox* data) {
if (Tag == X * Y) {
// 找到解,输出
isFind = true;// 已找到解
printf("第%d个解:\n", ++solveCount);
for (int i = 0; i < Y; i++) {
for (int j = 0; j < X; j++) {
printf("%d\t", chess[i][j]);
}
printf("\n");
}
return false;
}
int i;
xybox xy;
weightbox weight;
bool flag = 1;
weight.len = 0;
for (i = 0; i < 8; i++) {
xy = nextxy(x, y, i, data);
if (xy.x<0 || xy.x>=X || xy.y<0 || xy.y>=Y || chess[xy.y][xy.x]) {
// 超出边界或已被遍历,下次循环
continue;
}
else {
// 否则标记,并继续递归
weight.xy[weight.len++] = { xy.x,xy.y };
flag = 0;
chess[xy.y][xy.x] = ++Tag;
xybox xytmp;
// 模拟下一次遍历,并将遍历后可走格子数记为权重存入权重数组
for (int i = 0; i < 8; i++) {
xytmp = nextxy(xy.x, xy.y, i, data);
if (xytmp.x < 0 || xytmp.x >= X || xytmp.y < 0 || xytmp.y >= Y || chess[xytmp.y][xytmp.x]) {
// 超出边界或已被遍历,下次循环
continue;
}
else {
weight.w[weight.len - 1]++;
}
}
chess[xy.y][xy.x] = 0;
Tag--;
}
}
if (flag) {
// 遍历失败,回溯
return false;
}
// 按权值从小到大排序
int min, minIdx, j;
for (i = 0; i < weight.len; i++) {
min = weight.w[i]; minIdx = i;
for (j = i + 1; j < weight.len; j++) {
if (min < weight.w[j]) { min = weight.w[j]; minIdx = j; }
}
xybox tmp = weight.xy[i];
int tmp2 = weight.w[i];
weight.xy[i] = weight.xy[minIdx];
weight.w[i] = weight.w[minIdx];
weight.xy[minIdx] = tmp;
weight.w[minIdx] = tmp2;
}
// 依次遍历(贪心算法)
flag = 0;
for (i = 0; i < weight.len; i++) {
chess[weight.xy[i].y][weight.xy[i].x] = ++Tag;
if (!traverse(weight.xy[i].x, weight.xy[i].y, data)) {
chess[weight.xy[i].y][weight.xy[i].x] = 0;
Tag--;
flag = 1;
}
}
if (flag) {
// 遍历失败,回溯
return false;
}
return true;
}
int main() {
xybox* data = buildData();
// 这边数组输入 X 和 Y 是反过来的 chess [Y][X]
chess[0][0] = ++Tag;
if (!traverse(0, 0, data)) {
printf("算法失败!\n");
}
free(data);
return 0;
}
利用队列,每次把上一顶点的所有可选下一顶点依次排入队列,然后按照这个队列依次访问,类似树的层级遍历。 这个就像是每深入一层,就把这一深度下的所有顶点都先访问一遍,再往下深入。
最小生成树:将一个有权连通图转变为树,并且要求生成树的权值总和要最小。
普里姆算法从顶点入手寻找最佳路线,对于稠密图有优势,遵循以下规则:
绿色
绿色顶点
)的相关边涂为 紫色
白色顶点
)与多条 紫色边
相连,则只保留权值最小的 紫色边
,其余 紫色边
弃掉紫色边
中权值最小的那条涂为 红色
,与其相连的顶点连入生成树Example:
克鲁斯卡尔算法从边入手寻找最佳路线,对于稀疏图有优势,遵循以下规则:
parent数组
(这里 parent 数组的算法是一个关键)Example:
参考: C 语言数据结构与算法视频教程全集 VisuAlgo - 图形据结构(邻接矩阵,邻接列表,边缘列表)