跳转至

四、图(11 题)

2026 校订版 · 专题阅读 · 保留代码与校订注 · 与版本锁一致

四、图(11 题)

图默认结构

回源 · PDF 第 20 页 · b_20_1;PDF 第 20 页 · b_20_6

邻接表

typedef struct ArcNode {
    int adjvex;
    struct ArcNode *nextarc;
} ArcNode, *ArcPtr;

typedef struct {
    char data;
    ArcNode *firstarc;
} VertexNode;

typedef struct {
    VertexNode adjList[MAXSIZE];
    int numVertices;
    int numEdges;
} Graph;
回源 · PDF 第 20 页 · b_20_1;PDF 第 20 页 · b_20_3;PDF 第 20 页 · b_20_4;PDF 第 20 页 · b_20_5

邻接矩阵

/**/ #define INF INT_MAX

typedef struct {
    char vertex[MAXSIZE];
    int edge[MAXSIZE][MAXSIZE];
    int numVertices;
    int numEdges;
} MatrixGraph;

除第 8 题 Floyd 算法外,本节邻接矩阵代码均按无权图处理,以 0 表示无边、非 0 表示有边。Floyd 题另按“无边为 INF、对角线为 0”解释矩阵。

回源 · PDF 第 20 页 · b_20_6

1. 求无向连通图中距顶点 v 最远的一个结点

int farthestVertexByBFS(Graph G, int start) {
    bool visited[MAXSIZE] = {false};
    int queue[MAXSIZE];
    int front = 0, rear = 0;
    int last = start;

    visited[start] = true;
    queue[rear++] = start;

    while (front < rear) {
        int v = queue[front++];
        last = v;

        for (ArcPtr p = G.adjList[v].firstarc;
             p != NULL; p = p->nextarc) {
            int w = p->adjvex;
            if (!visited[w]) {
                visited[w] = true;
                queue[rear++] = w;
            }
        }
    }

    return last;
}

复杂度: 时间 O(|V|+|E|),额外空间 O(|V|)。

核心思路: BFS 按与起点的最短路径长度逐层访问;队列中最后出队的结点属于最远层,因此是一个最远结点。图连通是题设前提。

回源 · PDF 第 20 页 · b_20_7;PDF 第 20 页 · b_20_8;PDF 第 20 页 · b_20_9

2. 判断有向图中从 Vi 到 Vj 是否存在路径

回源 · PDF 第 21 页 · b_21_0

DFS

bool reachableDFSImpl(Graph G, int current, int target,
                      bool visited[]) {
    if (current == target)
        return true;

    visited[current] = true;
    for (ArcPtr p = G.adjList[current].firstarc;
         p != NULL; p = p->nextarc) {
        int next = p->adjvex;
        if (!visited[next] && reachableDFSImpl(G, next, target, visited))
            return true;
    }
    return false;
}

bool reachableDFS(Graph G, int start, int target) {
    bool visited[MAXSIZE] = {false};
    return reachableDFSImpl(G, start, target, visited);
}
回源 · PDF 第 21 页 · b_21_0;PDF 第 21 页 · b_21_3;PDF 第 21 页 · b_21_6

BFS

bool reachableBFS(Graph G, int start, int target) {
    bool visited[MAXSIZE] = {false};
    int queue[MAXSIZE];
    int front = 0, rear = 0;

    queue[rear++] = start;
    visited[start] = true;

    while (front < rear) {
        int v = queue[front++];
        if (v == target)
            return true;

        for (ArcPtr p = G.adjList[v].firstarc;
             p != NULL; p = p->nextarc) {
            int w = p->adjvex;
            if (!visited[w]) {
                visited[w] = true;
                queue[rear++] = w;
            }
        }
    }

    return false;
}

复杂度: 两种方法时间均为 O(|V|+|E|),额外空间均为 O(|V|)。

校订注:原稿 DFS 代码的 if (visit[p->adjvex] == 0)) 多出一个右括号;本版同时改成找到目标后立即返回,避免无意义搜索。

回源 · PDF 第 21 页 · b_21_1;PDF 第 21 页 · b_21_7;PDF 第 21 页 · b_21_8

3. 输出有向图的所有根结点

若从顶点 r 能到达图中所有顶点,则称 r 为图的根结点。

void markReachable(Graph G, int v, bool visited[]) {
    visited[v] = true;
    for (ArcPtr p = G.adjList[v].firstarc;
         p != NULL; p = p->nextarc) {
        if (!visited[p->adjvex])
            markReachable(G, p->adjvex, visited);
    }
}

void printAllRootVertices(Graph G) {
    for (int start = 0; start < G.numVertices; start++) {
        bool visited[MAXSIZE] = {false};
        markReachable(G, start, visited);

        bool reachesAll = true;
        for (int v = 0; v < G.numVertices; v++) {
            if (!visited[v]) {
                reachesAll = false;
                break;
            }
        }

        if (reachesAll)
            printf("%d ", start);
    }
}

