3。数据结构(3)
嵌入式软件开发第三部分,各类常用的数据结构及扩展,良好的数据结构选择是保证程序稳定运行的关键,(1)部分包括数组,链表,栈,队列。(2)部分包括树,堆。(3)部分包括散列表,图。
七 散列表(哈希表)
散列表(Hash Table)是一种根据关键字直接访问存储位置的数据结构,它能够实现快速查找、插入和删除操作。散列表通过把关键字映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫作散列函数(Hash Function),存放记录的数组叫作散列表(Hash Table)。
散列表在计算机领域有着广泛的应用,下面介绍其中一些具体的应用:
数据库索引:数据库中的索引通常使用散列表实现。每个索引项包含一个键值和一个指向对应数据项的指针。当需要查找某个数据项时,首先从索引中找到对应的键值,然后通过指针快速访问该数据项。
缓存系统:缓存系统通常使用散列表来存储缓存数据。当需要访问某个数据时,先在散列表中查找对应的缓存项,如果存在则直接返回结果。如果不存在则从原始数据源中获取数据,并将其存储到散列表中。
字典:字典通常使用散列表来存储单词和对应的解释。当需要查找某个单词的解释时,可以通过散列表中的键值快速找到对应的解释。
文件系统:部分文件系统(如NTFS文件系统)使用散列表来加速文件查找。文件系统中的散列表保存了文件名和对应的磁盘块号,通过散列表可以快速定位到指定文件的存储位置。
哈希加密:密码学中的哈希函数被广泛应用于密码加密和校验。哈希函数把任意长度的输入消息通过运算转换成固定长度的输出值,这个输出值可以作为消息的数字指纹,能够验证消息的完整性和真实性。
总之,散列表作为一种快速、高效的数据结构,被广泛应用于各种场景中,如数据库索引、缓存系统、字典、文件系统等。
散列表的核心思想是通过哈希函数将关键字映射到散列表地址上,实现快速查找。具体来说,散列表的操作可以分为以下几个步骤:
-
哈希函数的设计:哈希函数用来将任意长度的输入(例如电话号码)映射为固定长度的输出(例如散列表的地址)。好的哈希函数能够尽可能地把输入均匀地映射到各个散列表地址上,避免散列冲突(即多个关键字映射到同一个散列表地址上)。
-
散列冲突的处理:由于哈希函数的输出值有限,不同的关键字可能会被映射到同一个散列表地址上。因此,需要在散列表中采用一些方法来处理散列冲突。常见的解决冲突的方法包括:链地址法、开放地址法、再哈希法等。
-
再哈希法:设定若干个哈希函数,如果冲突了就用备用函数计算。多个哈希函数计算出来的还是同一个地址的概率就很低了
开放地址法:当发生冲突时,形成一个探查序列,从冲突的那个地址开始,沿着序列逐个探查,常用的探测再散列有线性再散列和二次再散列。以二次再散列来说,加如地址5冲突,则依次探测5+11=6,5+22=9。。。。。挺好理解的
链地址法:链地址就是在数组上插入链表,发生冲突后就插在链表头上。所以不管有多少冲突都没有问题
-
添加元素:将(键,值)对存储到散列表中,需要先通过哈希函数计算出该键对应的散列表地址,然后把值存放在该地址上。
-
查找元素:查找指定键对应的值时,需要用哈希函数计算出该键对应的散列表地址,然后在该地址上查找相应的值。如果存在散列冲突,则按照冲突处理方法进行查找。
散列表的时间复杂度主要取决于哈希函数的设计和散列冲突的处理方式。通常情况下,在哈希函数均匀分布的情况下,散列表的查找、插入和删除的时间复杂度平均为 O(1)。因此,散列表是一种高效的数据结构,被广泛应用于各种场景中,如数据库索引、缓存系统、字典等。
基于链地址法构建散列表(利用链表)
1)向散列表中添加元素
// 向散列表中添加元素
void insert(HashTable* hashtable, char* key, int value) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 判断该位置上是否已经有节点ListNode* node = hashtable->data[pos];while (node != NULL && strcmp(node->key, key) != 0) {node = node->next;}// 如果该位置上没有节点,则创建一个新节点并插入到链表头部if (node == NULL) {ListNode* new_node = new_node(key, value);new_node->next = hashtable->data[pos];hashtable->data[pos] = new_node;hashtable->size++;}// 如果该位置上已经有节点,则更新该节点的值else {node->value = value;}
}
2)在散列表中查找元素
// 在散列表中查找元素,如果找到则返回其对应的值,否则返回-1
int search(HashTable* hashtable, char* key) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 遍历链表查找该元素ListNode* node = hashtable->data[pos];while (node != NULL && strcmp(node->key, key) != 0) {node = node->next;}// 找到了该元素,返回其对应的值if (node != NULL) {return node->value;}// 没找到该元素,返回-1else {return -1;}
}
3)从散列表中删除元素
// 从散列表中删除元素
void remove_node(HashTable* hashtable, char* key) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 遍历链表查找该元素ListNode* node = hashtable->data[pos];ListNode* prev = NULL;while (node != NULL && strcmp(node->key, key) != 0) {prev = node;node = node->next;}// 处理该位置上的节点if (node != NULL) {// 如果该节点是链表头部,则将下一个节点设置为新的链表头部if (prev == NULL) {hashtable->data[pos] = node->next;}// 否则,将该节点从链表中删除else {prev->next = node->next;}free(node);hashtable->size--;}
}
全部程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 最大的散列表大小
#define MAX_SIZE 100// 定义一个结构体保存散列表节点的信息
typedef struct {char* key; // 关键字int value; // 存储的值
} ListNode;// 定义散列表的结构体
typedef struct {ListNode* data[MAX_SIZE]; // 散列表的数据int size; // 散列表的大小
} HashTable;// 哈希函数,将字符串映射成散列表地址
int hash(char* key) {int hash_code = 0;for (int i = 0; i < strlen(key); i++) {hash_code = hash_code * 31 + key[i];}return hash_code % MAX_SIZE;
}// 创建新的节点并返回指针
ListNode* new_node(char* key, int value) {ListNode* node = (ListNode*)malloc(sizeof(ListNode));node->key = (char*)malloc(sizeof(char) * strlen(key));strcpy(node->key, key);node->value = value;return node;
}// 向散列表中添加元素
void insert(HashTable* hashtable, char* key, int value) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 判断该位置上是否已经有节点ListNode* node = hashtable->data[pos];while (node != NULL && strcmp(node->key, key) != 0) {node = node->next;}// 如果该位置上没有节点,则创建一个新节点并插入到链表头部if (node == NULL) {ListNode* new_node = new_node(key, value);new_node->next = hashtable->data[pos];hashtable->data[pos] = new_node;hashtable->size++;}// 如果该位置上已经有节点,则更新该节点的值else {node->value = value;}
}// 在散列表中查找元素,如果找到则返回其对应的值,否则返回-1
int search(HashTable* hashtable, char* key) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 遍历链表查找该元素ListNode* node = hashtable->data[pos];while (node != NULL && strcmp(node->key, key) != 0) {node = node->next;}// 找到了该元素,返回其对应的值if (node != NULL) {return node->value;}// 没找到该元素,返回-1else {return -1;}
}// 从散列表中删除元素
void remove_node(HashTable* hashtable, char* key) {// 计算关键字的哈希值,并定位到该位置int pos = hash(key);// 遍历链表查找该元素ListNode* node = hashtable->data[pos];ListNode* prev = NULL;while (node != NULL && strcmp(node->key, key) != 0) {prev = node;node = node->next;}// 处理该位置上的节点if (node != NULL) {// 如果该节点是链表头部,则将下一个节点设置为新的链表头部if (prev == NULL) {hashtable->data[pos] = node->next;}// 否则,将该节点从链表中删除else {prev->next = node->next;}free(node);hashtable->size--;}
}// 打印散列表中的内容
void print_hashtable(HashTable* hashtable) {printf("Size of hashtable: %d\n", hashtable->size);for (int i = 0; i < MAX_SIZE; i++) {ListNode* node = hashtable->data[i];if (node != NULL) {printf("Pos: %d\n", i);while (node != NULL) {printf("%s : %d\n", node->key, node->value);node = node->next;}}}
}int main() {// 创建一个新的散列表HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));hashtable->size = 0;for (int i = 0; i < MAX_SIZE; i++) {hashtable->data[i] = NULL;}// 向散列表中添加元素insert(hashtable, "apple", 10);insert(hashtable, "banana", 20);insert(hashtable, "orange", 30);insert(hashtable, "grape", 40);// 在散列表中查找元素int value = search(hashtable, "banana");printf("The value of 'banana' is %d\n", value);// 从散列表中删除元素remove_node(hashtable, "grape");// 打印散列表中的内容print_hashtable(hashtable);// 释放散列表所占用的内存for (int i = 0; i < MAX_SIZE; i++) {ListNode* node = hashtable->data[i];while (node != NULL) {ListNode* temp = node;node = node->next;free(temp->key);free(temp);}}free(hashtable);return 0;
}
八 图
图是一种非线性的数据结构,它由节点(顶点)和边组成。节点之间的连线称为边,它们可以用来表示各种复杂的现实世界中的关系,例如社交网络、电路等。
以下是图的基本概念:
顶点:也称为节点,表示图中的一个元素。
边:表示两个顶点之间的关系。
有向图:如果边有方向,则称该图为有向图。
无向图:如果边没有方向,则称该图为无向图。
权:表示边的重要程度或代价。
路径:是通过一系列边连接在一起的顶点序列,用来描述从一个顶点到另一个顶点的路线。
环:是指至少含有一个顶点的路径,其中第一个顶点与最后一个顶点是相同的。
连通:如果图中每两个顶点之间都存在至少一条路径,则称该图为连通图。
强连通:如果图是有向图,且对于任意两个顶点 u 和 v,都存在一条从 u 到 v 和一条从 v 到 u 的路径,则称该图为强连通图。
图的类型
图有两种类型:有向图和无向图。在有向图中,边是有方向的,表示从一个顶点到另一个顶点有一个单向的路径;而在无向图中,边是没有方向的,表示两个顶点之间的路径是双向的。
常用的图的存储结构有以下几种:
邻接矩阵:使用一个二维数组来表示图中顶点之间的关系,其中数组的行和列分别表示图中的顶点,用 0 或 1 来表示是否存在边。当然,在带权图中,可以用数组元素的值来表示边的权值。
邻接表:使用一个链表数组来表示图中顶点之间的关系,每个链表表示一个顶点的邻接点集。链表中的节点包含两部分信息:邻接点的编号和这条边的权值(如果是带权图)。
关联矩阵:使用一个二维数组来表示图中顶点和边之间的关系,其中数组的行表示顶点,列表示边,用 0 或 1 表示该顶点与该边是否相关。当然,在带权图中,可以用数组元素的值来表示边的权值。
常用的图算法有以下几种:
深度优先搜索(DFS):从一个起点开始,依次遍历所有与之相邻的顶点,并不断深入直到无法继续为止。在进行搜索的过程中,需要使用一个栈来记录已经访问的顶点。
广度优先搜索(BFS):从一个起点开始,先访问其所有相邻的顶点,然后再访问这些相邻顶点的相邻顶点,依次类推。在进行搜索的过程中,需要使用一个队列来记录已经访问的顶点。
最短路径算法:用于找出图中两个顶点之间的最短路径,其中常用的算法包括 Dijkstra 算法和 Floyd-Warshall 算法。
最小生成树算法:用于找出图中的一棵生成树,使得该生成树的所有边权值之和最小。其中常用的算法包括 Prim 算法和 Kruskal 算法。
基于邻接表存储结构的深度优先搜索(DFS)
在下面的代码中,我们使用邻接表存储图,并采用递归的方式实现深度优先搜索。具体来说,我们使用一个 visited 数组来记录每个顶点是否被访问过,然后从某个未被访问的顶点开始,依次遍历其所有邻接点,并对每个未被访问的邻接点递归调用 DFS 函数。在 DFS 函数中,我们首先将当前顶点标记为已经被访问过,然后输出当前顶点的信息,并遍历它的所有邻接点。如果某个邻接点还没有被访问过,就递归调用 DFS 函数。最后,在主函数中,我们通过 InsertVertex 和 InsertArc 函数构造了一个简单的图,并对它进行深度优先遍历。
#define MAX_VERTEX_NUM 100 // 定义最大顶点数
#define FALSE 0
#define TRUE 1// 邻接表中每个链表结点的定义
typedef struct ArcNode {int adjvex; // 邻接点的编号struct ArcNode *nextarc; // 指向下一个邻接点的指针
} ArcNode;// 邻接表中每个头结点的定义
typedef struct VNode {char data; // 顶点的数据域ArcNode *firstarc; // 指向第一个邻接点的指针
} VNode, AdjList[MAX_VERTEX_NUM];// 图的定义
typedef struct {AdjList vertices; // 邻接表int vexnum, arcnum; // 顶点数和边数
} ALGraph;int visited[MAX_VERTEX_NUM]; // 记录每个顶点是否被访问过// 初始化图
void InitGraph(ALGraph *G) {G->vexnum = G->arcnum = 0;for (int i = 0; i < MAX_VERTEX_NUM; i++) {G->vertices[i].data = '\0';G->vertices[i].firstarc = NULL;}
}// 插入一个顶点,返回该顶点在邻接表中的位置
int InsertVertex(ALGraph *G, char v) {if (G->vexnum >= MAX_VERTEX_NUM) {printf("Error: Too many vertices\n");return -1;} else {G->vertices[G->vexnum].data = v;G->vertices[G->vexnum].firstarc = NULL;return G->vexnum++;}
}// 插入一条边 (vi, vj)
void InsertArc(ALGraph *G, int vi, int vj) {ArcNode *p = (ArcNode*)malloc(sizeof(ArcNode)); // 分配一个新的链表结点p->adjvex = vj;p->nextarc = G->vertices[vi].firstarc; // 将新结点插入到链表的头部G->vertices[vi].firstarc = p;G->arcnum++;
}// 深度优先搜索算法
void DFS(ALGraph *G, int v) {visited[v] = TRUE; // 标记当前顶点已被访问printf("%c ", G->vertices[v].data);ArcNode *p = G->vertices[v].firstarc;while (p != NULL) { // 遍历 v 的所有邻接点if (!visited[p->adjvex]) {DFS(G, p->adjvex); // 对每个未被访问的邻接点递归调用 DFS}p = p->nextarc;}
}// 对图进行深度优先遍历
void DFSTraverse(ALGraph *G) {memset(visited, FALSE, sizeof(visited)); // 初始化 visited 数组for (int i = 0; i < G->vexnum; i++) {if (!visited[i]) {DFS(G, i); // 对每个未被访问的顶点进行 DFS}}
}int main() {ALGraph G;InitGraph(&G);int v1 = InsertVertex(&G, 'A');int v2 = InsertVertex(&G, 'B');int v3 = InsertVertex(&G, 'C');int v4 = InsertVertex(&G, 'D');InsertArc(&G, v1, v2);InsertArc(&G, v2, v3);InsertArc(&G, v3, v4);InsertArc(&G, v4, v1);DFSTraverse(&G);return 0;
}
基于邻接表存储结构的广度优先搜索(BFS)(基于队列)
在下面的代码中,我们使用邻接表存储图,并采用队列的方式实现广度优先搜索。具体来说,我们使用一个 visited 数组来记录每个顶点是否被访问过,然后从某个未被访问的顶点开始,依次遍历其所有邻接点,并将每个未被访问的邻接点加入队列中。在队列非空的情况下,依次取出队首元素,并遍历它的所有邻接点,将未访问的邻接点加入队列中。直到队列为空为止。
在 BFS 函数中,我们首先初始化队列,将起始顶点加入队列中,并标记其已被访问。然后在队列非空的情况下,依次取出队首元素 u,并输出该顶点的信息。接着,我们遍历 u 的所有邻接点,如果某个邻接点 v 还没有被访问过,就将它加入队列,并标记 visited[v] 为 1。
在 BFSTraverse 函数中,我们对图中每个未访问的顶点分别调用 BFS 函数即可。
总之,广度优先搜索算法是一种基于队列的搜索方式,可以在无权图中找到从起始顶点到其他所有顶点的最短路径。
#include <stdio.h>
#include <stdlib.h>#define MAX_VERTEX_NUM 100 // 定义最大顶点数// 邻接表中每个链表结点的定义
typedef struct ArcNode {int adjvex; // 邻接点的编号struct ArcNode *nextarc; // 指向下一个邻接点的指针
} ArcNode;// 邻接表中每个头结点的定义
typedef struct VNode {char data; // 顶点的数据域ArcNode *firstarc; // 指向第一个邻接点的指针
} VNode, AdjList[MAX_VERTEX_NUM];// 图的定义
typedef struct {AdjList vertices; // 邻接表int vexnum, arcnum; // 顶点数和边数
} ALGraph;int visited[MAX_VERTEX_NUM]; // 记录每个顶点是否被访问过// 初始化图
void InitGraph(ALGraph *G) {G->vexnum = G->arcnum = 0;for (int i = 0; i < MAX_VERTEX_NUM; i++) {G->vertices[i].data = '\0';G->vertices[i].firstarc = NULL;}
}// 插入一个顶点,返回该顶点在邻接表中的位置
int InsertVertex(ALGraph *G, char v) {if (G->vexnum >= MAX_VERTEX_NUM) {printf("Error: Too many vertices\n");return -1;} else {G->vertices[G->vexnum].data = v;G->vertices[G->vexnum].firstarc = NULL;return G->vexnum++;}
}// 插入一条边 (vi, vj)
void InsertArc(ALGraph *G, int vi, int vj) {ArcNode *p = (ArcNode*)malloc(sizeof(ArcNode)); // 分配一个新的链表结点p->adjvex = vj;p->nextarc = G->vertices[vi].firstarc; // 将新结点插入到链表的头部G->vertices[vi].firstarc = p;G->arcnum++;
}// 广度优先搜索算法
void BFS(ALGraph *G, int v) {// 初始化队列int head = 0, tail = 0;int queue[MAX_VERTEX_NUM];queue[tail++] = v;visited[v] = 1;while (head != tail) { // 队列非空时int u = queue[head++]; // 取出队首元素printf("%c ", G->vertices[u].data);ArcNode *p = G->vertices[u].firstarc;while (p != NULL) { // 遍历 u 的所有邻接点if (!visited[p->adjvex]) {queue[tail++] = p->adjvex; // 将未访问的邻接点入队visited[p->adjvex] = 1; // 标记邻接点已被访问过}p = p->nextarc;}}
}// 对图进行广度优先遍历
void BFSTraverse(ALGraph *G) {memset(visited, 0, sizeof(visited)); // 初始化 visited 数组for (int i = 0; i < G->vexnum; i++) {if (!visited[i]) {BFS(G, i); // 对每个未被访问的顶点进行 BFS}}
}int main() {ALGraph G;InitGraph(&G);int v1 = InsertVertex(&G, 'A');int v2 = InsertVertex(&G, 'B');int v3 = InsertVertex(&G, 'C');int v4 = InsertVertex(&G, 'D');InsertArc(&G, v1, v2);InsertArc(&G, v1, v4);InsertArc(&G, v2, v3);InsertArc(&G, v3, v4);BFSTraverse(&G);return 0;
}
基于邻接矩阵存储结构的 Dijkstra 算法(计算最短路径)
在下面的代码中,我们使用邻接矩阵存储图,并采用 Dijkstra 算法计算单源最短路径。具体来说,我们需要维护三个数组:dist 数组、path 数组和 visited 数组。其中,dist[i] 表示从起始点到顶点 i 的最短距离,path[i] 表示从起始点到顶点 i 的最短路径上的前驱结点,visited[i] 记录每个顶点是否已确定最短路径。
Dijkstra 算法的基本思想是,将所有顶点分成两个集合:已确定最短路径的顶点集合 S 和未确定最短路径的顶点集合 T(初始时,S 只包括起始点)。然后每次从 T 中找出距离起始点最近的一个顶点 u,并将 u 加入 S 集合中,标记为已确定最短路径。然后用 u 松弛其邻接点,更新其它顶点的最短路径长度和前驱结点。依次进行 n 次这样的过程(n 为顶点数),直到所有顶点的最短路径都已确定。
在 Dijkstra 函数中,我们首先初始化 dist 数组和 path 数组,将起始点到各顶点的距离初始化为边的权值,前驱结点初始化为起始点(如果有边相连)。然后将起始点加入 S 集合中,标记为已确定最短路径。接着,在 T 集合中找出距离起始点最近的那个顶点 min_v,将其加入 S 集合中,标记为已确定最短路径。然后用 min_v 松弛其邻接点,更新其他顶点的最短路径长度和前驱结点。重复上述步骤,直到所有顶点的最短路径都已确定。
在 PrintPath 函数中,我们采用递归的方式输出从起始点到终点的最短路径。首先判断是否已到达起始点,如果是则直接输出;否则递归输出终点的前驱结点,最后再输出终点。
总之,Dijkstra 算法是一种贪心算法,能够求解单源最短路径问题。它的时间复杂度为 O(n^2),比较适用于稠密图。
#include <stdio.h>
#include <limits.h>#define MAX_VERTEX_NUM 100 // 定义最大顶点数// 邻接矩阵的定义
typedef struct {int edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 存储边的权值int vexnum, arcnum; // 顶点数和边数
} MGraph;// 初始化图
void InitGraph(MGraph *G) {G->vexnum = G->arcnum = 0;for (int i = 0; i < MAX_VERTEX_NUM; i++) {for (int j = 0; j < MAX_VERTEX_NUM; j++) {G->edges[i][j] = INT_MAX; // 初始化为无穷大}}
}// 添加一条边 (vi, vj),权值为 w
void AddArc(MGraph *G, int vi, int vj, int w) {G->edges[vi][vj] = w;G->arcnum++;
}// Dijkstra 算法求单源最短路径
void Dijkstra(MGraph *G, int start, int dist[], int path[]) {int visited[MAX_VERTEX_NUM] = {0}; // 记录每个顶点是否已确定最短路径for (int i = 0; i < G->vexnum; i++) {dist[i] = G->edges[start][i]; // 初始化 dist 数组为起始点到各顶点的距离if (G->edges[start][i] < INT_MAX) {path[i] = start; // 如果起始点到该顶点有边,则将其前驱为起始点} else {path[i] = -1; // 如果起始点到该顶点没有边,则将其前驱置为 -1}}dist[start] = 0; // 起始点到自身距离为 0visited[start] = 1; // 起始点已确定最短路径for (int i = 1; i < G->vexnum; i++) { // 其余 n-1 个顶点依次进入 S 集合int min_dist = INT_MAX;int min_v = -1;for (int j = 0; j < G->vexnum; j++) { // 找出当前未确定最短路径的顶点中距离最小的那个if (!visited[j] && dist[j] < min_dist) {min_dist = dist[j];min_v = j;}}if (min_v == -1) break; // 如果找不到这样的顶点,说明剩下的顶点都不连通visited[min_v] = 1; // 将该顶点加入 S 集合中,标记为已确定最短路径for (int j = 0; j < G->vexnum; j++) { // 更新其他顶点的最短路径if (!visited[j] && G->edges[min_v][j] < INT_MAX && dist[min_v] + G->edges[min_v][j] < dist[j]) {dist[j] = dist[min_v] + G->edges[min_v][j]; // 通过 min_v 松弛 jpath[j] = min_v; // 更新 j 的前驱为 min_v}}}
}// 输出从起始点到终点的最短路径
void PrintPath(MGraph *G, int start, int end, int path[]) {if (end == start) { // 如果已到达起始点,直接输出printf("%d ", start);return;}if (path[end] == -1) { // 如果终点没有前驱,说明不连通printf("Error: Not connected\n");return;}PrintPath(G, start, path[end], path); // 递归输出前驱printf("%d ", end);
}int main() {MGraph G;InitGraph(&G);int v0 = 0, v1 = 1, v2 = 2, v3 = 3, v4 = 4;AddArc(&G, v0, v1, 10);AddArc(&G, v0, v3, 30);AddArc(&G, v0, v4, 100);AddArc(&G, v1, v2, 50);AddArc(&G, v2, v4, 10);AddArc(&G, v3, v2, 20);AddArc(&G, v3, v4, 60);int dist[MAX_VERTEX_NUM] = {0}; // 存储起始点到各顶点的最短距离int path[MAX_VERTEX_NUM] = {-1}; // 存储各顶点到起始点的最短路径上的前驱结点Dijkstra(&G, v0, dist, path);for (int i = 0; i < G.vexnum; i++) {printf("%d ", dist[i]);}printf("\n");PrintPath(&G, v0, v4, path);return 0;
}
基于邻接矩阵存储结构的 Prim 算法(最小生成树)
在下面的代码中,我们使用邻接矩阵存储图,并采用 Prim 算法计算最小生成树。具体来说,我们需要维护两个数组:dist 数组和 path 数组。其中,dist[i] 表示顶点 i 到最小生成树的最短距离,path[i] 表示顶点 i 到最小生成树上的前驱结点。visited 数组用于记录每个顶点是否已加入生成树。
Prim 算法的基本思想是,将所有顶点分成两个集合:已加入生成树的顶点集合 S 和未加入生成树的顶点集合 T(初始时,S 只包括起始点)。然后每次从 T 中找出距离 S 最近的一个顶点 u,并将 u 加入 S 集合中。然后用 u 松弛其邻接点,更新 T 中的顶点到生成树的距离和前驱结点。重复上述步骤,直到所有顶点都已加入生成树。
在 Prim 函数中,我们首先初始化 dist 数组和 path 数组,将起始点到各顶点的距离初始化为边的权值,前驱结点初始化为起始点(如果有边相连)。然后将起始点加入生成树中,标记为已访问。接着,在 T 集合中找出距离 S 最近的那个顶点 min_v,将其加入 S 集合中。然后用 min_v 松弛其邻接点,更新其他顶点到生成树的距离和前驱结点。重复上述步骤,直到所有顶点都已加入生成树。
总之,Prim 算法是一种贪心算法,能够求解最小生成树问题。它的时间复杂度为 O(n^2),比较适用于稠密图。
#include <stdio.h>
#include <limits.h>#define MAX_VERTEX_NUM 100 // 定义最大顶点数// 邻接矩阵的定义
typedef struct {int edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 存储边的权值int vexnum, arcnum; // 顶点数和边数
} MGraph;// 初始化图
void InitGraph(MGraph *G) {G->vexnum = G->arcnum = 0;for (int i = 0; i < MAX_VERTEX_NUM; i++) {for (int j = 0; j < MAX_VERTEX_NUM; j++) {G->edges[i][j] = INT_MAX; // 初始化为无穷大}}
}// 添加一条边 (vi, vj),权值为 w
void AddArc(MGraph *G, int vi, int vj, int w) {G->edges[vi][vj] = w;G->edges[vj][vi] = w; // 无向图需要添加两条边G->arcnum++;
}// Prim 算法求最小生成树
void Prim(MGraph *G, int start, int dist[], int path[]) {int visited[MAX_VERTEX_NUM] = {0}; // 记录每个顶点是否已加入生成树for (int i = 0; i < G->vexnum; i++) {dist[i] = G->edges[start][i]; // 初始化 dist 数组为起始点到各顶点的距离if (G->edges[start][i] < INT_MAX) {path[i] = start; // 如果起始点到该顶点有边,则将其前驱为起始点} else {path[i] = -1; // 如果起始点到该顶点没有边,则将其前驱置为 -1}}visited[start] = 1; // 将起始点加入生成树中,标记为已访问for (int i = 1; i < G->vexnum; i++) { // 其余 n-1 个顶点依次加入生成树int min_dist = INT_MAX;int min_v = -1;for (int j = 0; j < G->vexnum; j++) { // 找出当前未加入生成树的顶点中距离最小的那个if (!visited[j] && dist[j] < min_dist) {min_dist = dist[j];min_v = j;}}if (min_v == -1) break; // 如果找不到这样的顶点,说明剩下的顶点都不连通visited[min_v] = 1; // 将该顶点加入生成树中,标记为已访问for (int j = 0; j < G->vexnum; j++) { // 更新其他顶点到生成树的距离if (!visited[j] && G->edges[min_v][j] < dist[j]) {dist[j] = G->edges[min_v][j]; // 通过 min_v 更新 j 的到生成树的距离path[j] = min_v; // 更新 j 的前驱为 min_v}}}
}int main() {MGraph G;InitGraph(&G);int v0 = 0, v1 = 1, v2 = 2, v3 = 3, v4 = 4;AddArc(&G, v0, v1, 10);AddArc(&G, v0, v3, 30);AddArc(&G, v0, v4, 100);AddArc(&G, v1, v2, 50);AddArc(&G, v2, v4, 10);AddArc(&G, v3, v2, 20);AddArc(&G, v3, v4, 60);int dist[MAX_VERTEX_NUM] = {0}; // 存储每个顶点到最小生成树的最短距离int path[MAX_VERTEX_NUM] = {-1}; // 存储每个顶点到最小生成树上的前驱结点Prim(&G, v0, dist, path);for (int i = 0; i < G.vexnum; i++) {printf("%d ", path[i]);}printf("\n");return 0;
}
相关文章:
3。数据结构(3)
嵌入式软件开发第三部分,各类常用的数据结构及扩展,良好的数据结构选择是保证程序稳定运行的关键,(1)部分包括数组,链表,栈,队列。(2)部分包括树,…...
QT停靠窗口QDockWidget类
QT停靠窗口QDockWidget类 QDockWidget类简介函数和方法讲解 QDockWidget类简介 QDockWidget 类提供了一个部件,它可以停靠在 QMainWindow 内或作为桌面上的顶级窗口浮动。 QDockWidget 提供了停靠窗口部件的概念,也称为工具面板或实用程序窗口。 停靠窗…...
【LeetCode】139. 单词拆分
139. 单词拆分(中等) 思路 首先将大问题分解成小问题: 前 i 个字符的子串,能否分解成单词;剩余子串,是否为单个单词; 动态规划的四个步骤: 确定 dp 数组以及下标的含义 dp[i] 表示 s…...
【三维重建】NeRF原理+代码讲解
文章目录 一、技术原理1.概览2.基于神经辐射场(Neural Radiance Field)的体素渲染算法3.体素渲染算法4.位置信息编码(Positional encoding)5.多层级体素采样 二、代码讲解1.数据读入2.创建nerf1.计算焦距focal与其他设置2.get_emb…...
IntelliJ IDEA 社区版2021.3配置SpringBoot项目详细教程及错误解决方法
目录 一、SpringBoot的定义 二、Spring Boot 优点 三、创建一个springboot的项目 四、使用IDEA创建SpringBoot失败案例 一、SpringBoot的定义 Spring 的诞⽣是为了简化 Java 程序的开发的,⽽ Spring Boot 的诞⽣是为了简化 Spring 程序开发的。 Spring Boot 翻…...
Qt中QDebug的使用
QDebug类为调试信息(debugging information)提供输出流。它的声明在<QDebug>中,实现在Core模块中。将调试或跟踪信息(debugging or tracing information)写出到device, file, string or console时都会使用QDebug。 此类的成员函数参考:https://doc…...
vue使用路由的query配置项时如何清除地址栏的参数
写vue项目时,如果想通过路由的query配置项把参数从一个组件传到另一个组件,但是又不希望?idxxx显示在地址栏(如:http://localhost:8080/test?idxxx的?idxxx),该怎么做: 举一个案例࿱…...
Redis-列表(List)
Redis列表(List) 介绍 单键多值Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)它的底层实际是个双向链表,对两端的操作性能很高,通过索…...
ripro主题修改教程-首页搜索框美化教程
先看效果图: 我们来看怎么实现: 1、找到wp-content/themes/ripro/assets/css/diy.css并将下面的内容整体复制进去并保存 /*首页搜索框*/ .bgcolor-fff {background-color: #fff; } .row,.navbar .menu-item-mega>.sub-menu{margin-left:-10px;margin-right:-10px;} .home…...
写作业用白光还是暖光?盘点色温4000K的护眼台灯
台灯的白光或者暖光指的是台灯的色温,低色温的光线看起来发黄发红,高色温的光线发白发蓝。 如果灯光的光源是高品质光源,本身没有蓝光问题,那么色温的选择对护眼的影响是比较少的,更多的是对人学习工作状态,…...
Java时间类(一)-- SimpleDateFormat类
目录 1. SimpleDateFormat的构造方法: 时间模式字母: 2. SimpleDateFormat的常用方法: “工欲善其事,必先利其器”。学习时间类之前,需要先学习SimpleDateFormat类。 java.text.SimpleDateFormat类是以与语言环境有关的方式来格式...
07 Kubernetes 网络与服务管理
课件 Kubernetes Service是一个抽象层,用于定义一组Pod的访问方式和访问策略,其作用是将一组Pod封装成一个服务,提供一个稳定的虚拟IP地址和端口号,以便于其他应用程序或服务进行访问。 以下是Kubernetes Service YAML配置文件的…...
并发编程之Atomic原子操作类
基本类型:AtomicInteger、AtomicBoolean、AtomicLong 引用类型:AtomicReference、AtomicMarkableReference、AtomicStampedReference 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 对象属性原子修改器:…...
管家婆辉煌Ⅱ 13.32版安装方法
因管家婆辉煌版已经长期不更新,现已经出现蓝屏的问题,故此新开此贴,慢慢更新安装方法。 首先管家婆下载地址:http://www.grasp.com.cn/download.aspx?id116 先安装sql server 2008 下载后,运行安装,请注…...
常见的接口优化技巧思路
一、背景 针对老项目,去年做了许多降本增效的事情,其中发现最多的就是接口耗时过长的问题,就集中搞了一次接口性能优化。本文将给小伙伴们分享一下接口优化的通用方案。 二、接口优化方案总结 1.批处理 批量思想:批量操作数据…...
【Java EE】-使用Fiddler抓包以及HTTP的报文格式
作者:学Java的冬瓜 博客主页:☀冬瓜的主页🌙 专栏:【JavaEE】 分享: 在满园弥漫的沉静的光芒之前,一个人更容易看到时间,并看到自己的身影。——史铁生《我与地坛》 主要内容:使用FIddler抓包的…...
Java异步编程
Java异步编程 1、什么是java异步编程2、异步编程有什么作用3、异步编程常用于哪些业务4、异步编程的方式5、Async异步调用Async简介 1、什么是java异步编程 Java异步编程是一种处理并发问题的技术,它可以在执行耗时操作的同时,不阻塞主线程,…...
C++类与对象(二)——构造函数与析构函数
文章目录 一.类的默认6个成员函数二.构造函数1.引例2.构造函数的概念及特性 三.析构函数😋析构函数的特性 前言: 上篇文章初步认识了类以及类的相关知识,本篇将继续深入学习类与对象——类的默认6个成员函数: 一.类的默认6个成员函…...
c++标准模板(STL)(std::array)(四)
定义于头文件 <array> template< class T, std::size_t N > struct array;(C11 起) std::array 是封装固定大小数组的容器。 此容器是一个聚合类型,其语义等同于保有一个 C 风格数组 T[N] 作为其唯一非静态数据成员的结构体。不同于 C 风格数…...
vue3计算属性
计算属性 模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。推荐使用计算属性来描述依赖响应式状态的复杂逻辑 基础示例 不够好的示例 模板中使用了表达式,不够直观&…...
Java 中的访问修饰符有哪些(九)
Java 中的访问修饰符用于限制类、接口、字段和方法的访问范围,它们分别表示不同的访问控制级别。Java 中共有四种访问修饰符:public、protected、default 和 private。 public public 是最开放的访问修饰符,用于指定公共访问级别。被 publi…...
HR员工管理的三重境界:管事、管人、管心
在一个公司里,员工来来往往是常态,虽说我们不能替他们决定,但是一定是与公司的管理者有一定的关系。马云曾经说过:“一个员工离职,不外乎两种原因,一是钱没给到位;二是心里委屈了”。一句话就是…...
延迟队列与SpringBoot实战
延迟队列与SpringBoot实战 概念 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列 …...
【算法】九键输入法
题目: 输入数字字符串, 输出这串字符对应的九键输入法有可能出现的所有情况 算法: 定义了一个全局变量 g_numStr,其中存储了每个数字对应的字母。定义了一个递归函数 str_combine,用于将每个数字对应的字母进行组合。str_combin…...
jvm之类加载器
写在前面 当我们通过javac命令将java源代码编译为Java字节码后,必须通过类加载器将其加载到jvm中才能运行,所以类加载器是jvm中非常重要的一个组成部分,本文我们就一起来看下吧! 1:类的生命周期 类的生命周期如下图…...
Chapter4:频率响应法(上)
第四章:频率响应法 Exercise4.1 已知微分网络和积分网络电路图如下图所示,求网络的频率特性。 解: 【图 ( a ) ({\rm a}) (a)微分网络】 由微分网络电路图可得:...
【6. 激光雷达接入ROS】
欢迎大家阅读2345VOR的博客【6. 激光雷达接入ROS】🥳🥳🥳 2345VOR鹏鹏主页: 已获得CSDN《嵌入式领域优质创作者》称号👻👻👻,座右铭:脚踏实地,仰望星空&#…...
Java 基础进阶篇(三)—— 面向对象的三大特征之二:继承
文章目录 一、继承概述二、内存运行原理 ★三、继承的特点四、继承后:成员变量和方法的访问特点五、继承后:方法重写六、继承后:子类构造器的特点七、继承后:子类构造器访问父类有参构造器八、this、super 总结 一、继承概述 Jav…...
[angstromctf 2023] 部分
这个比赛打了个开头就放弃了,最近放弃的比较多,国外的网太慢,国内的题太难。 Crypto ranch 这题直接给出密文这提示 rtkw{cf0bj_czbv_nvcc_y4mv_kf_kip_re0kyvi_uivjj1ex_5vw89s3r44901831} Caesar dressing is so 44 BC... 然后是加密程序…...
死信队列
死信队列 死信的概念 先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息…...
行业网站怎么推广/留号码的广告网站不需要验证码
假期 # 生活 # 水文 咱们继续假期第三天的日常更文,没看上篇的铁子们我把地址贴在下面。 点我 虽然是假期,但我规划已久的睡懒觉流程却是一直执行不下去。这不今天早上八点我就起床了,当然起的早不是为了“卷”,而是吃早餐。说出…...
龙岗网站推广/永久免费制作网页
我这个菜鸟真的弱爆了~~弱爆了~~以前竟然在写着超级垃圾的快速幂,彻底服了自己~~无语~~实在无语 program1(弱智快速幂取模) 图1:猴子爬下去: 图2:猴子很老实滴爬回来 #include<iostream> using namespace std; //求2^N%20…...
做网购的有哪几个网站/东莞seo管理
用JQuery Validate框架,在IE8下验证报错问题解决参考文章: (1)用JQuery Validate框架,在IE8下验证报错问题解决 (2)https://www.cnblogs.com/destimarve/p/5511257.html 备忘一下。...
网络营销方式举个例子/温州seo公司
下面介绍无监督机器学习算法,与前面分类回归不一样的是,这个不知道目标变量是什么,这个问题解决的是我们从这些样本中,我们能发现什么。 这下面主要讲述了聚类算法,跟数据挖掘中的关联挖掘中的两个主要算法。 K均值算法…...
网站开发一般流程/做网站建设的公司
LVS是Linux Virtual Server的简写,意即Linux虚拟服务器,是一个虚拟的服务器集群系统。本项目在1998年5月由章文嵩博士成立,是中国国内最早出现的自由软件项目之一。负载均衡集群是 load balance 集群的简写,翻译成中文就是负载均衡…...
使用flash做网站/世界比分榜
7转载于:https://www.cnblogs.com/wuguangzong/p/10925016.html...