238 阅读12分钟

图:

图的定义:

图是一种抽象的数据结构,它由节点(顶点)和连接这些节点的边组成。图可以用来表示各种关系,比如网络中的通信连接、社交网络中的人际关系等。图可以是有向的(边有方向)或无向的(边没有方向),可以是带权重的(边有权重)或不带权重的。在图的表示中,通常使用邻接矩阵或邻接表等数据结构来存储图的信息。

屏幕截图 2023-11-25 152822.png

除此之外,图我们一般称元素,我们一般称作顶点。

图的一些基本概念:

无向边:

若两个顶点间的边没有方向,则为无向边,记作(vi,vj),因为没有方向,也可写成(vj,vi),如果图任意两个顶点之间都是无向边,则为无向图。

有向边:

若两个顶点间的边有方向,则为有向边,记<vi,vj>,vi是弧尾,vj是弧头,如果图任意两个顶点之间都是有向边,则为有向图。

屏幕截图 2023-11-25 154318.png

屏幕截图 2023-11-25 154356.png

屏幕截图 2023-11-25 154710.png

屏幕截图 2023-11-25 154753.png

注意连通分量的概念:

1.要是子图。

2.子图要连通。

3.连通子图要有极大顶点数。

4.有极大顶点数的连通子图包含依附于这些顶点的所有边。

屏幕截图 2023-11-25 154844.png

对于一个图:

如果它有n个顶点:

1.如果边小于n-1条边,则为非连通图。

2.如果多于n-1条边,则必定构成了一个环。

3.如果等于n-1,则不一定为生成树,可能压根不连通。

屏幕截图 2023-11-25 155014.png

屏幕截图 2023-11-25 155045.png

下面再来看看图的储存结构:

一:邻接矩阵:

图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息,无向图如图所示:

屏幕截图 2023-11-26 134639.png

屏幕截图 2023-11-26 135007.png

二维数组中值为1,则不存在该边,反之则存在,上图中对角线为0是因为不存在到自身的边或者不存在该边,我们能发现无向图的边数组是一个对称矩阵(满足ai j==aj i)。

对于这样的数组:

1.某个顶点的度,就是这个顶点vi在第i行(或者i列)的元素之和,比如v1=1+0+1+0=2.

2.顶点vi的所有邻接点就是将第i行元素扫描一遍,为1则是邻接点。

有向图:

屏幕截图 2023-11-26 140454.png 0. 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。

  1. 有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。
  2. 与无向图同样的办法,判断顶点vi 到vj是否存在弧,只需要查找矩阵中A [ i ] [ j ]是否为1即可。

网:每条边上带权的图叫网,那我们怎么样把这些权值储存下来呢:

屏幕截图 2023-11-26 141108.png

屏幕截图 2023-11-26 141125.png

如图所示:

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;
}

二:邻接表:

数组和链表相结合的储存方式叫邻接表。

屏幕截图 2023-11-26 145022.png

屏幕截图 2023-11-26 145042.png

如图所示,以上分别是无向图和有向图的邻接表表示。

邻接表中,储存了每个顶点的指向边,如上图的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);

下面是每个部分的详细解释:

  1. typedef struct node:定义了一个名为node的结构体,用于表示图中的一个顶点。每个node包含一个vertex(顶点的值)和一个指向下一个node的指针next
  2. create_node(int v):这是一个函数,用于创建一个新的node。它接受一个整数v作为参数,然后创建一个新的node,其中vertex的值为v,并返回这个新创建的node
  3. typedef struct Graph:定义了一个名为Graph的结构体,用于表示整个图。Graph包含一个num_of_vertices(顶点的数量)和一个指向node指针数组的指针adj_lists
  4. create_graph(int vertices):这是一个函数,用于创建一个新的Graph。它接受一个整数vertices作为参数,然后创建一个新的Graph,其中num_of_vertices的值为vertices,并返回这个新创建的Graph
  5. add_edge(Graph* graph, int src, int dest):这是一个函数,用于在图中添加一条边。它接受一个Graph指针和两个整数srcdest作为参数,然后在srcdest之间添加一条边。
  6. print_graph(Graph* graph):这是一个函数,用于打印图的所有顶点和它们的邻居。

那问题来了,如果有的题目既要求指向关系,又要求被指向,那怎么办?

三:十字链表:

所以有了十字链表这种存储方式,如图所示:

屏幕截图 2023-11-26 203935.png

这是顶点表的节点结构,其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout 表示出边表头指针,指向该顶点的出边表中的第一个结点。

屏幕截图 2023-11-26 204059.png

这是边表节点结构,其中tailvex 是指弧起点在顶点表的下标,headvex 是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。

屏幕截图 2023-11-26 205108.png

如图所示,顶点依然是存入一个一维数组{ 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,即它本身。而对于逆邻接表,我们可以这么理解,既然它是入边,那必然有对应的出边,所以它就直接指向该出边即可。

四:边集数组:

屏幕截图 2023-11-26 210526.png

这个与邻接矩阵有点类似,不过这个是对每个边的描述。边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、 终点下标(end)和权(weight)组成,如下图所示。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。

五:邻接多重表:

邻接多重表是无向图的另一种链式存储结构。

在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。所以我们用多重表。

屏幕截图 2023-11-26 221728.png

这是边表。其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。

屏幕截图 2023-11-26 221829.png

这是顶点。其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。

屏幕截图 2023-11-26 221933.png

屏幕截图 2023-11-26 222511.png

图的遍历:

图有两种遍历方式,下面我们简要的介绍一下。

一、深度优先遍历

深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。

DFS算法

深度优先搜索类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图。它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点…重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。

屏幕截图 2023-11-26 223229.png

以此图为例,其深度优先遍历的结果为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_/…