9.图
1.图

1.图常见类型与术语



-
图数据结构包含以下常用术语。
- 邻接 (adjacency):当两顶点之间存在边相连 时,称这两顶点邻接 。
- 路径 (path):从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的路径 。
- 度 (degree):一个顶点拥有的边数 。对于有向图 ,入度 (in‑degree)表示有多少条边指向该顶点 ,出度 (out‑degree)表示有多少条边从该顶点指出 。
2.图的表示
1.邻接矩阵
-
设图的顶点数量为 𝑛 ,邻接矩阵 (adjacency matrix)使用一个 𝑛 × 𝑛 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 1 或 0 表示两个顶点之间是否存在边
-
设邻接矩阵为 𝑀、顶点列表为 𝑉 ,那么矩阵元素 𝑀[𝑖, 𝑗] = 1 表示顶点 𝑉 [𝑖] 到顶点 𝑉 [𝑗]之间存在边,反之 𝑀[𝑖, 𝑗] = 0 表示两顶点之间无边

2.邻接表

# 邻接表仅存储实际存在的边,而边的总数通常远小于 n2 ,因此它更加节省空间。
# 然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵
-
邻接表结构与哈希表中的链式地址 非常相似,因此也可以采用类似的方法来优化效率。
-
比如当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 𝑂(𝑛) 优化至 𝑂(log 𝑛) ;
-
还可以把链表转换为哈希表,从而将时间复杂度降至 𝑂(1) 。
3.图的常见应用
|
顶点 |
边 |
图计算问题 |
| 社交网络 |
用户 |
好友关系 |
潜在好友推荐 |
| 地铁线路 |
站点 |
站点间的连通性 |
最短路线推荐 |
| 太阳系 |
星体 |
星体间的万有引力作用 |
行星轨道计算 |
2.图的基础操作
1.基于邻接矩阵的实现





1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| typedef struct { int vertices[MAX_SIZE]; int adjMat[MAX_SIZE][MAX_SIZE]; int size; } GraphAdjMat;
GraphAdjMat *newGraphAdjMat() { GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat)); graph->size = 0; for (int i = 0; i < MAX_SIZE; i++) { for (int j = 0; j < MAX_SIZE; j++) { graph->adjMat[i][j] = 0; } } return graph; }
void delGraphAdjMat(GraphAdjMat *graph) { free(graph); }
void addVertex(GraphAdjMat *graph, int val) { if (graph->size == MAX_SIZE) { fprintf(stderr, " 图的顶点数量已达最大值\n"); return; } int n = graph->size; graph->vertices[n] = val; for (int i = 0; i <= n; i++) { graph->adjMat[n][i] = graph->adjMat[i][n] = 0; } graph->size++; }
void removeVertex(GraphAdjMat *graph, int index) { if (index < 0 || index >= graph->size) { fprintf(stderr, " 顶点索引越界\n"); return; } for (int i = index; i < graph->size - 1; i++) { graph->vertices[i] = graph->vertices[i + 1]; } for (int i = index; i < graph->size - 1; i++) { for (int j = 0; j < graph->size; j++) { graph->adjMat[i][j] = graph->adjMat[i + 1][j]; } } for (int i = 0; i < graph->size; i++) { for (int j = index; j < graph->size - 1; j++) { graph->adjMat[i][j] = graph->adjMat[i][j + 1]; } } graph->size--; }
void addEdge(GraphAdjMat *graph, int i, int j) { if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { fprintf(stderr, " 边索引越界或相等\n"); return; } graph->adjMat[i][j] = 1; graph->adjMat[j][i] = 1; }
void removeEdge(GraphAdjMat *graph, int i, int j) { if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { fprintf(stderr, " 边索引越界或相等\n"); return; } graph->adjMat[i][j] = 0; graph->adjMat[j][i] = 0; }
void printGraphAdjMat(GraphAdjMat *graph) { printf(" 顶点列表 = "); printArray(graph->vertices, graph->size); printf(" 邻接矩阵 =\n"); for (int i = 0; i < graph->size; i++) { printArray(graph->adjMat[i], graph->size); } }
|
2.基于邻接表的实现