复杂度: 时间 O(|V|(|V|+|E|)),额外空间 O(|V|)。

回源 · PDF 第 21 页 · b_21_2;PDF 第 21 页 · b_21_4;PDF 第 21 页 · b_21_9

4. 判断无向连通图是否存在 EL 路径(2021 年统考题)

题设给定图连通且 |E|>0。当奇度顶点数为 0 或 2 时,存在包含全部边的欧拉通路(EL 路径)。

int hasEulerTrail(MatrixGraph G) {
    int oddCount = 0;

    for (int i = 0; i < G.numVertices; i++) {
        int degree = 0;
        for (int j = 0; j < G.numVertices; j++) {
            if (G.edge[i][j] != 0)
                degree++;
        }
        if (degree % 2 != 0)
            oddCount++;
    }

    return oddCount == 0 || oddCount == 2;
}

复杂度: 时间 O(|V|²),额外空间 O(1)。

若邻接矩阵明确是只含 0/1 的无权矩阵,也可直接对一行元素求和得到度数。若是带权矩阵,则必须统计“非零边数”,不能把权值之和当作度。

回源 · PDF 第 22 页 · b_22_0;PDF 第 22 页 · b_22_4

5. 输出有向图的所有 K 顶点(2023 年统考题)

出度大于入度的顶点称为 K 顶点;函数输出所有 K 顶点并返回其数量。

int printKVertices(MatrixGraph G) {
    int count = 0;

    for (int i = 0; i < G.numVertices; i++) {
        int indegree = 0;
        int outdegree = 0;

        for (int j = 0; j < G.numVertices; j++) {
            if (G.edge[i][j] != 0) outdegree++;
            if (G.edge[j][i] != 0) indegree++;
        }

        if (outdegree > indegree) {
            printf("%c ", G.vertex[i]);
            count++;
        }
    }

    return count;
}

复杂度: 时间 O(|V|²),额外空间 O(1)。

回源 · PDF 第 22 页 · b_22_1;PDF 第 22 页 · b_22_7

6. 用邻接表统计有向图每个顶点的入度和出度

void calculateDegrees(Graph G, int indegree[], int outdegree[]) {
    for (int i = 0; i < G.numVertices; i++) {
        indegree[i] = 0;
        outdegree[i] = 0;
    }

    for (int v = 0; v < G.numVertices; v++) {
        for (ArcPtr p = G.adjList[v].firstarc;
             p != NULL; p = p->nextarc) {
            outdegree[v]++;
            indegree[p->adjvex]++;
        }
    }
}

复杂度: 时间 O(|V|+|E|);除输出数组外,额外空间 O(1)。

回源 · PDF 第 22 页 · b_22_2;PDF 第 22 页 · b_22_3;PDF 第 22 页 · b_22_8

7. 判断无向图是否为一棵树

void countReachableVertices(Graph G, int v,
                            bool visited[], int *count) {
    visited[v] = true;
    (*count)++;

    for (ArcPtr p = G.adjList[v].firstarc;
         p != NULL; p = p->nextarc) {
        if (!visited[p->adjvex])
            countReachableVertices(G, p->adjvex, visited, count);
    }
}

bool isTree(Graph G) {
    if (G.numVertices == 0)
        return false;

    bool visited[MAXSIZE] = {false};
    int reachableCount = 0;
    countReachableVertices(G, 0, visited, &reachableCount);

    return reachableCount == G.numVertices
        && G.numEdges == G.numVertices - 1;
}

复杂度: 时间 O(|V|+|E|),额外空间 O(|V|)。

核心思路: 无向图是树,当且仅当它连通且边数为 |V|-1。

校订注:原稿在 DFS 内误调用了不存在的 func(...),应递归调用 DFS 自身;本版重命名后已修正。

回源 · PDF 第 23 页 · b_23_2;PDF 第 23 页 · b_23_0;PDF 第 23 页 · b_23_1;PDF 第 23 页 · b_23_5

8. Floyd 算法求有向图或无向图的最短路径

假设 G.edge[i][j] 为边权;无边时为 INF,对角线为 0。

void floyd(MatrixGraph G,
           int dist[][MAXSIZE],
           int intermediate[][MAXSIZE]) {
    int n = G.numVertices;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            dist[i][j] = G.edge[i][j];
            intermediate[i][j] = -1;
        }
    }

    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dist[i][k] != INF && dist[k][j] != INF) {
                    long long candidate = (long long)dist[i][k] + dist[k][j];
                    if (candidate < dist[i][j]) {
                        dist[i][j] = (int)candidate;
                        intermediate[i][j] = k;
                    }
                }
            }
        }
    }
}

