图及其相关应用:
1邻接矩阵:
邻接矩阵(adjacency matrix)是一种表示有向图或无向图的方式。邻接矩阵是一个二维数组,其中行和列分别对应于图中的各个节点,而每个元素表示相应节点之间是否有边相连。对于无向图,邻接矩阵是对称的;对于有向图,邻接矩阵则不一定对称。例如,对于一个无向图,有以下邻接矩阵:
上述邻接矩阵表示图中有4个节点,它们的编号分别为A、B、C和D。矩阵中的第i行第j列的值为1,表示节点i与节点j之间有一条边相连;如果值为0,则表示它们之间没有边。
邻接矩阵是一种常用的图的表示方法,它能够提供以下的优点:
方便查找:由于邻接矩阵是一个表格,因此可以很方便地查找图中某两个节点之间是否有边相连。
快速遍历:邻接矩阵存储的信息可以被快速地转换为图的遍历序列,例如深度优先搜索(DFS)和广度优先搜索(BFS)。
空间效率:当图边比较稠密时,邻接矩阵比邻接表(adjacency list)需要更少的存储空间。
代码:
#include<stdio.h>
#include<stdlib.h>
#define MAXINT 32767
#define MVNum 100
#define OK 1
#define ERROR 0
typedef int Status;
typedef char VerTextType;//定义顶点数据类型全为char型
typedef int ArcType;//定义边的权值为整型
typedef struct{
VerTextType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum, arcnum;//顶点数和路径数
}AMGraph;
int LocateVex(AMGraph G, VerTextType u){
//存在则返回u在顶点表中的下标,否则返回-1
for(int i = 0;i<G.vexnum; i++)
{
if(u==G.vexs[i]) return i;
}
return -1;
}
//采用邻接矩阵表示法,创建无线网(UDN算法)
Status CreatUDN(AMGraph &G){
VerTextType v1, v2;//定义顶点
ArcType w;//定义权重(只能为0或1)
printf("请输入顶点数和边数: \n");
scanf("%d %d",&G.vexnum,&G.arcnum);//输入邻接矩阵的顶点数和路径数
getchar();
printf("请输入顶点: \n");
for(int i = 0; i < G.vexnum; i++){
scanf("%c", &G.vexs[i]);
} //off for i.
getchar();
for(int i = 0;i < G.vexnum; i++){
for(int j = 0; j<G.vexnum; j++){
G.arcs[i][j] = MAXINT;//初始化矩阵中的每一个位置为最大值
}//off for j.
}//off for i.
printf("请输入边的两个顶点及权重: \n");
for(int k = 0; k<G.arcnum; k++){
scanf("%c %c %d",&v1, &v2, &w);//输入一条边的顶点和权重.
getchar();
int i = LocateVex(G,v1);
int j = LocateVex(G,v2);//确定v1,v2在邻接矩阵中的位置
G.arcs[i][j] = w;
G.arcs[j][i]=G.arcs[i][j];
}
return OK;
}//off creatUDN
void printGraph(AMGraph G){
int i, j;
for(i = 0;i<G.vexnum; i++)
{
for(j = 0; j<G.vexnum;j++)
{
printf("%-5d ",G.arcs[i][j]);
if(j==G.vexnum-1) printf("\t\n");
}
}
}
//Entrance
int main(){
AMGraph G;
CreatUDN(G);
printGraph(G);
return 0;
}
然而,邻接矩阵也具有以下缺点:
空间复杂度高:当图边比较稀疏时,邻接矩阵会占用很多空间,因为大部分元素都是0。
动态修改复杂:如果在邻接矩阵表示的图中添加或删除节点,或者增加或删除边,则需要重新分配矩阵的存储空间,这可能会导致时间复杂度较高。
2邻接表:
邻接表(adjacency list)是一种表示有向图或无向图的方式,通常由一组链表来表示节点及其邻居。每个链表对应一个节点,存储与该节点相邻的所有节点。具体来说,在无向图中,每个节点在邻接表中对应一个链表,链表中的每个节点表示与该节点相邻的一个节点;在有向图中,每个节点在邻接表中对应两个链表,一个表示从该节点出发的有向边,另一个表示指向该节点的有向边。例如,对于一个无向图,有以下邻接表形式:
上述邻接表表示图中有4个节点,它们的编号分别为A、B、C和D。邻接表中的每一行表示一个节点,以该节点为起点的邻居节点列表是该节点对应的链表。
邻接表是一种常用的图的表示方法,它能够提供以下的优点:
空间效率:当图边比较稀疏时,邻接表比邻接矩阵更加节省存储空间,因为只需要存储邻居节点列表。
允许动态修改:如果在邻接表表示的图中添加或删除节点,或者增加或删除边,只需要修改对应的链表即可,不需要重新分配存储空间。
方便遍历:由于邻接表中存储的是邻居节点列表,因此可以很方便地遍历图的各个节点。
代码:
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
/*data:数据域,存储顶点信息
firstarc:指针域,指向边表中的第一个节点
adjvex:邻接点域
nextarc:指针域,指向边表的下一结点
info:存储和边或狐有关的信息,如权值
vex:顶点
arc:边*/
#define OK 1
#define ERROR 0
#define MVnum 100
typedef char VexTextType;//定义顶点数据类型全为char型
typedef int InfoType;//定义其他信息为int型,如权值等
typedef int Status;
//定义邻接表
//定义边表
typedef struct ArcNode{
int adjvex;//该边所指的顶点位置
struct ArcNode *nextarc;//指向下一条边的指针
InfoType info;//该边的相关信息
}ArcNode;
//定义顶点表
typedef struct VNode{
VexTextType data;//顶点信息
ArcNode *firstarc;//指向第一条依附该顶点的边的指针
}VNode, AdjList[MVnum];//AdjList表示邻接表类型
//图
typedef struct{
AdjList vertices;//邻接表 vertices[i] = VNode[i].data;
int vexnum, arcnum;//顶点数和边数
}ALGraph;
//Locate
int Locate(ALGraph G, VexTextType u){
//存在则返回u在顶点表中的下标,否则返回-1
for(int i = 0;i<G.vexnum; i++)
{
if(u==G.vertices[i].data)
return i;
}
return -1;
}
//图邻接表存储建立算法(UDG)
Status CreatUDG(ALGraph &G){
int v1, v2;
printf("请输入顶点个数和边数: ");
scanf("%d %d",&G.vexnum, &G.arcnum);
printf("请输入顶点: ");
for(int i = 0;i<G.vexnum;i++){ //输入各点,构造表头结点表
scanf("%d",&G.vertices[i].data); //输入顶点值
G.vertices[i].firstarc = NULL;//初始化表头结点指针域为空
}
for(int k = 0;k<G.arcnum;k++){
printf("请输入一对连接点: ");
scanf("%d %d",&v1,&v2);
int i = Locate(G,v1);
int j = Locate(G,v2);
ArcNode *p1 = (ArcNode *)malloc(sizeof(ArcNode));
p1->adjvex = j;//邻接点序号为j
p1->nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p1;//将新节点p1插入顶点vi的边表头部
ArcNode *p2 = (ArcNode *)malloc(sizeof(ArcNode));
p2->adjvex = i;//邻接点序号为i
p2->nextarc = G.vertices[j].firstarc;
G.vertices[j].firstarc = p2;//将新节点p2插入顶点vj的边表头部
}
return OK;
}
void printGraph(ALGraph G){
ArcNode *p;
for(int i = 0;i<G.vexnum;i++){
printf("%d->",G.vertices[i].data);
p = G.vertices[i].firstarc;
while(p){
printf("%d->",G.vertices[p->adjvex].data);
p = p->nextarc;
}
if (p==NULL)
printf("NULL\n");
}
}
int main(){
ALGraph G;
CreatUDG(G);
printGraph(G);
return 0;
}
然而,邻接表也具有以下缺点:
访问逐个遍历:如果需要查找两个节点之间是否有边相连,需要线性遍历其中一个节点的邻居列表。
节点编号限制:邻接表中节点的编号通常需要连续且从小到大排列,否则需要使用哈希表来存储对应关系,增加了实现的复杂度。
图的遍历:
1深度优先遍历:
图的深度优先遍历(Depth First Search,DFS)是一种遍历图的算法,其基本思想是从起点开始,递归访问与当前节点相邻的所有未访问过的节点,直到无法继续访问为止,然后回溯到上一个节点,继续遍历。
具体实现过程如下:
代码:
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#define MVNum 100
#define OK 1
#define ERROR 0
typedef char VerTexType;
typedef struct{
VerTexType vexs[MVNum];//顶点表
int vexnum,arcnum;//顶点数
int arcs[MVNum][MVNum];
}AMGraph;//邻接矩阵
int visited[MVNum];//已遍历过的节点以1存储在此数组中
void DFS(AMGraph G, int v){
printf("%c ",G.vexs[v]);
visited[v] = 1;
for(int i = 0;i<G.vexnum; i++){
printf("%d ",visited[i]);
}
printf("\n");
for(int w = 0; w<G.vexnum; w++){
if((G.arcs[v][w] != 0)&&(!visited[w]))
DFS(G,w);
}
}
void DFSTraver(AMGraph G){
for(int i = 0; i<G.vexnum; i++){
visited[i] = 0;
}
for(int i = 0; i<G.vexnum; i++){
if(visited[i] == 0){
DFS(G,i);//递归查找
}
}
}
int LocateVex(AMGraph G, VerTexType u){
//存在则返回u在顶点表中的下标,否则返回-1
for(int i = 0;i<G.vexnum; i++)
{
if(u==G.vexs[i]) return i;
}
return -1;
}
void creat(AMGraph &G)
{
char v1,v2;
scanf("%d %d",&G.vexnum,&G.arcnum);
getchar();
for(int i=0;i<G.vexnum;i++)
scanf("%c",&G.vexs[i]);
getchar();
for(int i=0;i<G.vexnum;i++)
for(int j=0;j<G.vexnum;j++)
G.arcs[i][j]=0;
for(int k=0;k<G.arcnum;k++){
scanf("%c %c", &v1, &v2);
getchar();
int i = LocateVex(G,v1);
int j = LocateVex(G,v2);//确定v1,v2在邻接矩阵中的位置
G.arcs[i][j] = 1;
G.arcs[j][i]=G.arcs[i][j];
}
}
int main(){
AMGraph G;
creat(G);
DFSTraver(G);
return 0;
}
从起点开始遍历,将其标记为已经访问过。
对于当前节点的所有未被访问过的邻居节点,递归遍历。
当前节点的所有邻居节点都被访问过,或者当前节点无邻居节点时,返回上一个节点,继续遍历。
重复步骤2和3,直到遍历完所有节点。
可以使用栈(Stack)来实现深度优先遍历,从起点开始进行深度优先搜索,并在栈中记录“回溯点”。每次遍历到一个未被访问过的邻居节点时,将其入栈,等到该节点遍历完后再从栈中弹出回到上一个未完成遍历的节点进行下一步的遍历,直到所有节点都被遍历过。
深度优先遍历的时间复杂度为O(V+E),其中V表示节点数,E表示边数。
2广度优先遍历:
图的广度优先遍历(Breadth First Search,BFS)是一种用于遍历图的算法,其基本思想是从起点开始,按照“层次”依次访问与当前节点相邻的所有未访问的节点,然后进入下一层访问,直到遍历完所有节点。
具体实现过程如下:
代码:
#define MVNum 100 //最大顶点数
typedef struct{ //图的结构体定义
char vexs[MVNum]; //存放顶点的一维数组
int arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的顶点数和边数
}Graph;
void BFS(Graph G,int v){ // 按照广度优先非递归遍历连通图G
cout << v;
visited[v] = true; // 访问第v个顶点
InitQueue(Q); // 使用队列辅助遍历,初始化队列Q
EnQueue(Q,v); // v进队
while(!QueueEmpty(Q)){ // 队列非空
DeQueue(Q,u); // 队头元素出队并赋值给u
for(w = FirstAdjVex(G,u);w>=0;w = NextAdjVex(G,u,w)) // 依次找弧
if(!visited[w]){ // w为u的尚未访问过的邻接顶点
cout << w;
visited[w] = true;
EnQueue(Q,w); // w进队
}
}
}
从起点开始遍历,将其标记为已经访问过,将其加入到一个队列中。
从队列中取出第一个未被访问过的节点,遍历其所有未被访问过的邻居节点,并将这些节点标记为已经访问过,然后将这些邻居节点加入到队列中。
如果队列不为空,则取出队列中的下一个未被访问过的节点,重复步骤2;否则遍历结束。
可以用队列来实现广度优先遍历,从起点开始进行广度遍历,每一轮访问与当前节点相邻的所有未访问过的节点,然后将这些节点加入队列中,在队列中按访问时间的顺序进行下一轮遍历,直到所有节点都被遍历过。
广度优先遍历的时间复杂度为O(V+E),其中V表示节点数,E表示边数。
最短路径:
迪杰斯特拉算法:
迪杰斯特拉算法(Dijkstra's algorithm)是一种用于求解带权图中单源最短路径问题的贪心算法。它的思路是从起点出发,不断更新当前已知的最短路径,直到到达目标节点为止。
具体实现过程如下:
代码:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MAX_VERTICES 100
#define MAX_EDGES 10000
struct Edge {
int v, w;
};
int dist[MAX_VERTICES];
int visited[MAX_VERTICES];
struct Edge edges[MAX_EDGES][MAX_VERTICES];
int numEdges[MAX_VERTICES];
void dijkstra(int n, int s) {
for (int i = 0; i < n; i++) {
dist[i] = INT_MAX;
visited[i] = 0;
}
dist[s] = 0;
for (int i = 0; i < n; i++) {
int u = -1;
for (int j = 0; j < n; j++) {
if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
u = j;
}
}
visited[u] = 1;
for (int j = 0; j < numEdges[u]; j++) {
int v = edges[u][j].v, w = edges[u][j].w;
if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
}
}
}
}
int main() {
int n, m, s;
printf("Enter the number of vertices: ");
scanf("%d", &n);
printf("Enter the number of edges: ");
scanf("%d", &m);
printf("Enter the source vertex: ");
scanf("%d", &s);
printf("Enter the edges and weights:\n");
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
edges[u][numEdges[u]++] = (struct Edge){v, w};
}
dijkstra(n, s);
printf("Vertex Distance from Source\n");
for (int i = 0; i < n; i++) {
printf("%d %d\n", i, dist[i]);
}
return 0;
}
创建一个集合S,存放已经确定了最短路径的节点;
初始化距离dis数组,将起点到每个节点的距离都设置为正无穷,并将起点到自己的距离设为0;
对于起点相邻的每个节点,更新dis数组中的距离值,并将这些节点加入一个优先队列Q,按照距离值大小排序;
从优先队列Q中取出距离最小的节点u,将其加入集合S中,并将与节点u相邻的,且未加入S集合的节点的距离值更新,并重新排序优先队列;
重复步骤4,直到目标节点被加入集合S为止。在这个过程中,每个节点只被访问一次,所以时间复杂度为O(V*logV),其中V表示节点数。
迪杰斯特拉算法的优化包括:
使用堆优化:使用最小堆来存储距离最小的节点,这样可以在插入和删除节点时保持堆的有序,减少排序次数,时间复杂度为O(E*logV),其中E表示边数。
使用双向Dijkstra算法:这种算法从起点和终点同时开始搜索,然后在两个方向的搜索中相遇,从而得到最短路径。它可以更快地找到最短路径,但需要更多空间存储每个节点的最短距离。
拓扑排序:
拓扑排序是一种对有向无环图进行排序的算法,它将图中的所有顶点按照一定的顺序排列,使得对于每条有向边 (u,v),都有 u 在排列中出现在 v 的前面。
代码:
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
int indegree[MAX_VERTICES];
int adj[MAX_VERTICES][MAX_VERTICES];
int n;
void topologicalSort() {
int count = 0;
int queue[MAX_VERTICES];
for (int i = 0; i < n; i++) {
if (indegree[i] == 0) {
queue[count++] = i;
}
}
while (count > 0) {
int u = queue[--count];
printf("%d ", u);
for (int v = 0; v < n; v++) {
if (adj[u][v]) {
indegree[v]--;
if (indegree[v] == 0) {
queue[count++] = v;
}
}
}
}
}
int main() {
int m;
printf("Enter the number of vertices: ");
scanf("%d", &n);
printf("Enter the number of edges: ");
scanf("%d", &m);
printf("Enter the edges:\n");
for (int i = 0; i < m; i++) {
int u, v;
scanf("%d %d", &u, &v);
adj[u][v] = 1;
indegree[v]++;
}
printf("Topological order: ");
topologicalSort();
return 0;
}
首先定义了一个常量MAX_VERTICES表示图中最大的顶点数。
然后定义了一个一维数组indegree表示每个顶点的入度,一个二维数组adj表示图的邻接矩阵,一个整数n表示顶点的数量。
定义了函数topologicalSort实现了拓扑排序,首先将入度为0的顶点加入队列中,然后依次取出队列中的顶点,并将其相邻的顶点的入度减1,如果减1后入度为0,则将其加入队列中。最后输出排序结果。
在main函数中读入图的信息,调用topologicalSort函数进行拓扑排序。需要注意的是,在输入边的时候需要将邻接矩阵中相应的元素设为1,并将终点的入度加1。