简单好理解的图论讲解(图论的三种表现形式以及相关操作)

  • Post author:
  • Post category:其他


图论是数据结构与算法中必须逾越的一座大山,适用范围非常广泛,当然也具有一定的难度,本人也是今年第一次学习图论的知识点,不能说掌握的很好,但我会尽可能的把我学会的知识点分享给大家,在写这篇博客之前我也查阅了很多大牛的图论分享,也算基本上涵盖了图论的重要知识点,如有错误还望各路大神斧正。另外以下程序都是由

C语言

编写的,海域。

首先我们简单认识一下图论,我举一个最简单的例子,就是我们常用的百度或者高德地图,我们在导航时,背后的本质就是图论的各种算法,例如是否有通路可以抵达,或者最短路径是哪条线路等,所以图论的应用是非常广泛的,包括全市公交线路,如何帮我我们快速找到最快抵达的路线,这都是图论的功劳。另外值得注意的是,在计算图论的复杂度的时候,我们不再用n进行计算了,而是要以图的顶点个数V和边的个数E进行计算。那么接下来我们就开始正式讲解。

图论主要有三种表现形式,分别是边的数组(Array of edges),临接矩阵(Adjacency matrix)和临接链表(Adjacency list)。其中临接矩阵是最好理解,也是大家一定要掌握的,而我个人比较喜欢临接链表,三种形式主要是数据储存的方式不同,但背后的理论是相似的,学会一个另外的都会变得很好理解。当然这三种我都会给大家一一介绍的,各位看官可以根据自己的需求选择相应的方法。

1. Array-of-edges

边的数组主要是以数组的形式去储存顶点和边的信息,具体说应该是二维数组。具体的程序细节都在注释里面,大家可以结合代码去理解。

typedef int Vertex;

typedef struct Edge* edge;
struct Edge {
    Vertex v; // 每条边的起点编号
    Vertex w; // 每条边的终点编号
};

typedef struct graph* Graph;
struct graph {
    edge **edges; //指向结构体指针的指针
    int numberOfV; //顶点的个数
    int numberOfE; //边的个数
};


graph* newGraph(int numberOfV) { //创建一个新的图
    graph* newGraph = malloc(sizeof (struct graph));
    newGraph->edges = malloc(sizeof (edge*) * 100); //构建二维数组(这是二维数组的第一层,定义大小)
    //初始化顶点和边的数量,边的数量要根据插入函数来决定
    newGraph->numberOfE = 0;
    newGraph->numberOfV = numberOfV;
    return newGraph;
}

//添加一条边
void append_edge(graph *g, int v, int w){ // 输入每条边起点和终点的编号
    edge *newEdge = malloc(sizeof (struct Edge));
    newEdge->v = v;
    newEdge->w = w;
    g->edges[g->numberOfE] = newEdge; //由创建街道可知,Graph->edges是一个可以存储100个边的数组,这一步的意思是从第0个开始存储边,起点和终点都同时存入了
    g->numberOfE++;
}

void showGraph (graph* g){
    printf("number of vertex: %d\n", g->numberOfV);
    printf("number of edges: %d\n", g->numberOfE);
    for (int i = 0; i < g->numberOfE; i++) {
        printf("edge[%d] v:%d - w:%d\n", i+1, g->edges[i]->v, g->edges[i]->w);
    }
}

void freeGraph(graph* g){
    for (int i = 0; i < g->numberOfE; i++)
        free(g->edges[i]);
    free(g->edges);
    free(g);
}

int main(void){
    graph *g = newGraph(6);

    append_edge(g, 1, 2);
    append_edge(g, 1, 3);
    append_edge(g, 1, 4);

    showGraph(g);
    freeGraph(g);
}

2. Adjacency matrix

矩阵相信大家都不陌生了,矩阵可以非常简单的让我们画出一个图,具体操作其实很简单,就是两个点若没有边则填0,若有边则填1.本质上也是二维数组,但是边的数组是有边才输入顶点编号,而矩阵是所有顶点都要写出来,然后两两不断的做对比。

//插入/删除一条边的算法法度是O(1),直接找到改为1/0即可,查找是O(E)
//创建(初始化,存储)一个矩阵的算法法度是O(v^2),
typedef int Vertex;

typedef struct edge { //类似于一个房屋
    Vertex v; // 每条边的起点编号
    Vertex w; // 每条边的终点编号
}Edge;

