图:
图的定义:
图是一种抽象的数据结构,它由节点(顶点)和连接这些节点的边组成。图可以用来表示各种关系,比如网络中的通信连接、社交网络中的人际关系等。图可以是有向的(边有方向)或无向的(边没有方向),可以是带权重的(边有权重)或不带权重的。在图的表示中,通常使用邻接矩阵或邻接表等数据结构来存储图的信息。
除此之外,图我们一般称元素,我们一般称作顶点。
图的一些基本概念:
无向边:
若两个顶点间的边没有方向,则为无向边,记作(vi,vj),因为没有方向,也可写成(vj,vi),如果图任意两个顶点之间都是无向边,则为无向图。
有向边:
若两个顶点间的边有方向,则为有向边,记<vi,vj>,vi是弧尾,vj是弧头,如果图任意两个顶点之间都是有向边,则为有向图。
注意连通分量的概念:
1.要是子图。
2.子图要连通。
3.连通子图要有极大顶点数。
4.有极大顶点数的连通子图包含依附于这些顶点的所有边。
对于一个图:
如果它有n个顶点:
1.如果边小于n-1条边,则为非连通图。
2.如果多于n-1条边,则必定构成了一个环。
3.如果等于n-1,则不一定为生成树,可能压根不连通。
下面再来看看图的储存结构:
一:邻接矩阵:
图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息,无向图如图所示:
二维数组中值为1,则不存在该边,反之则存在,上图中对角线为0是因为不存在到自身的边或者不存在该边,我们能发现无向图的边数组是一个对称矩阵(满足ai j==aj i)。
对于这样的数组:
1.某个顶点的度,就是这个顶点vi在第i行(或者i列)的元素之和,比如v1=1+0+1+0=2.
2.顶点vi的所有邻接点就是将第i行元素扫描一遍,为1则是邻接点。
有向图:
0. 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
- 有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。
- 与无向图同样的办法,判断顶点vi 到vj是否存在弧,只需要查找矩阵中A [ i ] [ j ]是否为1即可。
网:每条边上带权的图叫网,那我们怎么样把这些权值储存下来呢:
如图所示:
wi j表示权值,无穷表示一个计算机允许的,大于所有边的权值的值。
那么我们可以创建一个由邻接矩阵创建的图:
下面是一个创建无向网图的示范:
#include <stdio.h>
#define INFINITY 65535
typedef int my_int;
typedef char my_char;
struct graph {
int numsnodes;
int numsedges;
my_char vex[100];
my_int arc[100][100];
};
void creategraph(struct graph*G) {
printf("请输入顶点数和边数\n");
scanf("%d %d", &G->numsnodes, &G->numsnodes);
for (int i = 0; i < G->numsnodes; i++) {//读入顶点信息
scanf("%d", &G->vex[i]);
}
for (int i = 0; i < G->numsnodes; i++) {//初始化为极限值
for (int j = 0; j < G->numsnodes; j++) {
G->arc[i][j] = INFINITY;
}
}
for (int k = 0; k < G->numsedges; k++) {
int i, j, w;
printf("输入边的下标i,j以及权w\n");
scanf("%d %d %d", &i, &j, &w);
G->arc[i][j] = w;
G->arc[j][i] = w;//无向图对称
}
}
int main() {
struct graph G;
creategraph(&G);
return 0;
}
二:邻接表:
数组和链表相结合的储存方式叫邻接表。
如图所示,以上分别是无向图和有向图的邻接表表示。
邻接表中,储存了每个顶点的指向边,如上图的1号顶点就指向了2和4两个点。这种邻接表适合经常需要查询它指向谁这种情况,那有没有一种邻接表可以反向操作,记录谁指向了它呢,那就是:
逆邻接表:
及对于上图,4号顶点后面不是2,而是1和5,道理与邻接表类似。
下面来看看邻接表代码实现:
#include<stdio.h>
#include<stdlib.h>
typedef struct node {
int vertex;
struct node* next;
} node;
node* create_node(int v) {
node* new_node = malloc(sizeof(node));
new_node->vertex = v;
new_node->next = NULL;
return new_node;
}
typedef struct Graph {
int num_of_vertices;
node** adj_lists;
} Graph;
Graph* create_graph(int vertices) {
Graph* graph = malloc(sizeof(Graph));
graph->num_of_vertices = vertices;
graph->adj_lists = malloc(vertices * sizeof(node*));
for (int i = 0; i < vertices; i++)
graph->adj_lists[i] = NULL;
return graph;
}
void add_edge(Graph* graph, int src, int dest) {
node* new_node = create_node(dest);
new_node->next = graph->adj_lists[src];
graph->adj_lists[src] = new_node;
}
void print_graph(Graph* graph) {
for (int v = 0; v < graph->num_of_vertices; v++) {
node* temp = graph->adj_lists[v];
printf("\n Vertex %d\n: ", v);
while (temp) {
printf("%d -> ", temp->vertex);
temp = temp->next;
}
printf("\n");
}
}
int main() {
Graph* graph = create_graph(5);
add_edge(graph, 0, 1);
add_edge(graph, 0, 4);
add_edge(graph, 1, 2);
add_edge(graph, 1, 3);
add_edge(graph, 1, 4);
add_edge(graph, 2, 3);
add_edge(graph, 3, 4);
print_graph(graph);
return 0;
}
这个代码首先定义了一个节点和图的结构,然后通过add_edge函数添加边。在main函数中,我们创建一个图,并添加一些边,最后打印出邻接表。这个邻接表表示了一个有向图。如果想表示无向图,你可以在add_edge函数中添加一行add_edge(graph, dest, src);。
下面是每个部分的详细解释:
typedef struct node:定义了一个名为node的结构体,用于表示图中的一个顶点。每个node包含一个vertex(顶点的值)和一个指向下一个node的指针next。create_node(int v):这是一个函数,用于创建一个新的node。它接受一个整数v作为参数,然后创建一个新的node,其中vertex的值为v,并返回这个新创建的node。typedef struct Graph:定义了一个名为Graph的结构体,用于表示整个图。Graph包含一个num_of_vertices(顶点的数量)和一个指向node指针数组的指针adj_lists。create_graph(int vertices):这是一个函数,用于创建一个新的Graph。它接受一个整数vertices作为参数,然后创建一个新的Graph,其中num_of_vertices的值为vertices,并返回这个新创建的Graph。add_edge(Graph* graph, int src, int dest):这是一个函数,用于在图中添加一条边。它接受一个Graph指针和两个整数src和dest作为参数,然后在src和dest之间添加一条边。print_graph(Graph* graph):这是一个函数,用于打印图的所有顶点和它们的邻居。
那问题来了,如果有的题目既要求指向关系,又要求被指向,那怎么办?
三:十字链表:
所以有了十字链表这种存储方式,如图所示:
这是顶点表的节点结构,其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。
这是边表节点结构,其中tailvex 是指弧起点在顶点表的下标,headvex 是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。
如图所示,顶点依然是存入一个一维数组{ V0 , V1 , V2 , V3 }实线箭头指针的图示完全与的邻接表的结构相同。就以顶点V0说,firstout 指向的是出边表中的第一个结点V3。所以V0边表结点的headvex = 3,而tailvex就是当前顶点V0的下标0,由于V0只有一个出边顶点,所以headlink和taillink都是空。
我们重点需要来解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。对于V0来说,它有两个顶点V1和V2的入边。因此V0的firstin指向顶点V1的边表结点中headvex为0的结点,如上图右图中的①。接着由入边结点的headlink指向下一个入边顶点V2,如图中的②。对于顶点V1,它有一个入边顶点V2 ,所以它的firstin指向顶点V2的边表结点中headvex为1的结点,如图中的③。顶点V2和V3也是同样有一个入边顶点,如图中④和⑤。
再详细解释一下,它并不是简单的1+1,对于出边,它并不直接储存指向它的点,而是储存tailvex,即它本身。而对于逆邻接表,我们可以这么理解,既然它是入边,那必然有对应的出边,所以它就直接指向该出边即可。
四:边集数组:
这个与邻接矩阵有点类似,不过这个是对每个边的描述。边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、 终点下标(end)和权(weight)组成,如下图所示。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
五:邻接多重表:
邻接多重表是无向图的另一种链式存储结构。
在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。所以我们用多重表。
这是边表。其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。
这是顶点。其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。
图的遍历:
图有两种遍历方式,下面我们简要的介绍一下。
一、深度优先遍历
深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。
DFS算法
深度优先搜索类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图。它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点…重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
以此图为例,其深度优先遍历的结果为abdehcfg,类似于前序遍历。
一个举例的代码:
bool visited[MAX_VERTEX_NUM]; //访问标记数组
/*从顶点出发,深度优先遍历图G*/
void DFS(Graph G, int v){
int w;
visit(v); //访问顶点
visited[v] = TRUE; //设已访问标记
//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
for(w = FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w)){
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G, w);
}
}
}
/*对图进行深度优先遍历*/
void DFSTraverse(MGraph G){
int v;
for(v=0; v<G.vexnum; ++v){
visited[v] = FALSE; //初始化已访问标记数据
}
• for(v=0; v<G.vexnum; ++v){ //从v=0开始遍历
• if(!visited[v]){
• DFS(G, v);
• }
• }
}
二、广度优先遍历
广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。
如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。 广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。 以下是广度优先遍历的代码:
邻接矩阵的广度遍历算法
void BFSTraverse(MGraph G){
int i, j;
Queue Q;
for(i = 0; i<G,numVertexes; i++){
visited[i] = FALSE;
}
InitQueue(&Q); //初始化一辅助用的队列
for(i=0; i<G.numVertexes; i++){
//若是未访问过就处理
if(!visited[i]){
vivited[i] = TRUE; //设置当前访问过
visit(i); //访问顶点
EnQueue(&Q, i); //将此顶点入队列
//若当前队列不为空
while(!QueueEmpty(Q)){
DeQueue(&Q, &i); //顶点i出队列
//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
for(j=FirstNeighbor(G, i); j>=0; j=NextNeighbor(G, i, j)){
//检验i的所有邻接点
if(!visited[j]){
visit(j); //访问顶点j
visited[j] = TRUE; //访问标记
EnQueue(Q, j); //顶点j入队列
}
}
}
}
}
}
其广度优先遍历的结果为abcdefgh。
以上图片及部分内容为转载,声明等如下:
版权声明:本文为CSDN博主「UniqueUnit」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/Real_Fool_/…