一、图的介绍
图是一种比较复杂的数据结构,在线性表中数据元素之间仅有线性关系,每个元素只有一个直接前驱和直接后继(元素之间只存在一对一关系),在树形结构中元素之间有着明显的层次关系,每一层的元素只能和下层的多个元素有关系(元素之间存在一对多关系),而在图形结构中,任意两个结点之间都可能有关系(元素之间存在多对多关系)。
二、图的定义和术语
顶点: 图中的数据元素被称为顶点,一般使用V表示图的顶点的有穷非空集合。
弧: 两个顶点之间的关系记作<v,w>,表示能从顶点v到达顶点w,也就是v能到w,但w不一定能到v,我们称v为弧尾或初始点,称w为弧头或终端点。
有向图: 由弧+顶点构成的图叫有向图,也就是顶点之间是单行道。
边: 两个顶点之间的关系记录(v,w),表示既能从v到w,也能从w到v,我们称v和w之间的关系是一条边。
无向图: 由边+顶构成的图有无向图,顶点之间的双行道。
注意: 一般使用G代表图,V顶点的集合,VR代表弧或边的集合,n代表顶点的数目,e代表边或弧的数目,我们不讨论顶点到自己的边或弧。
完全图: 在无向图中,e的取值范围是0 ~ n/2(n-1),如果无向图的边的数量达到最大值这种无向图称称为完全图。
有向完全图: 在有向图中,e的取值范围是0 ~ n(n-1),如果有向图的弧的数量达到最大值这种有向图称称为有向完全图。
稀疏图和稠密图: 如果图中的边和弧很少,e<nlogn 这种图被称为稀疏图,反之称为稠密图。
权和网: 如果图中的顶点到另一个顶点需要代价(距离或耗费),那么在表示边或弧的时候需要附加数据,附加的数据就叫做权,带权的图通常被称为网,这也是互联网的由来。
子图: 假定有两个图G1和G2,如果G1的顶点集合是G2的顶点集合的子集,且G1的边或弧集合是G2的边或弧集合的子集,则称G1是G2的子图。
邻接点: 在无向图中如果有一条边(v,w),则v,w两个顶点互为邻接点,即v和w相邻接,边(v,w)依附于顶点v,w,或者说(v,w)和顶点v、w相关联。
顶点v的度: 在无向图中与顶点v相关联的边的数量。
顶点v的入度和出度: 在有向图中,以顶点v作为弧头的弧的数量称为顶点的入度,以顶点v作为弧尾的弧的数量称为顶点的出度。
路径: 从顶点v到达顶点w所经历的顶点序叫做路径,路径的长度就是边或弧的数目。
回路或环: 起始点和终点相同的路径称为回路或环。
简单路径: 路径中顶点不重复出现的路径称为简单路径。
简单回路或简单环: 起始点和终点相同且其余顶点不重复出现,被称为简单回路或简单环。
连通图: 在无向图中,从顶点v到顶点w有路径,则称v和w是连通的,如果图中任意两个顶点都是连通的,则称图为连通图。
连通分量: G1和G2都是连通图,且G1是G2的子图,则称G1是G2的连通分量或极大连通子图。
强连通图:在有向图中,如果任意一对顶点都双向连通,则称图为强连通图。
强连通分量:G1和G2都是强连通图,且G1是G2的子图,则称G1是G2的强连通分量或极大强连通子图。
三、图的存储结构:
邻接矩阵:
使用两个数组来存储数据元素(顶点)和数据元素之间的关系(边或弧)的信息。
一维数组:用于存储顶点。
二维数组:用于存储弧或边。
邻接矩阵的优点:
访问速度快,方便计算结点的度、入度、出度。
邻接矩阵的缺点:
顶点的容量有限,扩容非常麻烦,只适合存储稠密图,存储稀疏图时会有大量的内存浪费。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 无向图
typedef struct Grahh
{
size_t cal; // 顶点容量
size_t cnt; // 顶点数量
char *vertex; // 存储顶点的一维数组
char **edge; // 存储边的二维数组
} Graph;
Graph *create_graph(size_t cal)
{
Graph *graph = malloc(sizeof(Graph));
graph->vertex = malloc(sizeof(char) * cal);
graph->cal = cal;
graph->cnt = 0;
graph->edge = malloc(sizeof(char *) * cal);
for (int i = 0; i < cal; i++)
{
graph->edge[i] = calloc(sizeof(char), cal);
}
return graph;
}
bool add_vertex_graph(Graph *graph, char vertex)
{
if (graph->cnt >= graph->cal)
return false;
for (int i = 0; i < graph->cnt; i++)
{
if (graph->vertex[i] == vertex)
return false;
}
graph->vertex[graph->cnt++] = vertex;
return true;
}
bool add_edge_graph(Graph *graph, char v1, char v2)
{
int v1_index = -1, v2_index = -1;
for (int i = 0; i < graph->cnt; i++)
{
if (graph->vertex[i] == v1)
v1_index = i;
if (graph->vertex[i] == v2)
v2_index = i;
}
if (-1 == v1_index || -1 == v2_index || v1_index == v2_index)
return false;
graph->edge[v1_index][v2_index] = 1;
graph->edge[v2_index][v1_index] = 1;
return true;
}
// 查询顶点在一维数组中的位置,如果不存在则返回-1
int query_vertex_graph(Graph *graph, char vertex)
{
for (int i = 0; i < graph->cnt; i++)
{
if (vertex == graph->vertex[i])
return i;
}
return -1;
}
// 查询顶点第一个邻接点,如果不存在则返回'\0'
char first_vertex_graph(Graph *graph, char vertex)
{
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return '\0';
for (int i = 0; i < graph->cnt; i++)
{
if (1 == graph->edge[index][i])
return graph->vertex[i];
}
return '\0';
}
// 删除顶点
bool del_vertex_graph(Graph *graph, char vertex)
{
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return false;
for (int i = 0; i < graph->cnt; i++)
{
graph->edge[index][i] = 0;
graph->edge[i][index] = 0;
}
return true;
}
void _dfs_graph(Graph *graph, int index, bool *flags)
{
if (!flags[index])
return;
printf("%c\n", graph->vertex[index]);
flags[index] = false;
for (int i = 0; i < graph->cnt; i++)
{
if (1 == graph->edge[index][i])
_dfs_graph(graph, i, flags);
}
}
// 深度优先遍历,与树的前序遍历类似
void dfs_graph(Graph *graph)
{
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
for (int i = 0; i < graph->cnt; i++)
_dfs_graph(graph, i, flags);
}
// 广度优先遍历,与树的层序遍历一样,需要队列结构配合
void bfs_graph(Graph *graph)
{
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
int queue[graph->cnt], front = 0, rear = 0;
for (int i = 0; i < graph->cnt; i++)
{
if (flags[i])
{
flags[i] = false;
queue[rear++] = i;
}
while (front != rear)
{
int index = queue[front++];
printf("%c ", graph->vertex[index]);
for (int j = 0; j < graph->cnt; j++)
{
if (1 == graph->edge[index][j] && flags[j])
{
flags[j] = false;
queue[rear++] = j;
}
}
}
}
}
// 销毁图
void destroy_graph(Graph *graph)
{
for (int i = 0; i < graph->cnt; i++)
{
free(graph->edge[i]);
}
free(graph->edge);
free(graph->vertex);
free(graph);
}
int main(int argc, const char *argv[])
{
Graph *graph = create_graph(5);
add_vertex_graph(graph, 'A');
add_vertex_graph(graph, 'B');
add_vertex_graph(graph, 'C');
add_vertex_graph(graph, 'D');
add_vertex_graph(graph, 'E');
add_edge_graph(graph, 'A', 'C');
add_edge_graph(graph, 'A', 'D');
add_edge_graph(graph, 'C', 'B');
add_edge_graph(graph, 'B', 'D');
// del_vertex_graph(graph,'C');
printf("------------------------\n");
for (int i = 0; i < graph->cnt; i++)
{
for (int j = 0; j < graph->cnt; j++)
{
printf("%hhd ", graph->edge[i][j]);
}
printf("\n");
}
// destroy_graph(graph);
// printf("%c\n",first_vertex_graph(graph,'D'));
// dfs_graph(graph);
bfs_graph(graph);
return 0;
}
邻接表:
采用一维数组加单向链表的方式存储存储。
一维数组:用于存储顶点和单向链表的头指针。
单向链表:存储着从该顶点出发的弧或边。
邻接表优点:
与邻接矩阵相比,它可以节约内存,有多少条边或弧就创建多少个链表节点,比较适合存储稀疏图。
邻接表缺点:
计算入度、删除节点时比较麻烦。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 弧
typedef struct Bow
{
int dest;
struct Bow *next;
} Bow;
// 顶点
typedef struct Vertex
{
char data;
Bow first; // 单链表的空白头节点
} Vertex;
// 有向图
typedef struct Graph
{
Vertex *vertex;
size_t cal;
size_t cnt;
} Graph;
// 创建图
Graph *create_graph(size_t cal)
{
Graph *graph = malloc(sizeof(Graph));
graph->vertex = malloc(sizeof(Vertex) * cal);
graph->cal = cal;
graph->cnt = 0;
return graph;
}
// 添加顶点
bool add_vertex_graph(Graph *graph, char vertex)
{
if (graph->cnt >= graph->cal)
return false;
// 顶点不能重复
for (int i = 0; i < graph->cnt; i++)
if (vertex == graph->vertex[i].data)
return false;
graph->vertex[graph->cnt].data = vertex;
graph->vertex[graph->cnt++].first.next = NULL;
return true;
}
// 查询顶点
int query_vertex_graph(Graph *graph, char vertex)
{
for (int i = 0; i < graph->cnt; i++)
{
if (graph->vertex[i].data == vertex)
return i;
}
return -1;
}
// 添加弧
bool add_bow_graph(Graph *graph, char bow_tail, char bow_head)
{
int index_tail = query_vertex_graph(graph, bow_tail);
if (-1 == index_tail)
return false;
int index_head = query_vertex_graph(graph, bow_head);
if (-1 == index_head)
return false;
Bow *bow = malloc(sizeof(Bow));
bow->dest = index_head;
bow->next = graph->vertex[index_tail].first.next;
graph->vertex[index_tail].first.next = bow;
return true;
}
// 删除顶点
bool del_vertex_graph(Graph *graph, char vertex)
{
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return false;
if (NULL != graph->vertex[index].first.next)
{
Bow *bow = graph->vertex[index].first.next;
graph->vertex[index].first.next = bow->next;
free(bow);
}
graph->vertex[index].data = '\0';
for (int i = 0; i < graph->cnt; i++)
{
for (Bow *bow = &graph->vertex[i].first; NULL != bow->next; bow = bow->next)
{
if (index == bow->next->dest)
{
Bow *tmp = bow->next;
bow->next = tmp->next;
free(tmp);
}
}
}
return true;
}
// 计算入度,计算出有多个弧的弧头是某顶点
int indegree_vertex_graph(Graph *graph, char vertex)
{
// 打到顶点,遍历所有弧,dest等于vertex下的弧有多少条,如果找不到则返回-1
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return -1;
int indegree = 0;
for (int i = 0; i < graph->cnt; i++)
{
Bow *bow = graph->vertex[i].first.next;
while (NULL != bow)
{
if (index == bow->dest)
indegree++;
bow = bow->next;
}
}
return indegree;
}
// 计算出度
int outdegree_vertex_graph(Graph *graph, char vertex)
{
// 找到顶点,从它出发的弧有多少条,如果找不到则返回-1
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return -1;
int outdegree = 0;
Bow *bow = graph->vertex[index].first.next;
while (NULL != bow)
{
outdegree++;
bow = bow->next;
}
return outdegree;
}
// 深度优先遍历
void _dfs_graph(Graph *graph, int index, bool *flags)
{
flags[index] = false;
printf("%c ", graph->vertex[index].data);
Bow *bow = graph->vertex[index].first.next;
while (NULL != bow)
{
if (flags[bow->dest])
_dfs_graph(graph, bow->dest, flags);
bow = bow->next;
}
}
void dfs_graph(Graph *graph)
{
// 参考昨天邻接矩阵的业务逻辑
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
for (int i = 0; i < graph->cnt; i++)
if (flags[i])
_dfs_graph(graph, i, flags);
printf("\n");
}
// 广度优先遍历
void bfs_graph(Graph *graph)
{
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
int queue[graph->cnt], front = 0, rear = 0;
for (int i = 0; i < graph->cnt; i++)
{
if (flags[i])
{
flags[i] = false;
queue[rear++] = i;
}
while (front != rear)
{
int index = queue[front++];
printf("%c ", graph->vertex[index].data);
Bow *bow = graph->vertex[index].first.next;
while (NULL != bow)
{
if (flags[bow->dest])
{
flags[bow->dest] = false;
queue[rear++] = bow->dest;
}
bow = bow->next;
}
}
}
printf("\n");
}
// 销毁图
void destroy_graph(Graph *graph)
{
for (int i = 0; i < graph->cnt; i++)
{
Bow *bow = &graph->vertex[i].first;
while (NULL != bow->next)
{
Bow *tmp = bow->next;
bow->next = tmp->next;
free(tmp);
}
}
free(graph->vertex);
free(graph);
}
int main(int argc, const char *argv[])
{
Graph *graph = create_graph(10);
add_vertex_graph(graph, 'A');
add_vertex_graph(graph, 'B');
add_vertex_graph(graph, 'C');
add_vertex_graph(graph, 'D');
add_vertex_graph(graph, 'E');
add_bow_graph(graph, 'A', 'D');
add_bow_graph(graph, 'A', 'E');
add_bow_graph(graph, 'A', 'C');
add_bow_graph(graph, 'C', 'B');
add_bow_graph(graph, 'B', 'D');
// add_bow_graph(graph,'E','D');
/*
del_vertex_graph(graph,'E');
for(int i=0; i<graph->cnt; i++)
{
printf("%d %c first->%p",i,graph->vertex[i].data,graph->vertex[i].first.next);
for(Bow* bow=graph->vertex[i].first.next; NULL!=bow; bow=bow->next)
{
printf(" %p dest:%d ",bow,bow->dest);
}
printf("\n");
}
printf("%d\n",outdegree_vertex_graph(graph,'B'));
printf("%d\n",indegree_vertex_graph(graph,'A'));
*/
dfs_graph(graph);
bfs_graph(graph);
destroy_graph(graph);
return 0;
}
十字链表:
采用一维数组加两个十字链表进行存储,之所以叫十字链表,是因为它的链表弧节点会被两个链表指向,同时弧节点有两个指针,指向后续的弧节点。
一维数组:存储了顶点数据,两个链表头指针。
出度链表:指向的是以该结点作为弧尾的弧节点。
入度链表:指向的是以该结点作为弧头的弧节点。
十字链表的优点:
与邻接矩阵相比,它更节约内存,与邻接表相比它更方便计算顶点的出度。
十字链表的缺点:
删除顶点和删除弧时比较麻烦,也可以表示与实现无向图,但更合适表示与实现有向图。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 弧
typedef struct Bow
{
int tailvex; // 弧尾的顶点下标
int headvex; // 弧头的顶点下标
struct Bow *tlink; // 指向相同弧尾的弧节点
struct Bow *hlink; // 指向相同弧头的弧节点
} Bow;
// 顶点
typedef struct Vertex
{
char data;
Bow *inlink;
Bow *outlink;
} Vertex;
// 图
typedef struct Graph
{
Vertex *vertex;
size_t cal;
size_t cnt;
} Graph;
// 创建图
Graph *create_graph(size_t cal)
{
Graph *graph = malloc(sizeof(Graph));
graph->vertex = malloc(sizeof(Vertex) * cal);
graph->cal = cal;
graph->cnt = 0;
return graph;
}
// 查询顶点下标
int query_vertex_graph(Graph *graph, char vertex)
{
for (int i = 0; i < graph->cnt; i++)
if (vertex == graph->vertex[i].data)
return i;
return -1;
}
// 添加顶点
bool add_vertex_graph(Graph *graph, char vertex)
{
// 如果顶点已经满,或顶点已经存在则返回false
if (graph->cnt >= graph->cal || -1 != query_vertex_graph(graph, vertex))
return false;
graph->vertex[graph->cnt].data = vertex;
graph->vertex[graph->cnt].inlink = NULL;
graph->vertex[graph->cnt++].outlink = NULL;
return true;
}
// 添加弧
bool add_bow_graph(Graph *graph, char tailvex, char headvex)
{
// 如果弧头或弧尾的顶点不存在则添加失败
int tailindex = query_vertex_graph(graph, tailvex);
int headindex = query_vertex_graph(graph, headvex);
if (-1 == tailindex || -1 == headindex)
return false;
// 创建一个弧
Bow *bow = malloc(sizeof(Bow));
bow->tailvex = tailindex;
bow->headvex = headindex;
bow->tlink = graph->vertex[tailindex].outlink;
graph->vertex[tailindex].outlink = bow;
bow->hlink = graph->vertex[headindex].inlink;
graph->vertex[headindex].inlink = bow;
return true;
}
// 删除顶点
bool del_vertex_graph(Graph *graph, char vertex)
{
// 找到顶点的下标,如果找不到则返回false
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return false;
// 以它作为弧尾的弧要全部断开
graph->vertex[index].inlink = NULL;
// 以它作为弧头的弧要全部删除
for (int i = 0; i < graph->cnt; i++)
{
Bow *out = graph->vertex[i].outlink;
while (NULL != out && NULL != out->tlink)
{
if (out->tlink->headvex == index)
{
Bow *tmp = out->tlink;
out->tlink = tmp->tlink;
free(tmp);
}
out = out->tlink;
}
if (NULL != graph->vertex[i].outlink &&
index == graph->vertex[i].outlink->headvex)
{
Bow *tmp = graph->vertex[i].outlink;
graph->vertex[i].outlink = tmp->tlink;
free(tmp);
}
}
return true;
}
// 删除弧
bool del_bow_graph(Graph *graph, char tailvex, char headvex)
{
// 找到弧头、弧尾顶点的下标,如果有一个找不到就返回false
int tailindex = query_vertex_graph(graph, tailvex);
int headindex = query_vertex_graph(graph, headvex);
if (-1 == tailindex || -1 == headindex)
return false;
// 删除弧,并且要保证两个链表不断开
Bow *out = graph->vertex[tailindex].outlink;
while (NULL != out && NULL != out->tlink)
{
if (out->tlink->headvex == headindex)
{
out->tlink = out->tlink->tlink;
break;
}
out = out->tlink;
}
if (NULL != graph->vertex[tailindex].outlink &&
headindex == graph->vertex[tailindex].outlink->headvex)
{
graph->vertex[tailindex].outlink = graph->vertex[tailindex].outlink->tlink;
}
Bow *in = graph->vertex[headindex].inlink;
while (NULL != in && NULL != in->hlink)
{
if (in->hlink->tailvex == tailindex)
{
Bow *tmp = in->hlink;
in->hlink = tmp->hlink;
free(tmp);
break;
}
in = in->hlink;
}
if (NULL != graph->vertex[headindex].outlink &&
tailindex == graph->vertex[headindex].outlink->tailvex)
{
Bow *tmp = graph->vertex[headindex].inlink;
graph->vertex[headindex].inlink = tmp->hlink;
free(tmp);
}
return true;
}
// 计算入度
int indegree_vertex_graph(Graph *graph, char vertex)
{
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return -1;
int indegree = 0;
Bow *in = graph->vertex[index].inlink;
while (NULL != in)
{
indegree++;
in = in->hlink;
}
return indegree;
}
// 计算出度
int outdegree_vertex_graph(Graph *graph, char vertex)
{
int index = query_vertex_graph(graph, vertex);
if (-1 == index)
return -1;
int outdegree = 0;
Bow *out = graph->vertex[index].outlink;
while (NULL != out)
{
outdegree++;
out = out->tlink;
}
return outdegree;
}
// 深度优先
void _dfs_graph(Graph *graph, int index, bool *flags)
{
printf("%c ", graph->vertex[index].data);
flags[index] = false;
Bow *out = graph->vertex[index].outlink;
while (NULL != out)
{
if (flags[out->headvex])
_dfs_graph(graph, out->headvex, flags);
out = out->tlink;
}
}
void dfs_graph(Graph *graph)
{
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
for (int i = 0; i < graph->cnt; i++)
if (flags[i])
_dfs_graph(graph, i, flags);
printf("\n");
}
// 广度优先
void bfs_graph(Graph *graph)
{
bool flags[graph->cnt];
for (int i = 0; i < graph->cnt; i++)
flags[i] = true;
int queue[graph->cnt], front = 0, rear = 0;
for (int i = 0; i < graph->cnt; i++)
{
if (flags[i])
{
queue[rear++] = i;
flags[i] = false;
}
while (front != rear)
{
int index = queue[front++];
printf("%c ", graph->vertex[index].data);
Bow *out = graph->vertex[index].outlink;
while (NULL != out)
{
if (flags[out->headvex])
{
queue[rear++] = out->headvex;
flags[out->headvex] = false;
}
out = out->tlink;
}
}
}
printf("\n");
}
// 销毁图
void destroy_graph(Graph *graph)
{
for (int i = 0; i < graph->cnt; i++)
del_vertex_graph(graph, graph->vertex[i].data);
free(graph->vertex);
free(graph);
}
int main(int argc, const char *argv[])
{
Graph *graph = create_graph(10);
add_vertex_graph(graph, 'A');
add_vertex_graph(graph, 'B');
add_vertex_graph(graph, 'C');
add_vertex_graph(graph, 'D');
add_vertex_graph(graph, 'E');
add_bow_graph(graph, 'A', 'D');
add_bow_graph(graph, 'A', 'E');
add_bow_graph(graph, 'A', 'C');
add_bow_graph(graph, 'C', 'B');
add_bow_graph(graph, 'B', 'D');
add_bow_graph(graph, 'E', 'D');
dfs_graph(graph);
bfs_graph(graph);
destroy_graph(graph);
// del_vertex_graph(graph,'D');
// del_bow_graph(graph,'A','D');
/*
printf("%d\n",indegree_vertex_graph(graph,'B'));
for(int i=0; i<graph->cnt; i++)
{
printf("vex:%c",graph->vertex[i].data);
printf("\n\t");
Bow* out = graph->vertex[i].outlink;
while(NULL != out)
{
printf("%d->%d ",out->tailvex,out->headvex);
out = out->tlink;
}
printf("\n\t");
Bow* in= graph->vertex[i].inlink;
while(NULL != in)
{
printf("%d<-%d ",in->headvex,in->tailvex);
in = in->hlink;
}
printf("\n");
}
*/
return 0;
}
邻接多重表:
是对邻接表一种扩展,在邻接表边的基础上,增加了访问标记、组成边的两个顶点的下标、指向依附这两个顶点的下一条边的指针。
使用邻接表时,它的一条边的两个节点,分别在第i和第j的两个链表中,这给图的操作带来不便,而邻接多重表的链表节点增加各项成员,方便对图的操作。