建议先阅读: B 栈 Stack, F 队列 Queue


原理

图(Graph)由顶点(Vertex)和连接顶点的边(Edge)组成,是最灵活的数据结构之一,可表示社交网络、地图导航、依赖关系等。

图的分类

  • 有向图 vs 无向图: 边是否有方向
  • 加权图 vs 无权图: 边是否有权重
  • 连通图 vs 非连通图: 任意两点是否可达
  • 稠密图 vs 稀疏图: E ~ V^2(稠密)或 E ~ V(稀疏)

相关术语

  • 度(Degree): 顶点连接的边数
  • 出度/入度: 有向图中从该顶点出发/指向该顶点的边数
  • 路径: 顶点序列,相邻顶点间有边
  • 环: 起点等于终点的路径

存储方式

方式空间判边遍历邻居适用
邻接矩阵O(V^2)O(1)O(V)稠密图
邻接表O(V+E)O(degree)O(degree)稀疏图

实现

加权邻接表

无权图的 BFS 只能求最短路径长度(边数),而 Dijkstra 需要边权重。我们用加权邻接表:

#include <stdlib.h>
#include <limits.h>
 
// 加权邻接表边节点
typedef struct WeightedAdjNode {
    int vertex;
    int weight;
    struct WeightedAdjNode* next;
} WeightedAdjNode;
 
typedef struct {
    int V;
    WeightedAdjNode** adj;
} WeightedGraph;
 
WeightedAdjNode* create_wadj_node(int v, int w) {
    WeightedAdjNode* node = malloc(sizeof(WeightedAdjNode));
    node->vertex = v;
    node->weight = w;
    node->next = NULL;
    return node;
}
 
void wgraph_init(WeightedGraph* g, int V) {
    g->V = V;
    g->adj = calloc(V, sizeof(WeightedAdjNode*));
}
 
void wgraph_destroy(WeightedGraph* g) {
    for (int i = 0; i < g->V; i++) {
        WeightedAdjNode* cur = g->adj[i];
        while (cur) {
            WeightedAdjNode* tmp = cur;
            cur = cur->next;
            free(tmp);
        }
    }
    free(g->adj);
}
 
// directed=1 有向,directed=0 无向
void wgraph_add_edge(WeightedGraph* g, int u, int v, int w, int directed) {
    WeightedAdjNode* node = create_wadj_node(v, w);
    node->next = g->adj[u];
    g->adj[u] = node;
    if (!directed)
        wgraph_add_edge(g, v, u, w, 1);
}

BFS / DFS 遍历(无权图)

typedef struct AdjNode { int vertex; struct AdjNode* next; } AdjNode;
 
void graph_bfs(AdjNode** adj, int V, int start) {
    int* visited = calloc(V, sizeof(int));
    int* queue = malloc(V * sizeof(int));
    int head = 0, tail = 0;
    visited[start] = 1;
    queue[tail++] = start;
    while (head < tail) {
        int u = queue[head++];
        for (AdjNode* cur = adj[u]; cur; cur = cur->next)
            if (!visited[cur->vertex]) {
                visited[cur->vertex] = 1;
                queue[tail++] = cur->vertex;
            }
    }
    free(visited); free(queue);
}
 
void graph_dfs(AdjNode** adj, int V, int start) {
    int* visited = calloc(V, sizeof(int));
    int stack[V], top = 0;
    stack[top++] = start;
    while (top > 0) {
        int u = stack[--top];
        if (visited[u]) continue;
        visited[u] = 1;
        for (AdjNode* cur = adj[u]; cur; cur = cur->next)
            if (!visited[cur->vertex])
                stack[top++] = cur->vertex;
    }
    free(visited);
}

Dijkstra 最短路径

Dijkstra 的核心思想是贪心:每次从未确定的顶点中选出距离起点最近的顶点,用它去松弛其邻居。重复 V 次,每次选最近顶点需要 O(V),总 O(V^2)。用最小堆优化后选顶点降为 O(log V),总 O((V+E)log V)。

flowchart TD
    A["dist[start]=0, 其余 dist=INF"] --> B{"所有顶点已确定?"}
    B -->|否| C["从未确定顶点中选 dist 最小的 u"]
    C --> D["标记 u 为已确定"]
    D --> E["遍历 u 的每个邻居 v"]
    E --> F{"dist[u] + w(u,v) < dist[v]?"}
    F -->|是| G["更新 dist[v]"]
    G --> H["将 (v, dist[v]) 入堆"]
    H --> E
    E --> B
    B -->|是| I["结束,dist 数组即为最短路径"]

C 实现:用数组模拟最小堆作为优先队列。

// ---------- 最小堆优先队列 ----------
typedef struct { int dist; int vertex; } PQNode;
 
typedef struct { PQNode* data; int size; int cap; } MinPQ;
 
void pq_init(MinPQ* pq, int cap) {
    pq->data = malloc(cap * sizeof(PQNode));
    pq->size = 0; pq->cap = cap;
}
void pq_destroy(MinPQ* pq) { free(pq->data); }
 
