数据结构 图(代码实现篇 C语言版)

目录

图的构建

基本功能实现:

图的遍历方式

[graph_bfs 广度遍历](#graph_bfs 广度遍历)

[graph_dfs 深度遍历](#graph_dfs 深度遍历)

图的表示方法

邻接表

邻接矩阵


图的构建

首先实现图的构建的要知道的就是节点构建和节点连接到下一个节点的地址,并且链接到下一个地址是要有一个具有可的链接NULL和多个地址的的结构,所以图的结构是有一个存在一个整形和一个地址,我们在学习树的结构已经有抽象的理解过通过单链表实现树的结构理念,其实在图的结构也不难看出我们也可以通过单链表来实现。

图的实现理念:

图的结构有一个存入内容的结构和一个单链表来存入多个或者没有(NULL)的地址,链表的内容可以存入的是指向对应图当前位置的下一个位置的地址和如果是加权图还可以加进去。

代码展示:

cpp 复制代码
typedef struct AdjListNode {
    int dest;                    /* 目标顶点的编号(0 ~ V-1) */
    struct AdjListNode* next;    /* 指向下一个邻接点的指针,尾节点为 NULL */
} AdjListNode;

typedef struct AdjList {
    AdjListNode* head;           /* 该顶点邻接链表的头指针,无边时为 NULL */
} AdjList;
typedef struct Graph {
    int V;                       /* 顶点总数(Vertex count) */
    int E;                       /* 当前边总数(Edge count) */
    AdjList* array;              /* 邻接表数组,长度 V,array[i] 表示顶点 i 的邻接链表 */
    char** vertex_names;         /* 顶点名称数组,长度 V;vertex_names[i] 为顶点 i 的名称字符串(或 NULL) */
} Graph;

基本功能实现:

图节点初始化,链表创建创建,图节点的销毁

cpp 复制代码
static AdjListNode* new_node(int dest) {
    AdjListNode* node = (AdjListNode*)malloc(sizeof(AdjListNode));
    node->dest = dest;
    node->next = NULL;
    return node;
}

Graph* graph_create(int V) {
    if (V <= 0) return NULL;

    Graph* g = (Graph*)malloc(sizeof(Graph));//创建空间

    g->V = V;
    g->E = 0;

    g->array = (AdjList*)malloc(V * sizeof(AdjList));//创建了(多个头指针)连续空间,通过[]可以控制访问位置,

    g->vertex_names = (char**)malloc(V * sizeof(char*));//同理
    //初始化
    for (int i = 0; i < V; i++) {
        g->array[i].head = NULL;
        g->vertex_names[i] = NULL;
    }
    return g;
}


void graph_destroy(Graph* g) {
    if (!g) return;

    for (int i = 0; i < g->V; i++) {
        AdjListNode* cur = g->array[i].head;
        while (cur) {
            AdjListNode* tmp = cur;
            cur = cur->next;
            free(tmp);
        }
        free(g->vertex_names[i]);
    }

    free(g->array);
    free(g->vertex_names);
    free(g);
}

取名字和查找名字

cpp 复制代码
bool graph_set_vertex_name(Graph* g, int v, const char* name) {
    /* 检查图指针和顶点编号合法性 */
    if (!g || v < 0 || v >= g->V) return false;

    /* 如果该顶点已有名称,先释放旧的名称字符串 */
    free(g->vertex_names[v]);

    /* 如果 name 不是 NULL,使用 strdup 复制一份新字符串;
       否则设为 NULL(清除名称) */
    g->vertex_names[v] = name ? strdup(name) : NULL;

    /* 操作成功 */
    return true;
}

const char* graph_get_vertex_name(Graph* g, int v) {
    /* 检查参数有效性 */
    if (!g || v < 0 || v >= g->V) return NULL;
    /* 直接返回名称指针 */
    return g->vertex_names[v];
}

边的构造、边的删除和查找边

cpp 复制代码
bool graph_add_edge(Graph* g, int src, int dest) {
    /* 参数合法性检查 */
    if (!g || src < 0 || src >= g->V || dest < 0 || dest >= g->V) return false;

    /* 在 src 的邻接表头部插入新节点(指向 dest) */
    AdjListNode* node = new_node(dest);         /* 创建新节点 */
    node->next = g->array[src].head;            /* 新节点指向原头节点 */
    g->array[src].head = node;                  /* 更新头指针为新节点 */

    /* 在 dest 的邻接表头部插入新节点(指向 src),实现无向性 */
    AdjListNode* node2 = new_node(src);          /* 创建新节点 */
    node2->next = g->array[dest].head;           /* 新节点指向原头节点 */
    g->array[dest].head = node2;                 /* 更新头指针为新节点 */

    /* 边计数加 1 */
    g->E++;
    /* 操作成功 */
    return true;
}


bool graph_remove_edge(Graph* g, int src, int dest) {
    /* 参数合法性检查 */
    if (!g || src < 0 || src >= g->V || dest < 0 || dest >= g->V) return false;

    /* ---- 从 src 的链表中删除 dest 节点 ---- */
    /* cur 是二级指针,指向当前节点的 next 指针的地址 */
    AdjListNode** cur = &g->array[src].head;
    /* 遍历链表 */
    while (*cur) {
        /* 找到目标顶点编号相同的节点 */
        if ((*cur)->dest == dest) {
            /* 暂存待删除节点 */
            AdjListNode* tmp = *cur;
            /* 将前驱节点的 next 指向待删除节点的 next(跳过该节点) */
            *cur = (*cur)->next;
            /* 释放节点内存 */
            free(tmp);
            /* 边计数减 1 */
            g->E--;
            /* 跳出循环 */
            break;
        }
        /* cur 移到下一个节点的 next 指针地址 */
        cur = &(*cur)->next;
    }

    /* ---- 从 dest 的链表中删除 src 节点(无向图需要对等删除) ---- */
    AdjListNode** cur2 = &g->array[dest].head;
    /* 遍历 dest 的邻接链表 */
    while (*cur2) {
        /* 找到指向 src 的节点 */
        if ((*cur2)->dest == src) {
            /* 暂存待删除节点 */
            AdjListNode* tmp = *cur2;
            /* 跳过该节点 */
            *cur2 = (*cur2)->next;
            /* 释放节点内存 */
            free(tmp);
            /* 跳出循环 */
            break;
        }
        /* cur2 移到下一个节点的 next 指针地址 */
        cur2 = &(*cur2)->next;
    }

    /* 操作成功 */
    return true;
}

bool graph_has_edge(Graph* g, int src, int dest) {
    /* 参数合法性检查 */
    if (!g || src < 0 || src >= g->V || dest < 0 || dest >= g->V) return false;

    /* cur 指向 src 的邻接链表头 */
    AdjListNode* cur = g->array[src].head;
    /* 遍历链表 */
    while (cur) {
        /* 如果某个节点的 dest 与目标编号相等,说明边存在 */
        if (cur->dest == dest) return true;
        /* 继续下一个节点 */
        cur = cur->next;
    }
    /* 遍历完整个链表都没找到,说明边不存在 */
    return false;
}

因为是无向图,所以需要同时构建两个节点,相互链接,所以在删除边的也需要同时删除对应的链表的两个节点,需要用到循环进行查找位置指定位置进行删除操作。

图的遍历方式

graph_bfs 广度遍历

cpp 复制代码
void graph_bfs(Graph* g, int start) {
    /* 参数合法性检查 */
    if (!g || start < 0 || start >= g->V) return;

    /* visited 数组:记录每个顶点是否已被访问,初始全部为 false */
    bool* visited = (bool*)calloc(g->V, sizeof(bool));

    /* queue 数组:用数组模拟队列,存放待访问的顶点编号 */
    int* queue = (int*)malloc(g->V * sizeof(int));

    /* front ------ 队头下标(出队位置);rear ------ 队尾下标(入队位置) */
    int front = 0, rear = 0;

    /* 标记起始顶点为已访问 */
    visited[start] = true;
    /* 起始顶点入队 */
    queue[rear++] = start;

    /* 只要队列不为空,就继续遍历 */
    while (front < rear) {
        /* 出队一个顶点 v */
        int v = queue[front++];

        /* 获取顶点名称(有名称则显示名称,否则显示编号) */
        const char* name = g->vertex_names[v];
        if (name) printf("%s ", name);
        else      printf("%d ", v);

        /* 遍历 v 的所有邻接点 */
        AdjListNode* cur = g->array[v].head;
        while (cur) {
            /* 如果邻接点尚未被访问 */
            if (!visited[cur->dest]) {
                /* 标记为已访问 */
                visited[cur->dest] = true;
                /* 将该邻接点入队,等待后续访问 */
                queue[rear++] = cur->dest;
            }
            /* 继续下一个邻接点 */
            cur = cur->next;
        }
    }

    /* 换行 */
    printf("\n");

    /* 释放动态分配的内存 */
    free(visited);
    free(queue);
}

1,判断是否符合条件

2,申请两个空间,一个(visited)作为标记已将访问的空间,一个(queue)用于保存当前有分叉的路口。

3,循环:从头优先访问打印出头和分叉路口的个元素,标记已经访问的位置和入队分叉路口,如果遇到死胡同或者已经访问的位置就退出,每次循环都从队提出位置,直到所有元素被访问为止停止循环。

graph_bfs ------ 广度优先遍历(Breadth First Search)

参数:

g ------ 图指针

start ------ 起始顶点编号

说明:

使用队列(FIFO)实现,先访问起始顶点,

再逐层访问其所有邻居,直到所有可达顶点都被访问

graph_dfs 深度遍历

cpp 复制代码
static void dfs_util(Graph* g, int v, bool* visited) {
    /* 标记当前顶点为已访问 */
    visited[v] = true;

    /* 打印当前顶点 */
    const char* name = g->vertex_names[v];
    if (name) printf("%s ", name);
    else      printf("%d ", v);

    /* 遍历 v 的所有邻接点 */
    AdjListNode* cur = g->array[v].head;
    while (cur) {
        /* 如果邻接点未被访问,递归访问它 */
        if (!visited[cur->dest]) {
            dfs_util(g, cur->dest, visited);
        }
        /* 继续下一个邻接点 */
        cur = cur->next;
    }
}
void graph_dfs(Graph* g, int start) {
    /* 参数合法性检查 */
    if (!g || start < 0 || start >= g->V) return;

    /* 创建访问标记数组,初始全为 false */
    bool* visited = (bool*)calloc(g->V, sizeof(bool));

    /* 从 start 开始递归遍历 */
    dfs_util(g, start, visited);

    /* 换行 */
    printf("\n");

    /* 释放动态内存 */
    free(visited);
}

利用递归比广度优先遍历少了一个申请空间,visited 依旧功能是用于标记依旧读取过的地址。

图的表示方法

邻接表

代码展示:

cpp 复制代码
void graph_print(Graph* g) {
    /* 空指针检查 */
    if (!g) return;

    /* 打印图的基本信息:无向、顶点数 V、边数 E */
    printf("Graph (undirected, V=%d, E=%d):\n", g->V, g->E);

    /* 遍历每个顶点,打印其邻接链表 */
    for (int i = 0; i < g->V; i++) {
        /* 获取当前顶点名称,优先显示名称,否则显示编号 */
        const char* name = g->vertex_names[i];
        if (name) printf("%s", name);
        else      printf("%d", i);

        /* 打印箭头分隔符 */
        printf(" -> ");

        /* 遍历邻接链表 */
        AdjListNode* cur = g->array[i].head;
        while (cur) {
            /* 获取邻居顶点的名称,优先显示名称 */
            const char* dname = g->vertex_names[cur->dest];
            if (dname) printf("%s", dname);
            else       printf("%d", cur->dest);
            /* 邻居之间用空格分隔 */
            printf(" ");
            /* 移动到下一个邻接点 */
            cur = cur->next;
        }

        /* 链表结尾标记 */
        printf("NULL\n");
    }
}

main.c

cpp 复制代码
    graph_set_vertex_name(g, 0, "A");   /* 顶点 0 命名为 "A" */
    graph_set_vertex_name(g, 1, "B");   /* 顶点 1 命名为 "B" */
    graph_set_vertex_name(g, 2, "C");   /* 顶点 2 命名为 "C" */
    graph_set_vertex_name(g, 3, "D");   /* 顶点 3 命名为 "D" */
    graph_set_vertex_name(g, 4, "E");   /* 顶点 4 命名为 "E" */

    /* ---- 添加边 ---- */
    /* 在顶点之间添加无向边 */
    graph_add_edge(g, 0, 1);            /* A - B */
    graph_add_edge(g, 0, 3);            /* A - D */
    graph_add_edge(g, 1, 2);            /* B - C */
    graph_add_edge(g, 1, 3);            /* B - D */
    graph_add_edge(g, 2, 4);            /* C - E */
    graph_add_edge(g, 3, 4);            /* D - E */

    /* ---- 打印图结构 ---- */
    /* 输出邻接表,显示每个顶点的所有邻居 */
    graph_print(g);

输出格式示例:

邻接矩阵

cpp 复制代码
void graph_matrix_print(GraphMatrix* g) {
    /* 空指针检查 */
    if (!g) return;

    /* 打印基本信息 */
    printf("GraphMatrix (undirected, V=%d, E=%d):\n", g->V, g->E);

    /* ---- 打印列标头 ---- */
    printf("    ");  /* 左上角留空 */
    for (int j = 0; j < g->V; j++) {
        /* 每列宽度 4 个字符,显示顶点名称或编号 */
        const char* name = g->vertex_names[j];
        if (name) printf("%4s", name);
        else      printf("%4d", j);
    }
    printf("\n");

    /* ---- 逐行打印矩阵 ---- */
    for (int i = 0; i < g->V; i++) {
        /* 打印行标头(当前顶点名称) */
        const char* name = g->vertex_names[i];
        if (name) printf("%4s", name);
        else      printf("%4d", i);

        /* 打印该行每个元素:1 表示有边,0 表示无边 */
        for (int j = 0; j < g->V; j++) {
            printf("%4d", g->matrix[i][j] ? 1 : 0);
        }
        /* 行末换行 */
        printf("\n");
    }
}

输出结果:


感谢观看!

悠仁さん

相关推荐
88号技师1 小时前
2026年2月一区SCI-交叉传播优化算法Propagation Alongside Crossover-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
aini_lovee1 小时前
多智能体粒子群优化(Multi-Agent Particle Swarm Optimization, MAPSO)
算法
周末也要写八哥1 小时前
贪心法求经典算法题——最低加油次数
算法
插件开发1 小时前
vs2015 cuda c++ 线程号的计算详解
开发语言·c++·算法
有点。1 小时前
C++(前缀和与差分)
c++·算法
仍然.2 小时前
算法题目---BFS解决最短路问题
算法·宽度优先
渡众机器人2 小时前
第八届全球校园人工智能算法精英大赛-算法应用赛-空地协同侦排挑战赛规则
人工智能·算法
wayz112 小时前
Overlap:HWMA(Holt-Winter移动平均线)技术指标详解
算法·金融·数据分析·量化交易·特征工程
Shadow(⊙o⊙)3 小时前
专题四:前缀和
数据结构·算法