typedef struct graph{ //类似于一个街道
    int **matrix; //定义为二维数组
    int number_of_v; //顶点的数量
    int number_of_e; //边的数量
}Graph;


Graph *new_graph(int number_of_v){ //创建一个矩阵并将其初始化
    Graph *new_graph = malloc(sizeof(struct graph));
    new_graph->matrix = malloc(sizeof (int *) * number_of_v);
    for (int i = 0; i < number_of_v; i++) { //二维数组的初始化和赋值必须用两个for循环
        new_graph->matrix[i] = malloc(sizeof (int) * number_of_v); //这两个malloc就是建立矩阵的过程。00,01,02...53, 54, 55
        for (int j = 0; j < number_of_v; j++) {
            new_graph->matrix[i][j] = 0; //给每个点进行初始化,默认都是0,构成一个5*5的矩阵
        }
    }
    new_graph->number_of_e = 0;
    new_graph->number_of_v = number_of_v;
    return new_graph;
}

void insert_edge(Graph* g, Edge e){
    g->matrix[e.v][e.w] = 1;
    g->matrix[e.w][e.v] = 1;  //这一步是考虑了无向图,若是有向图则不用加
    g->number_of_e++;
}

void showGraph(Graph* g){
    printf("number of vertices: %d\n", g->number_of_v);
    printf("number of edges: %d\n", g->number_of_e);
    for (Vertex i = 0; i < g->number_of_v; i++) {
        for (Vertex j = 0; j < g->number_of_v; j++) {
            if (g->matrix[i][j] == 1){
                printf("edge v:%d - w:%d\n", i, j);
            }
        }
    }
}

void freeGraph(Graph* g){
    for (int i = 0; i < g->number_of_v; i++) {
        free(g->matrix[i]);
    }
    free(g->matrix);
    free(g);
}
3. Adjacency linked list

临接链表要略微复杂一点,它储存数据的原理是,先建立一个链表用于储存顶点的信息。若两个顶点之间有边,则在该顶点的node下插入相应编号的子节点。如下图所示:

接下来我就用临接链表的方式来做一个综合练习,以讲解图论的相关操作。具体操作内容有以下13个:

1.图的建立

2.图的消除

3.图的展示

但注意show的形式是:

vertex1
vertex2
vertex3
...
vertexN
vertex1 vertex2 weight
vertex2 vertex4 weight
vertexN vertex4 weight

4.增加一个顶点

5.是否有目标顶点

6.删除一个目标顶点

7.返回顶点个数

8.判断两个点之间是否有边

9.删除一条边

10.改变目标边的权重

11.显示目标边的权重

12.返回边的个数

13.插入一条边

首先是结构体的建立,这里比起一般的链表多了第三个结构体,专门用于方便链表的综合操作(母子节点操作):

typedef char* string
typedef struct house* Node;
typedef struct linked_list* List;
typedef struct Graph_Repr* g;

struct house{ //普通节点
    string data;
    size_t weight;
    Node next;
    Node prev;
    List children; //子节点
};

struct linked_list{ //一条链表
    Node head;
    Node tail;
    int length; //记录链表长度
};

struct Graph_Repr{
    List vertex; //代表每个顶点,每个顶点上还有子节点,子节点的编号就是边
};

在进行实际操作前要提前写好节点建立和链表建立这两个操作函数,这都是最基本的链表操作,就不过多解释了,如果看不懂可以看一下我之前写的单双链表的讲解:

Node create_node(string data){
    Node new_node = malloc(sizeof(struct house));
    new_node->data = malloc(sizeof(char) * (strlen(data) + 1));
    strcpy(new_node->data, data);
    new_node->data[strlen(data)] = '\0'; //强制加终止字符,避免不必要的bug
    new_node->next = new_node->prev = NULL;
    new_node->children = NULL;
    new_node->weight = 0;
    return new_node;
}

List g_list_create (void) {
    List new_list = malloc(sizeof(struct linked_list));
    new_list->tail = new_list->head = NULL;
    new_list->length = 0;
    return new_list;
}

现在正式开始操作讲解,首先是

图的建立和destroy

graph graph_create (void){
    g new_graph = malloc(sizeof(struct Graph_Repr));
    new_graph->vertex = g_list_create();
    new_graph->vertex->head =new_graph->vertex->tail = NULL; //再次强调必须要初始化
    return new_graph;
}


