链式前向星、vector存图
场景设定
想象你是一个社交达人,要记录你和所有朋友的关系(这就是“图”)。每个朋友是一个节点,关系是一条边。你需要快速回答:“我有哪些朋友?”(遍历邻居)。
方式1:链式前向星(固定小本本)
-
核心思想:你买了一个固定页数的笔记本(预分配的数组)。
-
怎么记?
- 给每个朋友一个专属页(
head[你]
)。 - 每认识一个新朋友,就在笔记本最新空白页记录:
- 朋友名字 (
to
) - 和你的关系 (
weight
,可选) - 关键! 还要写上“上一个记录在笔记本第几页?” (
next
)
- 朋友名字 (
- 更新你的专属页:写上最新这条记录的页码。
- 给每个朋友一个专属页(
-
通俗比喻:
- 你的笔记本就是
edges[]
数组(每条边占一页)。 head[你]
是你名字标签贴纸指向的最新记录页码。next
就像是“上一条记录在第几页?”的提示。- 要查所有朋友?从
head[你]
找到最新记录,然后根据next
翻到前一页,再前一页…直到没记录了(next = -1
)。
- 你的笔记本就是
优点
- 省空间(内存少):
- 笔记本只记核心信息(朋友名、关系、上条页码),没有多余开销。
vector
像活页夹,每页本身还有夹子、标签等额外重量(vector
的容量指针、大小等元数据)。
- 加朋友超快(O(1)):
- 直接写在最新空白页,更新你的标签贴纸 (
head
) 就行。固定操作,永不拖延。
- 直接写在最新空白页,更新你的标签贴纸 (
- 笔记本整齐(内存连续):
- 所有记录按页码连续存放。CPU 一次“搬”一摞记录进缓存效率高(缓存友好),但翻页查找过程不连续。
缺点
- 笔记本页数固定(静态大小):
- 买的时候要猜最多交多少朋友。朋友太多?本子写满了,得重买更大的本子,全部重抄一遍(重新初始化,扩容麻烦)。
- 绝交(删除边)超麻烦:
- 想删掉一个朋友的记录?你得顺着链条一页页找,找到后还要修改它前后记录的
next
指向,像拆掉一节火车车厢。几乎没人用链式前向星删边。
- 想删掉一个朋友的记录?你得顺着链条一页页找,找到后还要修改它前后记录的
- 查朋友名单有点慢(遍历效率):
- 虽然记录在物理上是连续的,但你查名单时是根据
next
跳着翻页(第 5 页 -> 第 2 页 -> 第 8 页)。翻页动作不连贯,CPU 缓存可能帮不上忙。
- 虽然记录在物理上是连续的,但你查名单时是根据
- 写起来费劲(代码复杂):
- 你得自己维护
head
和每条记录的next
指针。写代码时容易搞晕,调试也麻烦。
- 你得自己维护
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1000; // 最大节点数
const int M = 10000; // 最大边数// 定义边的结构体
struct Edge {int to; // 这条边指向哪个节点int next; // 同一个起点的下一条边的索引(相当于"上一条记录的页码")int w; // 边权(可选)
} edges[M]; // 存储所有边的数组int head[N]; // head[u] 表示节点 u 的第一条边的索引(初始为 -1)
int idx = 0; // 当前边的索引(相当于"最新空白页的页码")// 添加一条从 u 到 v 的边,权重为 w
void addEdge(int u, int v, int w) {edges[idx].to = v; // 记录这条边指向 vedges[idx].w = w; // 记录边权edges[idx].next = head[u]; // 新边的 next 指向 u 原来的第一条边head[u] = idx++; // 更新 u 的第一条边为当前这条边
}// 遍历节点 u 的所有邻居
void printNeighbors(int u) {cout << "节点 " << u << " 的朋友: ";for (int i = head[u]; i != -1; i = edges[i].next) {int v = edges[i].to;int w = edges[i].w;cout << v << "(亲密度:" << w << ") ";}cout << endl;
}int main() {memset(head, -1, sizeof(head)); // 初始化 head 数组为 -1(表示空链表)// 添加边:0 -> 1 (权重 5), 0 -> 2 (权重 3), 1 -> 2 (权重 2)addEdge(0, 1, 5);addEdge(0, 2, 3);addEdge(1, 2, 2);// 打印邻居printNeighbors(0); // 输出: 节点 0 的朋友: 2(亲密度:3) 1(亲密度:5)printNeighbors(1); // 输出: 节点 1 的朋友: 2(亲密度:2)return 0;
}
关键点
head[u]
存储节点u
的最新一条边的索引(类似链表头指针)。edges[idx]
存储所有边,idx
是当前可用的边索引(类似动态分配的页数)。next
指向同起点的上一条边(类似链表的前驱指针)。- 遍历时,从
head[u]
开始,沿着next
走,直到-1
(类似链表遍历)。
适合谁? 朋友数量上限确定(比如竞赛题已知最大点数/边数),追求极致速度和省内存的“极客”。
方式2:Vector 邻接表(活页文件夹)
-
核心思想:你买了一个带标签的活页文件夹。给每个朋友(包括你自己)分配一个专属分区。
-
怎么记?
- 想加一个新朋友“老王”?
- 直接找到你自己的分区。
- 在分区里新加一页活页纸,写上“老王”和你们的关系(
vector[你].push_back(Edge{老王, 关系})
)。
-
通俗比喻:
- 文件夹是
vector > graph
。 - 每个分区是
graph[你]
、graph[朋友A]
、graph[朋友B]
… - 每个分区里的活页纸就是该节点的所有邻居边记录。
- 文件夹是
优点
- 分区独立,无限加页(动态扩容):
- 你的分区写满了?系统自动给你换更大的分区页夹(
vector
自动扩容)。虽然换的时候要复印所有旧页(复制数据),但均摊下来每次加页还是很快(均摊 O(1))。
- 你的分区写满了?系统自动给你换更大的分区页夹(
- 查朋友名单超快(遍历高效):
- 打开你自己的分区,里面的活页纸按添加顺序叠放(内存连续)。你可以一口气从头翻到尾,翻页动作流畅,CPU 缓存疯狂点赞!
- 用起来超简单(代码简洁):
- 加朋友?一句
graph[你].push_back(老王)
。查朋友?一个for
循环遍历graph[你]
。逻辑清晰,不易出错。
- 加朋友?一句
- 额外功能强大(STL支持):
- 想按关系亲密度排序朋友?直接用
sort(graph[你].begin(), graph[你].end())
。想找特定朋友?可以用find
。活页夹兼容各种“办公用具”(STL算法)。
- 想按关系亲密度排序朋友?直接用
缺点
- 文件夹本身有点重(内存开销大):
- 每个分区(
vector
)都要维护自己的“目录”(容量、大小等元数据)。朋友非常多时,比链式前向星多占 20%-50% 内存。
- 每个分区(
- 换分区页夹时手忙脚乱(扩容开销):
- 虽然系统自动帮你换,但换的那一刻(扩容)要把所有旧页复印到新分区,这时添加操作会短暂变慢(虽然均摊后很快,但有波动)。
- 撕掉某页很麻烦(删除边低效):
- 想删掉分区中间一页?你得把后面所有页往前挪一格(
vector
删除中间元素 O(度数))。除非你总是删最后一页(pop_back
)。
- 想删掉分区中间一页?你得把后面所有页往前挪一格(
- 分区分散(内存不连续):
- 你的分区、朋友A的分区、朋友B的分区在文件夹里可能不挨着。虽然每个分区内部连续,但访问不同节点时,CPU 可能要在文件夹里跳来跳去。
适合谁? 朋友数量不确定或经常变,追求代码好写、好读、易维护的“务实派”。绝大多数工程项目的首选。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1000; // 最大节点数// 定义边的结构体(比链式前向星更简单,不需要 next)
struct Edge {int to; // 这条边指向哪个节点int w; // 边权(可选)
};vector<Edge> graph[N]; // graph[u] 存储节点 u 的所有邻居// 添加一条从 u 到 v 的边,权重为 w
void addEdge(int u, int v, int w) {graph[u].push_back({v, w}); // 直接 push_back 即可
}// 遍历节点 u 的所有邻居
void printNeighbors(int u) {cout << "节点 " << u << " 的朋友: ";for (Edge e : graph[u]) {cout << e.to << "(亲密度:" << e.w << ") ";}cout << endl;
}int main() {// 添加边:0 -> 1 (权重 5), 0 -> 2 (权重 3), 1 -> 2 (权重 2)addEdge(0, 1, 5);addEdge(0, 2, 3);addEdge(1, 2, 2);// 打印邻居printNeighbors(0); // 输出: 节点 0 的朋友: 1(亲密度:5) 2(亲密度:3)printNeighbors(1); // 输出: 节点 1 的朋友: 2(亲密度:2)return 0;
}
关键点
graph[u]
是一个vector
,直接存储u
的所有邻居边。- 添加边直接用
push_back
,无需维护next
指针。 - 遍历时直接
for (Edge e : graph[u])
,比链式前向星更直观。
总结
总的来说,两款存图方式各有优劣,具体取决于需求和喜好