1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| typedef struct AdjListNode { Vertex *vertex; struct AdjListNode *next; } AdjListNode;
AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { for (int i = 0; i < graph->size; i++) { if (graph->heads[i]->vertex == vet) { return graph->heads[i]; } } return NULL; }
void addEdgeHelper(AdjListNode *head, Vertex *vet) { AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode)); node->vertex = vet; node->next = head->next; head->next = node; }
void removeEdgeHelper(AdjListNode *head, Vertex *vet) { AdjListNode *pre = head; AdjListNode *cur = head->next; while (cur != NULL && cur->vertex != vet) { pre = cur; cur = cur->next; } if (cur == NULL) return; pre->next = cur->next; free(cur); }
typedef struct { AdjListNode *heads[MAX_SIZE]; int size; } GraphAdjList;
GraphAdjList *newGraphAdjList() { GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList)); if (!graph) { return NULL; } graph->size = 0; for (int i = 0; i < MAX_SIZE; i++) { graph->heads[i] = NULL; } return graph; }
void delGraphAdjList(GraphAdjList *graph) { for (int i = 0; i < graph->size; i++) { AdjListNode *cur = graph->heads[i]; while (cur != NULL) { AdjListNode *next = cur->next; if (cur != graph->heads[i]) { free(cur); } cur = next; } free(graph->heads[i]->vertex); free(graph->heads[i]); } free(graph); }
AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { for (int i = 0; i < graph->size; i++) { if (graph->heads[i]->vertex == vet) { return graph->heads[i]; } } return NULL; }
void addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { AdjListNode *head1 = findNode(graph, vet1); AdjListNode *head2 = findNode(graph, vet2); assert(head1 != NULL && head2 != NULL && head1 != head2); addEdgeHelper(head1, vet2); addEdgeHelper(head2, vet1); }
void removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { AdjListNode *head1 = findNode(graph, vet1); AdjListNode *head2 = findNode(graph, vet2); assert(head1 != NULL && head2 != NULL); removeEdgeHelper(head1, head2->vertex); removeEdgeHelper(head2, head1->vertex); }
void addVertex(GraphAdjList *graph, Vertex *vet) { assert(graph != NULL && graph->size < MAX_SIZE); AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode)); head->vertex = vet; head->next = NULL; graph->heads[graph->size++] = head; }
void removeVertex(GraphAdjList *graph, Vertex *vet) { AdjListNode *node = findNode(graph, vet); assert(node != NULL); AdjListNode *cur = node, *pre = NULL; while (cur) { pre = cur; cur = cur->next; free(pre); } for (int i = 0; i < graph->size; i++) { cur = graph->heads[i]; pre = NULL; while (cur) { pre = cur; cur = cur->next; if (cur && cur->vertex == vet) { pre->next = cur->next; free(cur); break; } } } int i; for (i = 0; i < graph->size; i++) { if (graph->heads[i] == node) break; } for (int j = i; j < graph->size - 1; j++) { graph->heads[j] = graph->heads[j + 1]; } graph->size--; free(vet); }
|
# 可以使用列表(动态数组)来代替链表
# 使用哈希表来存储邻接表
3.效率对比
|
邻接矩阵 |
邻接表(链表) |
邻接表(哈希表) |
| 判断是否邻接 |
𝑂(1) |
𝑂(𝑚) |
𝑂(1) |
| 添加边 |
𝑂(1) |
𝑂(1) |
𝑂(1) |
| 删除边 |
𝑂(1) |
𝑂(𝑚) |
𝑂(1) |
| 添加顶点 |
𝑂(𝑛) |
𝑂(1) |
𝑂(1) |
| 删除顶点 |
𝑂(n2 ) |
𝑂(𝑛 + 𝑚) |
𝑂(𝑛) |
| 内存空间占用 |
𝑂(n2 ) |
𝑂(𝑛 + 𝑚) |
𝑂(𝑛 + 𝑚) |
3.图的遍历
1.广度优先遍历

1.算法实现
# 为了防止重复遍历顶点,需要借助一个哈希集合 visited 来记录哪些节点已被访问
# 哈希集合可以看作一个只存储 key 而不存储 value 的哈希表,它可以在 𝑂(1) 时间复杂度下进行 key的增删查改操作。根据 key 的唯一性,哈希集合通常用于数据去重等场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| typedef struct { Vertex *vertices[MAX_SIZE]; int front, rear, size; } Queue;
Queue *newQueue() { Queue *q = (Queue *)malloc(sizeof(Queue)); q->front = q->rear = q->size = 0; return q; }
int isEmpty(Queue *q) { return q->size == 0; }
void enqueue(Queue *q, Vertex *vet) { q->vertices[q->rear] = vet; q->rear = (q->rear + 1) % MAX_SIZE; q->size++; }
Vertex *dequeue(Queue *q) { Vertex *vet = q->vertices[q->front]; q->front = (q->front + 1) % MAX_SIZE; q->size--; return vet; }
int isVisited(Vertex **visited, int size, Vertex *vet) { for (int i = 0; i < size; i++) { if (visited[i] == vet) return 1; } return 0; }
void graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) { Queue *queue = newQueue(); enqueue(queue, startVet); visited[(*visitedSize)++] = startVet; while (!isEmpty(queue)) { Vertex *vet = dequeue(queue); res[(*resSize)++] = vet; AdjListNode *node = findNode(graph, vet); while (node != NULL) { if (!isVisited(visited, *visitedSize, node->vertex)) { enqueue(queue, node->vertex); visited[(*visitedSize)++] = node->vertex; } node = node->next; } } free(queue); }
|











# 广度优先遍历 的序列不唯一。广度优先遍历只要求按由近及远 的顺序遍历,而多个相同距离的顶点的遍历顺序允许被任意打乱。
2.复杂度分析
2.深度优先遍历

1.算法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| int isVisited(Vertex **res, int size, Vertex *vet) { for (int i = 0; i < size; i++) { if (res[i] == vet) { return 1; } } return 0; }
void dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) { res[(*resSize)++] = vet; AdjListNode *node = findNode(graph, vet); while (node != NULL) { if (!isVisited(res, *resSize, node->vertex)) { dfs(graph, res, resSize, node->vertex); } node = node->next; } }
void graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) { dfs(graph, res, resSize, startVet); }
|
# 与广度优先遍历类似,深度优先遍历序列的顺序也不是唯一的。给定某顶点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。
以树的遍历为例,“根 → 左 → 右”“左 → 根 → 右” “左 → 右 → 根”分别对应前序、中序、后序遍历,它们展示了三种遍历优先级,然而这三者都属于深度优先遍历。
2.复杂度分析