1. 邻接表与动态增边:为什么它如此重要?
如果你正在准备算法竞赛,或者在工作中需要处理一些网络关系、社交图谱、路径规划的问题,那你肯定绕不开“图”这个数据结构。图说起来挺抽象的,但你可以把它想象成一张人际关系网:每个人是一个“顶点”,人与人之间的认识关系就是一条“边”。现在,如果来了一个新朋友,并且他认识了好几个人,你怎么快速地把这些新关系(新边)加到你的关系网里呢?这就是我们今天要聊的“邻接表动态增边”。
很多教材和入门文章都会教你用邻接矩阵,也就是一个二维数组来存图。比如有5个人,就开一个5x5的表格,如果1号认识2号,就在表格的[1][2]位置标个1。这种方法对于查找两个人是否认识特别快,看一眼表格就行。但是,它的缺点也特别明显:太占地方了。想象一下,如果你的社交网络有10万人,但平均每个人只认识100个朋友,你需要一个10万乘以10万的巨大表格,里面绝大部分格子都是空的(表示不认识),这简直是内存的灾难。而且,当你需要遍历某个人的所有朋友时,你不得不把这10万个格子都检查一遍,哪怕他只有100个朋友。
这时候,邻接表的优势就体现出来了。它有点像一种“花名册”管理法。为网络里的每个人(每个顶点)建立一个单独的小名单(链表),名单上只记录他直接认识的朋友(相邻的顶点)。这样,存储空间只和实际的“认识关系”数量成正比,非常节省。遍历某个人的朋友时,也只需要看他的小名单,效率极高。而“动态增边”,就是当来了新的认识关系时,我们要快速、正确地把它更新到对应两个人的小名单里去。这个操作在算法题里是基本功,在工程里更是保证图数据实时性的关键。我刚开始学的时候,觉得不就是加个节点嘛,后来在项目里真用上了,才发现边界条件没处理好,整个图遍历都能出乱子,debug到头疼。所以,咱们今天不玩虚的,就扎扎实实用C++,把无向图邻接表的动态增边搞明白,让你写的代码既高效又健壮。
2. 从零开始:理解邻接表的核心结构
在动手写代码之前,我们得先在心里把邻接表这幅“图”画清楚。无向图意味着边是没有方向的,如果A和B之间有一条边,那么A在B的朋友名单里,B也一定在A的朋友名单里。这种对称性是我们实现增边操作时必须牢记的。
2.1 结构体定义:图的骨架
在C++里,我们通常用结构体来搭建邻接表。这里我分享一个我用了很多年的经典定义,它非常清晰,也容易扩展。
// 定义邻接表链表的节点
typedef struct LNode {
int vertex; // 存储邻接顶点的编号
struct LNode* next; // 指向下一个邻接节点的指针
} LNode, *LinkList;
// 定义图的结构
typedef struct {
int vexNum; // 顶点总数
int arcNum; // 边的总数
LinkList* adjList; // 邻接表数组。注意,这里是一个指针,指向一个LinkList数组。
} ALGraph;
我来解释一下这几个关键成员:
vexNum和arcNum:这两个整数是图的“元信息”,分别记录了图里有多少个顶点、多少条边。动态增边时,arcNum是需要更新的。adjList:这是整个结构的灵魂。它是一个指针,指向一个LinkList类型的数组。数组的长度是vexNum + 1(为了方便,我们通常让下标从1开始,符合题目习惯)。adjList[i]本身是一个链表的头指针(或者头节点),这个链表里就存储了所有与顶点i直接相连的邻居顶点。
这里有个初学者容易混淆的点:adjList 是一个指向指针数组的指针。adjList[i] 是数组的第i个元素,它的类型是 LinkList(即 LNode*),所以它本身就是一个指向链表第一个节点的指针。你可以把 adjList 想象成一本通讯录的目录页,目录的每一项(adjList[i])都指向了记录某人所有朋友的那一页名单。
2.2 内存布局可视化
让我们假设一个无向图:顶点1、2、3,初始有两条边 (1,2) 和 (2,3)。那么它的邻接表在内存里大概是这个样子:
adjList (数组,索引从1开始):
索引 [1] -> [节点|vertex=2|next] -> NULL
索引 [2] -> [节点|vertex=1|next] -> [节点|vertex=3|next] -> NULL
索引 [3] -> [节点|vertex=2|next] -> NULL
看到了吗?顶点2的链表有两个节点,分别指向顶点1和顶点3,这正反映了无向图中边的双向性。这种结构下,要查顶点2的所有邻居,我们只需要从头遍历 adjList[2] 指向的链表即可,非常高效。理解了这个内存模型,后面的插入操作就直观多了。


被折叠的 条评论
为什么被折叠?



