本文所有代码均在仓库中,这是一个完整的由纯C语言实现的可以存储任意类型元素的数据结构的工程项目。
最后也是最重要的一点,数据结构的通用性和舒适的体验感,下面以平衡二叉树为例:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
int dataCompare(void *, void *);
typedef struct People {
char *name;
int age;
} *People;
int main(int argc, char **argv) {
struct People dataList[] = {
{"张三", 15},
{"李四", 3},
{"王五", 7},
{"赵六", 10},
{"田七", 9},
{"周八", 8},
};
BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
for (int i = 0; i < 6; ++i) {
balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
}
return 0;
}
/**
* 根据人的年龄比较
*/
int dataCompare(void *data1, void *data2) {
int sub = ((People) data1)->age - ((People) data2)->age;
if (sub > 0) {
return 1;
} else if (sub < 0) {
return -1;
} else {
return 0;
}
}
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
int dataCompare(void *, void *);
void dataPrint(void *);
typedef struct People {
char *name;
int age;
} *People;
int main(int argc, char **argv) {
struct People dataList[] = {
{"张三", 15},
{"李四", 3},
{"王五", 7},
{"赵六", 10},
{"田七", 9},
{"周八", 8},
};
BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
for (int i = 0; i < 6; ++i) {
balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
balancedBinaryTreePrint(tree, dataPrint);
printf("-------------\n");
}
return 0;
}
/**
* 根据人的年龄比较
*/
int dataCompare(void *data1, void *data2) {
int sub = ((People) data1)->age - ((People) data2)->age;
if (sub > 0) {
return 1;
} else if (sub < 0) {
return -1;
} else {
return 0;
}
}
/**
* 打印人的年龄
* @param data
*/
void dataPrint(void *data) {
People people = (People) data;
printf("%d", people->age);
}
打印的结果如下:
最后期待大佬们的点赞。
图 G G G由顶点集 V V V和边集 E E E组成,记为:
G = ( V , E ) G=(V,E) G=(V,E)
若 V = { v 1 , v 2 , … , v n } V=\{v_1,v_2,\dots,v_n\} V={v1,v2,…,vn}则用 ∣ V ∣ |V| ∣V∣表示图 G G G中顶点的个数,也称为图 G G G的阶;若 E = { ( a , b ) ∣ a ∈ V , b ∈ V } E=\{(a,b)|a\in V,b\in V\} E={(a,b)∣a∈V,b∈V}则用 ∣ E ∣ |E| ∣E∣表示图 G G G中边的条数。不存在空图,即一个图的点集是非空的。图的分类如下:
图有关的术语如下:
顶点的度、入度和出度:
路径、路径长度和回路:
简单路径和简单回路:
路径长度:路径上边的数目称为路径长度。
距离:两个顶点之间的最短路径长度称为距离,路径不存在则距离为无穷。
连通、连通图和连通分量:
强连通图和强连通分量:
子图和生成子图:
完全图:任意两个顶点之间都有一条边相连的图称为完全图。
连通图的生成树和非连通图的生成森林:
边的权、网和带权路径长度:
树和有向树:
图一般有以下几种存储方式:
邻接矩阵法用一个一维数组存储图中的顶点信息,用一个二维数组存储图中边的信息(即顶点之间的邻接关系),这个二维数组就是邻接矩阵。邻接矩阵法适合存储稠密图。
struct AdjacentMatrixGraph {
void **vertexList;
int **edgeMatrix;
int vertexCount;
int edgeCount;
int size;
int (*compare)(void *, void *);
};
对于图而言:
对于网而言:
邻接表法为每个顶点建立一个单链表,这个链表中的结点表示依附于该顶点的边,这个单链表称为顶点的边表。邻接表法适合存储稀疏图。
typedef struct Edge {
int weight;
int vertexIndex;
struct Edge *next;
} *Edge;
typedef struct Vertex {
void *data;
Edge firstEdge;
} *Vertex, *VertexList;
struct AdjacentListGraph {
VertexList *vertexList;
int vertexCount;
int edgeCount;
int size;
int (*compare)(void *, void *);
};
十字链表法是有向图的一种链式存储结构,一个十字链表可以唯一确定一个图。
typedef struct ArcNode{
ArcType data;
int tailVex; //弧头顶点位置
int headVex; //弧尾顶点位置
int headLink; //弧头相同的下一条弧
int tailLink; //弧尾相同的下一条弧
}ArcNode;
typedef struct VexNode {
VertexType data;
ArcNode * firstIn; //以该顶点为弧头的第一个弧顶点
ArcNode * firstOut; //以该顶点为为弧尾的第一个弧顶点
}VexNode,* VexList;
typedef struct OLGraph{
VexList vexList;
int vexNum;
int arcNum;
}* OLGraph;
邻接多重表是无向图的一种链式存储结构。
typedef struct EdgeNode {
bool mark; //是否被搜搜过
int iVex; //该边依附的一个顶点位置
int jVex; //该边依附的另一个顶点位置
int iLink; //下一个依附顶点iVex的边
int jLink; //下一个依附顶点jVex的边
} EdgeNode;
typedef struct VexNode {
VexType data;
EdgeNode *firstEdge; //第一个依附该顶点的边
} VexNode, *VexList;
typedef struct AMLGraph {
VexList vexList;
int vexNum;
int edgeNum;
} *AMLGraph;
图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次的过程。图的遍历方式主要有以下两种:
void BFS(AdjacentListGraph graph, bool isVisited[], LinkedQueue queue, LinkedQueue BFSDataQueue) {
while (!linkedQueueIsEmpty(queue)) {
Vertex vertex = linkedQueueDeQueue(queue);
linkedQueueEnQueue(BFSDataQueue, vertex->data);
isVisited[getVertexIndex(graph, vertex->data) - 1] = true;
for (Vertex j = firstVertex(graph, vertex->data); j != NULL; j = nextVertex(graph, vertex->data, j->data)) {
if (!isVisited[getVertexIndex(graph, j->data) - 1]) {
linkedQueueEnQueue(queue, j);
}
}
}
}
/**
* 广度优先遍历
* @param graph
* @param BFSDataQueue
*/
void BFSTraverse(AdjacentListGraph graph, void *data, LinkedQueue BFSDataQueue) {
bool *isVisited = calloc(graph->vertexCount, sizeof(bool));
LinkedQueue queue = linkedQueueConstructor();
linkedQueueEnQueue(queue, graph->vertexList[getVertexIndex(graph, data) - 1]);
BFS(graph, isVisited, queue, BFSDataQueue);
for (int i = 1; i <= graph->vertexCount; ++i) {
if (!isVisited[i - 1]) {
BFS(graph, isVisited, queue, BFSDataQueue);
}
}
}
void DFS(AdjacentListGraph graph, int vertexIndex, bool isVisited[], LinkedQueue DFSDataQueue) {
Vertex vertex = graph->vertexList[vertexIndex - 1];
linkedQueueEnQueue(DFSDataQueue, vertex->data);
isVisited[vertexIndex - 1] = true;
for (Vertex j = firstVertex(graph, vertex->data); j != NULL; j = nextVertex(graph, vertex->data, j->data)) {
int index = getVertexIndex(graph, j->data);
if (!isVisited[index - 1]) {
DFS(graph, index, isVisited, DFSDataQueue);
}
}
}
/**
* 深度优先遍历
* @param graph
* @param data
* @param DFSDataQueue
*/
void DFSTraverse(AdjacentListGraph graph, void *data, LinkedQueue DFSDataQueue) {
bool *isVisited = calloc(graph->vertexCount, sizeof(bool));
int index = getVertexIndex(graph, data);
DFS(graph, index, isVisited, DFSDataQueue);
for (int i = 1; i <= graph->vertexCount; ++i) {
if (!isVisited[i - 1]) {
DFS(graph, i, isVisited, DFSDataQueue);
}
}
}
对于一个带权连通无向图 G G G,生成树不同,每棵树的权值也可能不同,若 T T T为权值最小的生成树,则 T T T称为 G G G的最小生成树(MST)。最小生成树的性质如下:
普里姆算法的步骤:
/**
* Prim算法
* @param graph
* @return
*/
AdjacentListGraph Prim(AdjacentListGraph graph) {
AdjacentListGraph MST = adjacentListGraphConstructor(graph->vertexCount, graph->compare);
//是否加入最小生成树
bool isJoin[graph->vertexCount];
//路径长度
int distance[graph->vertexCount];
//路径前驱
int path[graph->vertexCount];
for (int i = 0; i < graph->vertexCount; ++i) {
isJoin[i] = false;
distance[i] = INFINITY;
path[i] = -1;
}
while (MST->size != graph->vertexCount) {
int fromIndex = -1, toIndex = -1, minDistance = 0;
if (MST->vertexCount == 0) {
toIndex = 1;
} else {
for (int i = 1; i <= graph->vertexCount; ++i) {
if (isJoin[i - 1] == false && distance[i - 1] < minDistance) {
fromIndex = path[i - 1];
toIndex = i;
minDistance = distance[i - 1];
}
}
if (toIndex == -1) {
break;
}
}
isJoin[toIndex - 1] = true;
distance[toIndex - 1] = minDistance;
path[toIndex - 1] = fromIndex;
for (Edge edge = graph->vertexList[toIndex - 1]->firstEdge; edge != NULL; edge = edge->next) {
if (edge->weight < distance[edge->vertexIndex - 1]) {
distance[edge->vertexIndex - 1] = edge->weight;
path[edge->vertexIndex - 1] = toIndex;
}
}
insertVertex(MST, graph->vertexList[toIndex - 1]->data);
MST->vertexCount++;
addEdge(MST, graph->vertexList[fromIndex - 1]->data, graph->vertexList[toIndex - 1]->data);
MST->edgeCount++;
fromIndex = -1;
toIndex = -1;
minDistance = 0;
}
return MST;
}
克鲁斯卡尔算法的步骤:
/**
* 克鲁斯卡尔算法
* @param graph
* @return
*/
AdjacentListGraph Kruskal(AdjacentListGraph graph) {
AdjacentListGraph MST = adjacentListGraphConstructor(graph->vertexCount, graph->compare);
PriorityQueue queue = priorityQueueConstructor(graph->compare);
DisjointSet set = disjointSetConstructor(graph->vertexCount, graph->compare);
for (int i = 1; i <= graph->vertexCount; ++i) {
Vertex vertex = graph->vertexList[i - 1];
disjointSetInsert(set, vertex->data);
insertVertex(MST, vertex->data);
for (Edge edge = graph->vertexList[i - 1]->firstEdge; edge != NULL; edge = edge->next) {
int *tuple = calloc(3, sizeof(int));
tuple[0] = i;
tuple[1] = edge->vertexIndex;
tuple[2] = edge->weight;
priorityQueueEnQueue(queue, tuple);
}
}
while (!priorityQueueIsEmpty(queue)) {
int *tuple = priorityQueueDeQueue(queue);
Vertex fromVertex = graph->vertexList[tuple[0] - 1];
Vertex toVertex = graph->vertexList[tuple[1] - 1];
int weight = tuple[2];
if (graph->compare(disjointSetFind(set, fromVertex->data), disjointSetFind(set, toVertex->data)) != 0) {
addEdge(MST, fromVertex->data, toVertex->data);
setWeight(MST, fromVertex->data, toVertex->data, weight);
disjointSetUnion(set, fromVertex, toVertex);
}
}
return MST;
}
在图中,把一个顶点到另一个顶点的一条路径所经过边上的权值 (无权图视为权为 1 1 1)之和称为该路径的带权路径长度,带权路径长度最短的路径称为最短路径。两点之间的最短路径一定包含路径上其它顶点之间的最短路径。其中:
/**
* BFS求无权图最短路径
* @param graph
* @param startVertex
* @param endVertex
* @param stack
*/
void BFSMinPath(AdjacentListGraph graph, void *startVertex, void *endVertex, SequenceStack stack) {
//起始顶点到各个顶点的最短路径
int distance[graph->vertexCount];
//最短路径从哪个顶点过来
int path[graph->vertexCount];
bool isVisited[graph->vertexCount];
for (int i = 0; i < graph->vertexCount; ++i) {
distance[i] = INFINITY;
path[i] = -1;
isVisited[i] = false;
}
int startIndex = getVertexIndex(graph, startVertex);
LinkedQueue queue = linkedQueueConstructor();
distance[startIndex - 1] = 0;
isVisited[startIndex - 1] = true;
linkedQueueEnQueue(queue, startVertex);
while (linkedQueueIsEmpty(queue)) {
Vertex vertex = linkedQueueDeQueue(queue);
for (Vertex near = firstVertex(graph, vertex->data); near != NULL; near = nextVertex(graph, vertex->data, near->data)) {
int nearIndex = getVertexIndex(graph, near->data);
if (!isVisited[nearIndex - 1]) {
distance[nearIndex - 1] = distance[startIndex - 1] + 1;
path[nearIndex - 1] = startIndex;
isVisited[nearIndex - 1] = true;
linkedQueueEnQueue(queue, near);
}
}
}
int minPath = getVertexIndex(graph, endVertex);
while (path[minPath - 1] != -1) {
sequenceStackPush(stack, graph->vertexList[minPath - 1]);
minPath = path[minPath - 1];
}
}
算法思想:
缺陷:不适用于负权值带权图
/**
* 迪杰斯特拉算法
* @param graph
* @param startVertex
* @param endVertex
* @param stack
*/
void Dijkstra(AdjacentListGraph graph, void *startVertex, void *endVertex, SequenceStack stack) {
int findCount = 0;
//标记各顶点是否找到最短路径
bool isFind[graph->vertexCount];
//最短路径长度
int distance[graph->vertexCount];
//路径前驱
int path[graph->vertexCount];
for (int i = 0; i < graph->vertexCount; ++i) {
isFind[i] = false;
distance[i] = INFINITY;
path[i] = -1;
}
while (findCount != graph->vertexCount) {
int fromIndex = -1, toIndex = -1, minDistance = 0;
if (findCount == 0) {
toIndex = getVertexIndex(graph, startVertex);
} else {
for (int i = 1; i <= graph->vertexCount; ++i) {
if (isFind[i - 1] == false && distance[i - 1] < minDistance) {
fromIndex = path[i - 1];
toIndex = i;
minDistance = distance[i - 1];
}
}
if (toIndex == -1) {
break;
}
}
isFind[toIndex - 1] = true;
distance[toIndex - 1] = minDistance;
path[toIndex - 1] = fromIndex;
for (Edge edge = graph->vertexList[toIndex - 1]->firstEdge; edge != NULL; edge = edge->next) {
if (distance[toIndex - 1] + edge->weight < distance[edge->vertexIndex - 1]) {
distance[edge->vertexIndex - 1] = distance[toIndex - 1] + edge->weight;
path[edge->vertexIndex - 1] = toIndex;
}
}
findCount++;
fromIndex = -1;
toIndex = -1;
minDistance = 0;
}
int minPath = getVertexIndex(graph, endVertex);
while (path[minPath - 1] != -1) {
sequenceStackPush(stack, graph->vertexList[minPath - 1]);
minPath = path[minPath - 1];
}
}
若一个有向图中不存在环,则称该图为有向无环图,简称DAG图。若用DAG图表示一个工程的各个子工程及其相互制约的关系:
其中以顶点表示活动,弧表示活动之间的优先制约关系。则将这样的图称为AOV网。AOV网的特点如下:
其中以弧表示活动,顶点表示活动之的开始和结束事件。则将这样的图称为AOE网。AOE网的性质如下:
在AOV网中,我们将全部活动排列成一个线性序列,使得若AOV网中有边AB存在,则在这个序列中,A一定排在B的前面,具有这种性质的线性序列称为拓扑有序序列。即做事的先后顺序。用于求解拓扑序列的算法称为拓扑排序算法,拓扑排序算法可以用于检测图中是否含有环:如果拓扑序列中含有所有图中的结点,那么该图就没有环,否则就含有环。
拓扑排序的算法思想如下:
/**
* 拓扑排序
* @param graph
* @param queue
*/
void topologicalSort(AdjacentListGraph graph, LinkedQueue queue) {
SequenceStack stack = sequenceStackConstructor(graph->vertexCount);
int inDegreeList[graph->vertexCount];
for (int i = 1; i <= graph->vertexCount; ++i) {
inDegreeList[i - 1] = getInDegree(graph, graph->vertexList[i - 1]->data);
if (inDegreeList[i - 1] == 0) {
sequenceStackPush(stack, graph->vertexList[i - 1]);
}
}
while (!sequenceStackIsEmpty(stack)) {
Vertex vertex = sequenceStackPop(stack);
linkedQueueEnQueue(queue, vertex->data);
for (Edge edge = vertex->firstEdge; edge != NULL; edge = edge->next) {
if (--inDegreeList[edge->vertexIndex - 1] == 0) {
sequenceStackPush(stack, graph->vertexList[edge->vertexIndex - 1]);
}
}
}
}
逆拓扑排序的算法思想如下:
void DFS(AdjacentListGraph graph, int index, int isVisited[], LinkedQueue queue) {
isVisited[index - 1] += 2;
Vertex vertex = graph->vertexList[index - 1];
for (Edge edge = vertex->firstEdge; edge != NULL; edge = edge->next) {
if (isVisited[edge->vertexIndex - 1] == 0) {
DFS(graph, edge->vertexIndex, isVisited, queue);
}
if (isVisited[edge->vertexIndex - 1] == 2) {
throw Error(CYCLIC_GRAPH_ERROR, "图中含有环,逆拓扑排序失败");
}
}
isVisited[index - 1]--;
linkedQueueEnQueue(queue, vertex->data);
}
/**
* 深度优先算法求逆拓扑排序
* @param graph
* @param queue
*/
void DFSInTopologicalSort(AdjacentListGraph graph, LinkedQueue queue) {
int *isVisited = calloc(graph->vertexCount, sizeof(bool));
for (int i = 1; i <= graph->vertexCount; ++i) {
if (isVisited[i - 1] == 0) {
DFS(graph, i, isVisited, queue);
}
}
}
在AOE网中,入度为零的顶点称为源点,出度为零的顶点称为汇点,关键路径是指从源点到汇点路径长度最长的路径。关键路径上的活动称为关键活动。完成整个工程最短的时间就是关键路径的长度。