static void pq_swap(PQNode* a, PQNode* b) {
    PQNode t = *a; *a = *b; *b = t;
}
 
void pq_push(MinPQ* pq, int dist, int v) {
    int i = pq->size++;
    pq->data[i] = (PQNode){dist, v};
    while (i > 0) {
        int p = (i - 1) / 2;
        if (pq->data[p].dist <= pq->data[i].dist) break;
        pq_swap(&pq->data[p], &pq->data[i]);
        i = p;
    }
}
 
int pq_pop(MinPQ* pq, int* out_dist, int* out_v) {
    if (pq->size == 0) return -1;
    *out_dist = pq->data[0].dist;
    *out_v = pq->data[0].vertex;
    pq->data[0] = pq->data[--pq->size];
    int i = 0;
    while (1) {
        int smallest = i;
        int left = 2 * i + 1, right = 2 * i + 2;
        if (left < pq->size && pq->data[left].dist < pq->data[smallest].dist)
            smallest = left;
        if (right < pq->size && pq->data[right].dist < pq->data[smallest].dist)
            smallest = right;
        if (smallest == i) break;
        pq_swap(&pq->data[i], &pq->data[smallest]);
        i = smallest;
    }
    return 0;
}
 
int pq_empty(MinPQ* pq) { return pq->size == 0; }
// ---------- 堆结束 ----------
 
void dijkstra(WeightedGraph* g, int start, int* dist) {
    for (int i = 0; i < g->V; i++) dist[i] = INT_MAX / 2;
    dist[start] = 0;
 
    MinPQ pq;
    pq_init(&pq, g->V * 2);
    pq_push(&pq, 0, start);
 
    while (!pq_empty(&pq)) {
        int d, u;
        pq_pop(&pq, &d, &u);
        if (d != dist[u]) continue;   // 过期条目跳过
 
        for (WeightedAdjNode* cur = g->adj[u]; cur; cur = cur->next) {
            int v = cur->vertex, w = cur->weight;
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq_push(&pq, dist[v], v);
            }
        }
    }
    pq_destroy(&pq);
}

Kruskal 最小生成树

Kruskal 的核心思想:将所有边按权重排序,从小到大依次加入,若加入后不形成环则保留,直到有 V-1 条边。

flowchart LR
    A["所有边按 w 排序"] --> B["依次取最小边 (u,v,w)"]
    B --> C{"u 和 v 已连通?"}
    C -->|否| D["加入此边,合并 u 和 v"]
    C -->|是| E["跳过"]
    D --> F{"已有 V-1 条边?"}
    E --> B
    F -->|否| B
    F -->|是| G["生成树完成"]

C 实现需要并查集,这里直接内联一个简易版:

#include <stdlib.h>
 
typedef struct { int u, v, w; } KEdge;
 
int kedge_cmp(const void* a, const void* b) {
    return ((KEdge*)a)->w - ((KEdge*)b)->w;
}
 
static int kruskal_find(int* parent, int x) {
    return parent[x] == x ? x : (parent[x] = kruskal_find(parent, parent[x]));
}
 
int kruskal(int V, KEdge* edges, int E) {
    int* parent = malloc(V * sizeof(int));
    int* rank = calloc(V, sizeof(int));
    for (int i = 0; i < V; i++) parent[i] = i;
 
    qsort(edges, E, sizeof(KEdge), kedge_cmp);
 
    int total_weight = 0, cnt = 0;
    for (int i = 0; i < E && cnt < V - 1; i++) {
        int pu = kruskal_find(parent, edges[i].u);
        int pv = kruskal_find(parent, edges[i].v);
        if (pu != pv) {
            if (rank[pu] < rank[pv]) { int t = pu; pu = pv; pv = t; }
            parent[pv] = pu;
            if (rank[pu] == rank[pv]) rank[pu]++;
            total_weight += edges[i].w;
            cnt++;
        }
    }
    free(parent); free(rank);
    return total_weight;
}

常用算法复杂度

算法用途时间复杂度条件
BFS无权最短路径、层序遍历O(V+E)任意图
DFS遍历、环检测、连通分量O(V+E)任意图
Dijkstra单源最短路径O((V+E)logV)非负权
Bellman-Ford单源最短路径(可负权)O(VE)可检负环
Floyd-Warshall全源最短路径O(V^3)稠密图
Kruskal最小生成树O(E log E)任意图
Prim最小生成树O((V+E)logV)任意图

应用场景

  • 地图导航: 加权图 + Dijkstra/A* 寻路
  • 社交网络: 好友推荐(二度好友)、影响力分析(连通分量大小)
  • 包依赖管理: 有向无环图(DAG)+ 拓扑排序确定安装顺序

练习

题号题目难度知识点
P3371单源最短路径普及Dijkstra/Bellman-Ford
P4779单源最短路径(标准版)提高堆优化 Dijkstra
P3366最小生成树普及+Kruskal/Prim
P5318查找文献入门BFS/DFS 遍历

力扣 (LeetCode) 有对应题型,竞赛方向推荐力扣/Codeforces。