一、图的基础概念:从零开始理解图

说实话,我刚学数据结构那会儿,觉得图是最有意思的部分。树是一种特殊的图,链表又是特殊的树——你想想看,整个数据结构体系其实都在围着图转。今天我就带你从最基础的概念开始,一步步把图这个东西吃透。

核心要点:图是由顶点(Vertex)和边(Edge)组成的非线性结构。相比线性表和树,图能表达更复杂的关系——比如社交网络、地图导航、电路连接等。

1.1 图的定义与术语

图用数学语言描述就是 G = (V, E)。V 是顶点集合,E 是边集合。举个例子:一个朋友圈,每个人是一个顶点,好友关系就是边。

几个关键术语,我当年面试时被问过无数次:

  • 有向图 vs 无向图:边有没有方向。微博关注是有向的,微信好友是无向的。
  • 完全图:任意两个顶点之间都有边。n 个顶点的无向完全图有 n(n-1)/2 条边。
  • 度(Degree):顶点关联的边数。有向图中还分入度和出度。
  • 路径与回路:从一个顶点到另一个顶点的顶点序列。起点终点相同叫回路。
  • 连通图与连通分量:任意两点都有路径叫连通图。非连通图由多个连通分量组成。

我的经验:做嵌入式开发时,图论用得最多的是拓扑排序——比如编译器的依赖分析、任务调度。有一次我在做多核处理器任务分配,就是靠拓扑排序解决了死锁问题。

1.2 图的存储结构

图怎么存进计算机?两种主流方式:邻接矩阵和邻接表。各有各的脾气,选错了性能差十倍不止。

1.2.1 邻接矩阵

说白了就是一个二维数组。matrix[i][j] = 1 表示顶点 i 到 j 有边。无向图是对称矩阵。

// 邻接矩阵实现
#define MAX_VERTEX 100

typedef struct {
    int vertex[MAX_VERTEX];      // 顶点数组
    int edge[MAX_VERTEX][MAX_VERTEX]; // 邻接矩阵
    int vertexNum, edgeNum;      // 顶点数和边数
} MGraph;

// 初始化图
void initGraph(MGraph *g, int n) {
    g->vertexNum = n;
    g->edgeNum = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            g->edge[i][j] = 0;
        }
    }
}

// 添加边
void addEdge(MGraph *g, int i, int j) {
    g->edge[i][j] = 1;
    g->edge[j][i] = 1;  // 无向图
    g->edgeNum++;
}
特性 邻接矩阵 邻接表
空间复杂度 O(V²) O(V+E)
判断两顶点是否相邻 O(1) O(度)
遍历所有边 O(V²) O(V+E)
适用场景 稠密图 稀疏图

我曾经踩过的坑:在嵌入式系统里用邻接矩阵存一个 1000 个顶点的图,结果内存直接爆了。1000×1000 的 int 数组就是 4MB,对于单片机来说太奢侈了。后来换成邻接表,内存降到了几十 KB。

1.2.2 邻接表

每个顶点维护一个链表,链表中存的是它相邻的顶点。空间省了,但查边慢了点。

// 邻接表实现
typedef struct EdgeNode {
    int adjVertex;              // 邻接顶点下标
    struct EdgeNode *next;      // 下一个邻接点
} EdgeNode;

typedef struct {
    int data;                   // 顶点信息
    EdgeNode *firstEdge;        // 第一条边
} VertexNode;

typedef struct {
    VertexNode adjList[MAX_VERTEX];
    int vertexNum, edgeNum;
} ALGraph;

// 添加边(无向图)
void addEdge(ALGraph *g, int i, int j) {
    EdgeNode *e = (EdgeNode*)malloc(sizeof(EdgeNode));
    e->adjVertex = j;
    e->next = g->adjList[i].firstEdge;
    g->adjList[i].firstEdge = e;
    
    // 无向图需要加两次
    e = (EdgeNode*)malloc(sizeof(EdgeNode));
    e->adjVertex = i;
    e->next = g->adjList[j].firstEdge;
    g->adjList[j].firstEdge = e;
    
    g->edgeNum++;
}

1.3 图的遍历:DFS 与 BFS

遍历图就像走迷宫。两种走法:一条道走到黑(DFS),或者一层层往外扩(BFS)。

1.3.1 深度优先搜索(DFS)

核心思想:从起点出发,沿着一条路走到尽头,然后回溯。用递归或栈实现。

// DFS 递归实现
int visited[MAX_VERTEX];

void DFS(ALGraph *g, int v) {
    visited[v] = 1;
    printf("访问顶点: %d\n", v);
    
    EdgeNode *p = g->adjList[v].firstEdge;
    while (p) {
        if (!visited[p->adjVertex]) {
            DFS(g, p->adjVertex);
        }
        p = p->next;
    }
}

void DFSTraverse(ALGraph *g) {
    for (int i = 0; i < g->vertexNum; i++) {
        visited[i] = 0;
    }
    for (int i = 0; i < g->vertexNum; i++) {
        if (!visited[i]) {
            DFS(g, i);  // 处理非连通图
        }
    }
}

1.3.2 广度优先搜索(BFS)

一层层往外扩散,像水波一样。用队列实现,先访问的顶点先处理。

// BFS 实现
void BFS(ALGraph *g, int start) {
    int queue[MAX_VERTEX];
    int front = 0, rear = 0;
    
    for (int i = 0; i < g->vertexNum; i++) {
        visited[i] = 0;
    }
    
    visited[start] = 1;
    queue[rear++] = start;
    
    while (front != rear) {
        int v = queue[front++];
        printf("访问顶点: %d\n", v);
        
        EdgeNode *p = g->adjList[v].firstEdge;
        while (p) {
            if (!visited[p->adjVertex]) {
                visited[p->adjVertex] = 1;
                queue[rear++] = p->adjVertex;
            }
            p = p->next;
        }
    }
}

实际应用:DFS 常用于拓扑排序、连通分量检测。BFS 常用于最短路径(无权图)、社交网络中的"六度分隔"。我在做地图导航时,BFS 就是找最短路径的基础。

1.4 知识体系总览

下面这张图把本章的核心内容串起来了。我建议你把它存下来,复习时一眼就能看到全貌。

图的基础概念 图的定义与术语 有向图 / 无向图 度 / 入度 / 出度 连通图 / 连通分量 存储结构 邻接矩阵(稠密图) 邻接表(稀疏图) 图的遍历 DFS(栈/递归) BFS(队列)

1.5 避坑指南

最后分享几个我实际开发中遇到的坑,你遇到了能少走弯路:

  • 内存泄漏:邻接表用 malloc 分配边节点,记得在程序退出时释放。我见过一个同事的代码跑了三天后内存耗尽,就是因为忘了 free。
  • 非连通图:遍历时一定要检查所有顶点是否都被访问过。很多人只从起点开始遍历,结果漏掉了孤立的顶点。
  • 递归深度:DFS 用递归实现时,如果图很深(比如 10000 个顶点),递归栈可能溢出。嵌入式环境栈空间有限,建议用显式栈代替递归。
  • 有向图的度:有向图要分别统计入度和出度。我做过一个网页爬虫,就是因为没区分入度出度,导致链接分析全错了。

总结一下:图是描述复杂关系的利器。邻接矩阵适合稠密图,邻接表适合稀疏图。DFS 和 BFS 是图算法的基础,很多高级算法(如最短路径、最小生成树)都建立在它们之上。把这些基础打牢,后面学起来就轻松了。

公众号:蓝海资料掘金营,微信 deep3321