void graph_destroy (graph G){
    Node temp = G->vertex->head;
    Node next;
    while (temp != NULL) {
        next = temp->next;
        free(temp->data);
        free(temp);
        temp = next;
    }
    free(G->vertex);
    free(G);
}


首先是顶点的相关操作


增加一个顶点:

在这相信大家就能理解刚刚多建立的第三个结构体的作用了,它是指向链表结构体的指针,帮助我们在使用临接链表时定位不同的顶点,也可以方便我们在后续添加子节点。

void graph_add_vertex (graph G, string vertex){
    if (G->vertex->head == NULL) {
        G->vertex = g_list_create(); //在vertex里初始化一个链表空间
        G->vertex->head = G->vertex->tail = create_node(vertex);
    } else {
        Node new_v = create_node(vertex);
        G->vertex->tail->next = new_v;
        new_v->prev = G->vertex->tail;
        G->vertex->tail = new_v;
    }
    G->vertex->length++;
}


判断是否有目标顶点:



很简单,就是用strcmp函数,之前在链表的讲解也有介绍过,就不过多赘述了。

bool graph_has_vertex (graph G, string vertex) {
    bool result = false;
    Node temp = G->vertex->head;
    while (temp != NULL) {
        if (strcmp(vertex, temp->data) == 0) {
            result = true;
        }
        temp = temp->next;
    }
    return result;
}


删除目标链表:



虽然感觉很简单,但这个函数要复杂一点,因为要考虑四种可能的情况:只有一个节点,且是目标节点,头是目标节点,尾是目标节点以及中间是目标节点。其实本质上和双链表的操作是一样的,所以大家一定要掌握好链表和指针的基础知识。

void graph_remove_vertex (graph G, string vertex) {
    if (graph_has_vertex(G, vertex) == true) {
        if (G->vertex->head == G->vertex->tail) { //只有一个节点
            G->vertex->length--;
            G->vertex->head = G->vertex->tail = NULL;
            free(G->vertex->tail->data);
            free(G->vertex->tail);
        } else if (strcmp(vertex, G->vertex->tail->data) == 0) { //删尾节点
            Node delete_node = G->vertex->tail;
            G->vertex->length--;
            G->vertex->tail->prev->next = NULL;
            G->vertex->tail = G->vertex->tail->prev;
            free(delete_node->data);
            free(delete_node);
        } else if (strcmp(vertex, G->vertex->head->data) == 0) { //删头节点
            Node delete_node = G->vertex->head;
            G->vertex->length--;
            G->vertex->head->next->prev = NULL;
            G->vertex->head = G->vertex->head->next;
            free(delete_node->data);
            free(delete_node);
        } else { //删除的是中间节点
            Node temp = G->vertex->head;
            G->vertex->length--;
            while (temp != NULL) {
                if (strcmp(vertex, temp->data) == 0) {
                    break;
                }
                temp = temp->next;
            }
            temp->next->prev = temp->prev;
            temp->prev->next = temp->next;
            free(temp->data);
            free(temp);
        }
    }
}


返回顶点的个数:

记得要先用if判断链表不为空,且有节点,不然遇到这样的特殊case程序会报错。

size_t graph_vertices_count (graph G) {
    size_t result = 0;
    if (G->vertex != NULL && G->vertex->head != NULL) {
        result = G->vertex->length;
    }
    return result;
}


接下来都是边的操作


首先是3个工具函数的封装,这样写出来的具体操作更整洁


1.


找到目标vertex并返回其结果(值):

Node find_parent (graph G, string vertex) {
    Node result = NULL;
    if (G->vertex != NULL && G->vertex->head != NULL) {
        Node temp = G->vertex->head;
        while (temp != NULL) {
            if (strcmp(vertex, temp->data) == 0) { // 找到目标parent
                result = temp;
                break;
            }
            temp = temp->next;
        }
    }
    return result;
}


2.在目标母节点下找到目标子节点并返回其结果:

Node find_child (graph G, string vertex1, string vertex2) {
    Node result = NULL;
    Node target_parent = find_parent(G, vertex1); //找到目标母节点
    Node temp = target_parent->children->head; //定位到目标母节点子链表的head
    while (temp != NULL) {
        if (strcmp(vertex2, temp->data) == 0) { //判断是否是目标子节点
            result = temp;
            break;
        }
        temp = temp->next;
    }
    return result;
}