void printIntermediateVertices(int u, int v,
                               int intermediate[][MAXSIZE]) {
    int k = intermediate[u][v];
    if (k == -1)
        return;

    printIntermediateVertices(u, k, intermediate);
    printf("%d ", k);
    printIntermediateVertices(k, v, intermediate);
}

void printShortestPath(int u, int v,
                       int dist[][MAXSIZE],
                       int intermediate[][MAXSIZE]) {
    if (dist[u][v] == INF) {
        printf("no path\n");
        return;
    }

    if (u == v) {
        printf("%d\n", u);
        return;
    }

    printf("%d ", u);
    printIntermediateVertices(u, v, intermediate);
    printf("%d\n", v);
}

复杂度: 时间 O(|V|³),额外空间 O(|V|²)。

校订注:原稿写作 Floyed/floyed,规范名称为 Floyd;本版补上了对 INF 的判断,避免“不可达距离相加”导致溢出或错误更新。路径输出也先检查 dist[u][v],从而区分“直接相连”(中间结点同样为 -1)和“不可达”。

回源 · PDF 第 23 页 · b_23_3

9. 用邻接表判断有向图是否有环(拓扑排序)

函数约定:有环返回 true,无环返回 false。

bool hasDirectedCycleList(Graph G) {
    int indegree[MAXSIZE] = {0};
    int stack[MAXSIZE];
    int top = -1;
    int visitedCount = 0;

    for (int v = 0; v < G.numVertices; v++) {
        for (ArcPtr p = G.adjList[v].firstarc;
             p != NULL; p = p->nextarc)
            indegree[p->adjvex]++;
    }

    for (int v = 0; v < G.numVertices; v++) {
        if (indegree[v] == 0)
            stack[++top] = v;
    }

    while (top != -1) {
        int v = stack[top--];
        visitedCount++;

        for (ArcPtr p = G.adjList[v].firstarc;
             p != NULL; p = p->nextarc) {
            if (--indegree[p->adjvex] == 0)
                stack[++top] = p->adjvex;
        }
    }

    return visitedCount != G.numVertices;
}

复杂度: 时间 O(|V|+|E|),额外空间 O(|V|)。

校订注:原稿标题写“有/无向图”,并注释无向图使用度为 1 的结点。拓扑排序只定义在有向图上,不能据此一般性地判断无向图是否有环。无向图判环可使用带父结点参数的 DFS 或并查集。

回源 · PDF 第 23 页 · b_23_4;PDF 第 24 页 · b_24_0

10. 用邻接矩阵判断有向图是否有环(拓扑排序)

bool hasDirectedCycleMatrix(MatrixGraph G) {
    int indegree[MAXSIZE] = {0};
    int stack[MAXSIZE];
    int top = -1;
    int visitedCount = 0;

    for (int i = 0; i < G.numVertices; i++) {
        for (int j = 0; j < G.numVertices; j++) {
            if (G.edge[j][i] != 0)
                indegree[i]++;
        }
    }

    for (int v = 0; v < G.numVertices; v++) {
        if (indegree[v] == 0)
            stack[++top] = v;
    }

    while (top != -1) {
        int v = stack[top--];
        visitedCount++;

        for (int w = 0; w < G.numVertices; w++) {
            if (G.edge[v][w] != 0 && --indegree[w] == 0)
                stack[++top] = w;
        }
    }

    return visitedCount != G.numVertices;
}

复杂度: 时间 O(|V|²),额外空间 O(|V|)。

回源 · PDF 第 24 页 · b_24_1;PDF 第 24 页 · b_24_2

11. 判断有向图的拓扑排序是否唯一(2024 年统考题)

bool hasUniqueTopologicalOrder(MatrixGraph G) {
    int indegree[MAXSIZE] = {0};
    int stack[MAXSIZE];
    int top = -1;
    int visitedCount = 0;

    for (int i = 0; i < G.numVertices; i++) {
        for (int j = 0; j < G.numVertices; j++) {
            if (G.edge[j][i] != 0)
                indegree[i]++;
        }
    }

    for (int v = 0; v < G.numVertices; v++) {
        if (indegree[v] == 0)
            stack[++top] = v;
    }

    if (top > 0)
        return false;

    while (top != -1) {
        int v = stack[top--];
        visitedCount++;

        for (int w = 0; w < G.numVertices; w++) {
            if (G.edge[v][w] != 0 && --indegree[w] == 0)
                stack[++top] = w;
        }

        if (top > 0)
            return false;
    }

    return visitedCount == G.numVertices;
}

复杂度: 时间 O(|V|²),额外空间 O(|V|)。

核心思路: 拓扑排序的每一步都必须恰好只有一个入度为 0 的候选顶点;若某一步候选数大于 1,拓扑序不唯一;若最终未访问全部顶点,则图有环,也不存在拓扑序。

回源 · PDF 第 24 页 · b_24_3;PDF 第 24 页 · b_24_4