1一、图 Graph 基础
2
31. 前提条件
4
5之前学的树:
6
7一个根
8父子关系
9没有环
10
11图比树更一般:
12
13点和点之间可以任意连接
14可以有方向
15可以有权值
16可以有环
17
18例如:
19
20A —— B
21| |
22C —— D
23
24这里:A、B、C、D 是顶点
25线是边
26
272. 核心概念
28
29图由两部分组成:G = (V, E)
30
31其中:
32
33V:顶点集合
34E:边集合
35
36比如:
37
38V = {A, B, C, D}
39
40E = {(A,B), (A,C), (B,D), (C,D)}
41
42二、有向图和无向图
43
441. 无向图
45
46边没有方向。
47
48A —— B
49
50表示:
51
52A 可以到 B
53B 也可以到 A
54
552. 有向图
56
57边有方向。
58
59A -> B
60
61表示:
62
63A 可以到 B
64但 B 不一定能到 A
65
66比如关注关系、任务依赖关系。
67
68三、带权图
69
70如果边上有数值,就叫带权图。
71
72A --5-- B
73
74这里:5 是权值
75
76可以表示:
77
78距离
79时间
80费用
81代价
82
83最短路径算法就是处理这种图。
84
85四、度、入度、出度
86
871. 无向图的度
88
89一个点连了几条边,度就是几。
90
91A —— B
92|
93C
94
95A 连着 B 和 C。
96
97所以:A 的度 = 2
98
992. 有向图的入度
100
101指向这个点的边数。
102
103A -> C
104B -> C
105
106C 的入度:2
107
1083. 有向图的出度
109
110从这个点出去的边数。
111
112A -> B
113A -> C
114
115A 的出度:2
116
117五、图的存储方式
118
119最常用两种:邻接矩阵和邻接表
120
121六、邻接矩阵
122
1231. 前提条件
124
125邻接矩阵用二维数组存图。
126
127如果有 n 个点,就开:
128
129int graph[n][n];
130
131含义:graph[i][j] 表示 i 到 j 是否有边
132
1332. 无权图
134
135如果没有权值:
136
137有边:1
138无边:0
139
140例如:
141
1420 —— 1
1430 —— 2
1441 —— 2
145
146矩阵:
147
148 0 1 2
1490 0 1 1
1501 1 0 1
1512 1 1 0
152
153因为是无向图,所以矩阵关于主对角线对称。
154
1553. 有向图
156
157例如:
158
1590 -> 1
1600 -> 2
1612 -> 1
162
163矩阵:
164
165 0 1 2
1660 0 1 1
1671 0 0 0
1682 0 1 0
169
170注意:
171
172graph[0][1] = 1
173
174不代表
175
176graph[1][0] = 1
177
1784. 带权图
179
180如果有权值:
181
182有边:权值
183无边:INF
184
185例如:
186
1870 --5-- 1
1880 --2-- 2
189
190可以写:
191
192graph[0][1] = 5
193graph[1][0] = 5
194
195graph[0][2] = 2
196graph[2][0] = 2
197
198无边用:
199
200#define INF 1000000000
201
2025. 邻接矩阵代码
203#define MAXN 100
204#define INF 1000000000
205
206int graph[MAXN][MAXN];
207
208void initGraph(int n)
209{
210 for(int i = 0; i < n; i++)
211 {
212 for(int j = 0; j < n; j++)
213 {
214 if(i == j)
215 {
216 graph[i][j] = 0;
217 }
218 else
219 {
220 graph[i][j] = INF;
221 }
222 }
223 }
224}
225
226无向带权边:
227
228void addUndirectedEdge(int u, int v, int w)
229{
230 graph[u][v] = w;
231 graph[v][u] = w;
232}
233
234有向带权边:
235
236void addDirectedEdge(int u, int v, int w)
237{
238 graph[u][v] = w;
239}
240
241七、邻接表
2421. 前提条件
243
244邻接表是:数组 + 链表
245
246每个点开一条链表,存它能到哪些点。
247
248例如:
249
2500 -> 1
2510 -> 2
2521 -> 2
253
254邻接表:
255
2560: 1 -> 2
2571: 2
2582:
259
2602. 为什么邻接表常用?
261
262如果点很多,边很少,邻接矩阵很浪费。
263
264比如:
265
2661000 个点
267只有 2000 条边
268
269邻接矩阵要:1000 × 1000 = 1000000 个位置
270
271邻接表只存真实存在的边。
272
273所以:
274
275边少用邻接表
276边多用邻接矩阵
277
278八、邻接表 C 代码
279
2801. 结构体
281#include <stdlib.h>
282
283#define MAXN 100
284
285struct EdgeNode {
286 int to;
287 int weight;
288 struct EdgeNode* next;
289};
290
291struct EdgeNode* adj[MAXN];
292
293含义:
294
295adj[i] 是 i 号点的边链表头指针
296
297to 表示这条边到哪个点
298weight 表示边权
299next 指向下一条边
300
3012. 初始化
302void initAdjList(int n)
303{
304 for(int i = 0; i < n; i++)
305 {
306 adj[i] = NULL;
307 }
308}
309
3103. 加有向边
311void addDirectedEdge(int u, int v, int w)
312{
313 struct EdgeNode* node =
314 malloc(sizeof(struct EdgeNode));
315
316 node->to = v;
317 node->weight = w;
318
319 node->next = adj[u];
320 adj[u] = node;
321}
322
323意思:从 u 出发,能到 v,权值是 w
324
325用的是链表中的头插法。
326
3274. 加无向边
328
329无向边:u —— v
330
331等价于两条有向边:
332
333u -> v
334v -> u
335
336代码:
337
338void addUndirectedEdge(int u, int v, int w)
339{
340 addDirectedEdge(u, v, w);
341 addDirectedEdge(v, u, w);
342}
343
344九、遍历邻接表
345
346如果要看某个点 u 能到哪些点:
347
348void printNeighbors(int u)
349{
350 struct EdgeNode* cur = adj[u];
351
352 while(cur != NULL)
353 {
354 printf("%d %d\n", cur->to, cur->weight);
355
356 cur = cur->next;
357 }
358}
359
360十、区别
361
362邻接矩阵:
363优点:
364 判断 u 到 v 有没有边很快
365 O(1)
366
367缺点:
368 空间大
369 O(n²)
370
371适合:
372 点少、边多的稠密图
373
374--------------------------------
375
376邻接表:
377
378优点:
379 省空间
380 O(n + m)
381
382缺点:
383 判断 u 到 v 有没有边要遍历链表
384
385适合:
386 点多、边少的稀疏图
387
388其中:
389
390n = 点数
391m = 边数
392
393十一、总结
394
395图 Graph
396
397组成:G = (V, E)
398
399V:顶点
400E:边
401
402--------------------------------
403
404图的类型:
405
406无向图:
407 边没有方向
408
409有向图:
410 边有方向
411
412带权图:
413 边有权值
414
415--------------------------------
416
417度:
418
419无向图:
420 度 = 连着几条边
421
422有向图:
423 入度 = 指向它的边数
424 出度 = 从它出去的边数
425
426--------------------------------
427
428存储方式:
429
4301. 邻接矩阵
431
432graph[i][j]
433
434表示 i 到 j 是否有边或边权
435
436空间:
437 O(n²)
438
439适合:
440 稠密图
441
442--------------------------------
443
4442. 邻接表
445
446adj[i]
447
448表示 i 能到的所有点
449
450空间:
451 O(n + m)
452
453适合:
454 稀疏图
455
456--------------------------------
457
458无向边:
459
460u - v
461
462存两次:
463
464u -> v
465v -> u
466
467--------------------------------
468
469有向边:
470
471u -> v
472
473只存一次
一、图 Graph 基础
- 前提条件
之前学的树:
一个根
父子关系
没有环
图比树更一般:
点和点之间可以任意连接
可以有方向
可以有权值
可以有环
例如:
A —— B
| |
C —— D
这里:A、B、C、D 是顶点
线是边
- 核心概念
图由两部分组成:G = (V, E)
其中:
V:顶点集合
E:边集合
比如:
V = {A, B, C, D}
E = {(A,B), (A,C), (B,D), (C,D)}
二、有向图和无向图
- 无向图
边没有方向。
A —— B
表示:
A 可以到 B
B 也可以到 A
- 有向图
边有方向。
A -> B
表示:
A 可以到 B
但 B 不一定能到 A
比如关注关系、任务依赖关系。
三、带权图
如果边上有数值,就叫带权图。
A --5-- B
这里:5 是权值
可以表示:
距离
时间
费用
代价
最短路径算法就是处理这种图。
四、度、入度、出度
- 无向图的度
一个点连了几条边,度就是几。
A —— B
|
C
A 连着 B 和 C。
所以:A 的度 = 2
- 有向图的入度
指向这个点的边数。
A -> C
B -> C
C 的入度:2
- 有向图的出度
从这个点出去的边数。
A -> B
A -> C
A 的出度:2
五、图的存储方式
最常用两种:邻接矩阵和邻接表
六、邻接矩阵
- 前提条件
邻接矩阵用二维数组存图。
如果有 n 个点,就开:
int graph[n][n];
含义:graph[i][j] 表示 i 到 j 是否有边
- 无权图
如果没有权值:
有边:1
无边:0
例如:
0 —— 1
0 —— 2
1 —— 2
矩阵:
0 1 2
0 0 1 1
1 1 0 1
2 1 1 0
因为是无向图,所以矩阵关于主对角线对称。
- 有向图
例如:
0 -> 1
0 -> 2
2 -> 1
矩阵:
0 1 2
0 0 1 1
1 0 0 0
2 0 1 0
注意:
graph[0][1] = 1
不代表
graph[1][0] = 1
- 带权图
如果有权值:
有边:权值
无边:INF
例如:
0 --5-- 1
0 --2-- 2
可以写:
graph[0][1] = 5
graph[1][0] = 5
graph[0][2] = 2
graph[2][0] = 2
无边用:
#define INF 1000000000
- 邻接矩阵代码
#define MAXN 100
#define INF 1000000000
int graph[MAXN][MAXN];
void initGraph(int n)
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
if(i == j)
{
graph[i][j] = 0;
}
else
{
graph[i][j] = INF;
}
}
}
}
无向带权边:
void addUndirectedEdge(int u, int v, int w)
{
graph[u][v] = w;
graph[v][u] = w;
}
有向带权边:
void addDirectedEdge(int u, int v, int w)
{
graph[u][v] = w;
}
七、邻接表
- 前提条件
邻接表是:数组 + 链表
每个点开一条链表,存它能到哪些点。
例如:
0 -> 1
0 -> 2
1 -> 2
邻接表:
0: 1 -> 2
1: 2
2:
- 为什么邻接表常用?
如果点很多,边很少,邻接矩阵很浪费。
比如:
1000 个点
只有 2000 条边
邻接矩阵要:1000 × 1000 = 1000000 个位置
邻接表只存真实存在的边。
所以:
边少用邻接表
边多用邻接矩阵
八、邻接表 C 代码
- 结构体
#include <stdlib.h>
#define MAXN 100
struct EdgeNode {
int to;
int weight;
struct EdgeNode* next;
};
struct EdgeNode* adj[MAXN];
含义:
adj[i] 是 i 号点的边链表头指针
to 表示这条边到哪个点
weight 表示边权
next 指向下一条边
-
初始化
void initAdjList(int n)
{
for(int i = 0; i < n; i++)
{
adj[i] = NULL;
}
}
-
加有向边
void addDirectedEdge(int u, int v, int w)
{
struct EdgeNode* node =
malloc(sizeof(struct EdgeNode));
node->to = v;
node->weight = w;
node->next = adj[u];
adj[u] = node;
}
意思:从 u 出发,能到 v,权值是 w
用的是链表中的头插法。
- 加无向边
无向边:u —— v
等价于两条有向边:
u -> v
v -> u
代码:
void addUndirectedEdge(int u, int v, int w)
{
addDirectedEdge(u, v, w);
addDirectedEdge(v, u, w);
}
九、遍历邻接表
如果要看某个点 u 能到哪些点:
void printNeighbors(int u)
{
struct EdgeNode* cur = adj[u];
while(cur != NULL)
{
printf("%d %d\n", cur->to, cur->weight);
cur = cur->next;
}
}
十、区别
邻接矩阵:
优点:
判断 u 到 v 有没有边很快
O(1)
缺点:
空间大
O(n²)
适合:
点少、边多的稠密图
邻接表:
优点:
省空间
O(n + m)
缺点:
判断 u 到 v 有没有边要遍历链表
适合:
点多、边少的稀疏图
其中:
n = 点数
m = 边数
十一、总结
图 Graph
组成:G = (V, E)
V:顶点
E:边
图的类型:
无向图:
边没有方向
有向图:
边有方向
带权图:
边有权值
度:
无向图:
度 = 连着几条边
有向图:
入度 = 指向它的边数
出度 = 从它出去的边数
存储方式:
- 邻接矩阵
graph[i][j]
表示 i 到 j 是否有边或边权
空间:
O(n²)
适合:
稠密图
- 邻接表
adj[i]
表示 i 能到的所有点
空间:
O(n + m)
适合:
稀疏图
无向边:
u - v
存两次:
u -> v
v -> u
有向边:
u -> v
只存一次