3.边的插入:

void edge_insert(List child, string vertex, size_t weight) {
    if (child->head == NULL) {
        child->head = child->tail = create_node(vertex);
        child->head->weight = weight;
    } else {
        Node new_node = create_node(vertex);
        new_node->weight = weight;
        child->tail->next = new_node;
        new_node->prev= child->tail;
        child->tail = new_node;
    }
    child->length++; //不要忘了这一步,方便记录总的边数
} 

接下来开始正式操作。


添加一条边:

void graph_add_edge (graph G, string vertex1, string vertex2, size_t weight) {

    Node temp = find_parent(G, vertex1);//先找到目标母节点
    Node need_insert_v = find_parent(G, vertex2); //待插入的字节点
    if (temp != NULL && need_insert_v != NULL && G->vertex != NULL && G->vertex->head != NULL) {
        if (temp->children == NULL) { //这一步不能忘,如果是空的要先开辟一个子链表的空间
            temp->children = g_list_create();
        }
        edge_insert(temp->children, vertex2, weight);//用刚刚写的函数进行插入
    }
}


判断两个点之间是否有边:

bool graph_has_edge (graph G, string vertex1, string vertex2) {
    bool result = false;
    Node target_parent = find_parent(G, vertex1);
    if (target_parent != NULL && target_parent->children != NULL && target_parent->children->head != NULL) {
        Node target_child = target_parent->children->head;
        while (target_child != NULL) {
            if (strcmp(vertex2, target_child->data) == 0) {
                result = true;
            }
            target_child = target_child->next;
        }
    }
    return result;
}


删除特定的一条边:

同样的要考虑四种情况,看上去复杂,其实还是很好理解的。

size_t graph_remove_edge (graph G, string vertex1, string vertex2) {
    size_t result = 0;
    if (graph_has_edge(G, vertex1, vertex2) == true) {
        Node target_parent = find_parent(G, vertex1);
        if (target_parent->children->head == target_parent->children->tail) { //只有一个节点
            target_parent->children->length--;
            target_parent->children->head = target_parent->children->tail = NULL;
            result = target_parent->children->head->weight;
            free(target_parent->children->head->data);
            free(target_parent->children->head);
        } else if (strcmp(vertex2, target_parent->children->tail->data) == 0) { //删除的是尾节点
            Node delete_node = target_parent->children->tail;
            target_parent->children->length--;
            target_parent->children->tail->prev->next = NULL;
            target_parent->children->tail = delete_node->prev;
            result = delete_node->weight;
            free(delete_node->data);
            free(delete_node);
        } else if (strcmp(vertex2, target_parent->children->head->data) == 0) { //删除的是头节点
            target_parent->children->length--;
            Node delete_node = target_parent->children->head;
            target_parent->children->head->next->prev = NULL;
            target_parent->children->head = delete_node->next;
            result = delete_node->weight;
            free(delete_node->data);
            free(delete_node);
        } else { //删除的是中间节点
            Node temp = find_child(G, vertex1, vertex2);
            temp->next->prev = temp->prev;
            temp->prev->next = temp->next;
            result = temp->weight;
            free(temp->data);
            free(temp);
        }
    }
    return result;
}


改变某一条边的权重:

void graph_set_edge (graph G, string vertex1, string vertex2, size_t weight) {
    if (graph_has_edge(G, vertex1, vertex2) == true) {
        Node target_child = find_child(G, vertex1, vertex2);
        target_child->weight = weight;
    }
}


返回某一条边的权重:

size_t graph_get_edge (graph G, string vertex1, string vertex2) {
    size_t result = 0;
    if (graph_has_edge(G, vertex1, vertex2) == true) {
        Node target_child = find_child(G, vertex1, vertex2);
        result = target_child->weight;
    }
    return result;
}


返回边的数量:

size_t graph_edges_count (graph G, string vertex) {
    size_t result = 0;
    Node find_vertex = find_parent(G, vertex);
    if (find_vertex != NULL && find_vertex->children != NULL && find_vertex->children->head) {
        result = find_vertex->children->length;
    }
    return result;
}

以上所有就是图论的一些基本操作,欢迎大家在评论区讨论,接下来几天我会找时间总结关于图的最短路径、是否有通路等重要算法,希望大家多多支持!



版权声明:本文为weixin_55469859原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。