详细介绍:图论中邻接矩阵和邻接表详解

详细介绍:图论中邻接矩阵和邻接表详解

1. 图(Graph)的基本概念一个图 ( G = (V, E) ) 包含:

节点 / 顶点(Vertex):( V )边(Edge):( E )分为:

无向图有向图带权图(权重通常为距离、概率等)2. 邻接矩阵(Adjacency Matrix)2.1 定义邻接矩阵 (A) 对于有 (n) 个顶点的图是一个 (n\times n) 的二维数组。常见变体:

无向无权图:

(A[i][j] = 1) 表示存在边,(0) 表示无边。对称矩阵:(A[i][j] = A[j][i])。有向无权图:

(A[i][j] = 1) 表示从 (i) 指向 (j) 的有向边(不一定对称)。带权图(有向或无向):

(A[i][j] = w_{ij})(例如边长、代价、相似度)。无边处通常用特殊值表示(0、INF、或 NaN,取决于语义)。自环:如果允许自环,则对角线 (A[i][i]) 可为 1 或权值。稀疏矩阵 vs 密集矩阵:邻接矩阵天然是密集表示;当 (m \ll n^2)(稀疏图)时它并非空间友好。无边的表示(实务)带权最常用:A[i][j] = +Inf(用 std::numeric_limits::infinity()),这样在最短路算法中方便跳过;也可用一个布尔 mask 表示存在性与否。无权场景下可用 0/1,但当 0 可能是合法权重时需小心。2.2 图示例举例扩展:带权有向图的邻接矩阵示例(设无边用 INF):

0 1 2 3

0 0 2.5 INF INF

1 INF 0 1.0 INF

2 INF INF 0 3.2

3 INF INF INF 0

该矩阵直接可用于 Floyd–Warshall(全源最短路),矩阵自底向上更新 dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])。

2.3 内存复杂度空间复杂度:(O(n^2))。

若使用 double 存权值:内存约 8 * n^2 bytes。

例如 (n=10000) 时:8 * 1e8 = 800MB(仅矩阵)。若使用 bool 或位图(bitset):可以压缩到约 n^2 / 8 bytes——仍会很快变大。工程建议

(n \lesssim 10^4) 且图稠密:可用密集邻接矩阵。(n) 很大且稀疏(如 SLAM 的变量图通常 (m = O(n))):不要用邻接矩阵,应使用邻接表或稀疏矩阵(CSR/CSC)。2.4 典型操作复杂度检查边(i,j):O(1) —— 直接访问 A[i][j]。遍历 i 的所有邻居:O(n) —— 扫一整行。稀疏情况下这是低效的(很多是0/INF)。插入/删除边:O(1) —— 直接写入/置零。增添节点:需要重新分配并复制整个 (n\times n) 矩阵:O(n^2) —— 非常昂贵。矩阵乘法 / 幂:用于计数长度为 k 的路径(A^k)等,可用线性代数库(复杂度 (O(n^3)) 或更好并行实现)。2.5 优点O(1) 边查询:适合需要频繁判断顶点对是否相连的场景(例如动态验证全连通性或稠密相似度矩阵)。适合线性代数处理:可以直接用 BLAS / GPU(cuBLAS / cuSPARSE)进行并行运算(矩阵乘法、特征分解)。简单且常用于理论分析:很多代数图理论(图谱、拉普拉斯矩阵)都基于邻接矩阵或其变体(度矩阵、拉普拉斯)。2.6 缺点空间没处可逃:稀疏图会浪费大量内存。遍历邻居代价高:即使实际度很小,也要扫描整个行。动态调整困难:添加/删除大量节点/重编号代价高。缓存局部性:虽然矩阵按行存储在内存中,连续访问行很快,但若算法需要随机访问多行/列会出现缓存不友好问题。2.7 C++ 示例1) 固定大小的静态数组适合小图、最高性能需求,但不灵活:

const int N = 100;

std::array, N> adj{}; // 或静态 int adj[N][N]

adj[u][v] = 1;

注意:std::vector> 会额外分配很多小块,导致性能下降;优先使用一维连续数组模拟二维(vector data(n*n); data[i*n + j])以获得更好缓存。

2) 动态大小

struct AdjMatrix {

size_t n;

std::vector data; // 使用 double 表示权值(无边用 INF)

static constexpr double INF = std::numeric_limits::infinity();

AdjMatrix(size_t n_) : n(n_), data(n_*n_, INF) {

for(size_t i=0;i

}

inline double& at(size_t i, size_t j) { return data[i*n + j]; }

inline bool hasEdge(size_t i,size_t j) { return data[i*n + j] != INF; }

std::vector neighbors(size_t i) {

std::vector nb;

for(size_t j=0;j

return nb;

}

};

优势:一维连续存储,友好缓存;方便序列化与 GPU 拷贝。

注意:vector 不建议用于位图(语义/效率问题),若需要位图请用 std::vector 或 boost::dynamic_bitset。

3) 使用位矩阵(节省空间、加速位运算)当图是无权且只需存在性测试,可以用位矩阵(每行为 bitset),支持快速交并运算(例如计算公共邻居,Jaccard 相似度):

#include // 若 n 编译时已知

std::vector> bit_rows; // 行压缩为 uint64_t 单元

// 测试公共邻居数

uint64_t count_common(size_t i, size_t j) {

uint64_t sum = 0;

for(k...) sum += __popcnt64(rowi[k] & rowj[k]);

return sum;

}

位运算非常适合 GPU/向量化加速与大量相似度计算(例如 loop closure 相似度矩阵)。

4) 用线性代数库(如 Eigen)带权图且需要做谱分解 / 特征值运算时,使用 Eigen / Armadillo:

#include

Eigen::MatrixXd A(n, n);

// fill A

Eigen::SelfAdjointEigenSolver solver(A);

auto evals = solver.eigenvalues();

注意:Eigen 的 MatrixXd 是 row-major 或 col-major 可配置;与你内存布局要匹配以免额外拷贝。

附:邻接矩阵的常见优化与变体只存上(三角)或下三角(对称无向图):节省一半空间,但访问需做索引变换。压缩行(CSR / CSC):虽然这本质是稀疏矩阵格式,若邻接矩阵非常稀疏,可以把邻接矩阵视作稀疏矩阵存储,利用 row_ptr, col_idx, val 来节省空间并加快遍历邻居(与邻接表类似但更便于数值运算)。位图 + 权值分离:存在性用位图,权值另存(仅保存实际边)。对查询速度与存储权衡有利。Block-sparse / Tiled:把大矩阵分成块,适合 GPU / 分布式场景(按块分配可加速并行乘法与存取)。在 SLAM / 图优化 中的实践提示因子图(GTSAM)通常稀疏 → 使用邻接表或稀疏矩阵(CSR)更合适。邻接矩阵只在特定阶段(例如全连接相似度计算、BoW 两两相似计算)临时使用。回环候选相似度(全配对):若需要两两相似度矩阵(例如 10k 帧),可用 位矩阵 + GPU 并行 计算快速过滤候选,再细化用稀疏结构。图谱方法(谱聚类、拉普拉斯特征):需要密集矩阵或稀疏线性代数库支持(Eigen / Spectra / ARPACK)。网格地图(occupancy grid):本质上是一个稠密的二维邻接矩阵(每个 cell 与邻居相连),这里邻接矩阵/邻接格点表示都常用,且非常适合 GPU。常见误区与陷阱用 vector> 表示邻接矩阵会产生大量小内存分配,影响性能;优先用 vector 一维数组映射 2D 索引。用 vector 作为位图时语义/性能会受损(vector 为特殊化实现),建议用 std::bitset(编译期大小)或 boost::dynamic_bitset,或自己用 vector。混合使用多处 Eigen 版本或旧 Eigen 可能导致 CUDA 编译失败(你之前遇到的问题),务必保证 include 顺序与版本一致。若算法频繁增删节点,应避免邻接矩阵。实用算法示例(邻接矩阵场景优先)Floyd–Warshall(全源最短路):直接在邻接矩阵上三重循环 O(n^3)。路径计数(A^k):矩阵幂可以求长度恰为 k 的路径数量,可用快速幂或并行矩阵乘法。谱聚类 / 特征值分解:从邻接矩阵构造度矩阵 (D) 与拉普拉斯 (L = D - A),然后求前 k 个特征向量。快速公共邻居 / 相似度:若用位矩阵,公共邻居数可通过位与 + popcount 迅速计算;适用于 loop closure 提名。3. 邻接表(Adjacency List)3.1 定义邻接表为每个顶点维护一个容器(vector、list、deque、甚至链表索引数组),存储该顶点的所有邻居(以及可选的边属性,如权重、时间戳、信息矩阵等)。

常见表示:

vector>:最常用,连续内存,缓存友好。vector>>:带权图,pair。Forward-star / CSR-like:用三数组 head[], to[], next[] 或 offset[], to[],避免大量小分配,内存紧凑,适合静态图/高性能场景。vector> 或 vector:用于需要 O(1) 边查找且允许动态增删。注意:邻接表按顶点维护“出边列表”(directed)或“邻居列表”(undirected,存双向或半存储)。

3.2 图示例同样图 0–1–2–3:

vector> adj 表示:

adj[0] = {1}

adj[1] = {0,2}

adj[2] = {1,3}

adj[3] = {2}

带权例子(vector>>):

adj[1] = { {0,0.8}, {2,1.2} }

Forward-star(数组表示)示意:

head[0] = 0; head[1]=1; head[2]=3; head[3]=5; head[4]=6 (head[n]=m)

to = [1, 0,2, 1,3, 2]

offset method: neighbors of u are to[offset[u] .. offset[u+1]-1]

3.3 内存复杂度空间复杂度:(O(n + m)),更精确:

使用 vector>:节点数组 n + 所有邻节点存储 2m(无向双向存储)元素 + 每个内层 vector 的元数据(约 3 pointers 每个);使用 CSR/offset:offset 长度 n+1,to 长度 m(或 2m),极佳压缩比;工程量化示例:

(n=10^6, m=5\times10^6)(稠密度低):

vector> 可能因每个 vector 分配开销显著;CSR 存储 offset + to 仅需 ~ 8*(n+1) + 4*m bytes(假设 4 字节 int),非常节省。3.4 操作复杂度操作邻接表(vector 列表)判断边 (i,j)O(k)(k = deg(i)),若用 hash 集合则 O(1) 平均遍历邻居O(k)插入边平均 O(1) (push_back,若内存扩容则摊销)删除边O(k)(若不知道位置),可用 swap-erase 达到 O(1)(但破坏顺序)增加节点O(1)(adj.push_back({}))静态构建(一次性填充)O(m)(如果预分配)细节:

删除具体边:若保持邻接顺序,可 erase(iterator),为 O(k);若不在意顺序,可 swap 与 pop_back() 实现 O(1)。快速判断边存在性:在 adj[u] 使用 unordered_set 或排序后用二分查找(O(log k))。3.5 优点节省空间:对稀疏图最优。遍历邻居高效:遍历速度与实际邻居数成正比。易动态修改:添加节点/边、增删动态方便。适配多种边属性:可直接在邻居项中加权值、时间戳、covariance 等结构体字段,便于 SLAM 中记录多模态信息(例如 loop confidence、information matrix)。与稀疏线性代数相配:生成稀疏矩阵(CSR)容易,用于稀疏优化。3.6 缺点查边非 O(1),除非使用额外结构(hash)。内存碎片与 allocator 问题:大量小 vector 分配会导致内存碎片、性能下降 —— 在大规模工程中推荐使用 reserve 或统一内存池。不适合非常稠密图:若边接近 (n^2),邻接表会有极多小数组,CSR/矩阵更合适。并发写入复杂:并发 add/remove 需要锁或原子操作,设计需谨慎。3.7 C++ 示例与工程实现变体下面给出多个工程可用的数据结构实现,以及并发 / 高性能 / 序列化 / 转换代码片段。

A. 最简单(开发与教学用)

int n = 100;

std::vector> adj(n);

// add undirected edge

void add_edge(int u, int v) {

adj[u].push_back(v);

adj[v].push_back(u);

}

// iterate

for (int v : adj[u]) {

// ...

}

问题:每个 push_back 可能会引起 reallocation,建议 reserve。

B. 带权、带属性(常用 SLAM)

struct Edge {

int to;

double weight;

uint64_t timestamp;

// optional: information matrix index or small fixed-size array

};

std::vector> adj;

适合在每条边上存储 covariance/information/score,直接用于 loop-closure 筛选和后端因子构造。

C. 前置分配 + swap-erase 删除(高性能)

// reserve neighbor lists

adj[u].reserve(expected_deg[u]);

// O(1) remove (unordered)

void remove_edge_unordered(int u, int v) {

auto &list = adj[u];

for (size_t i = 0; i < list.size(); ++i) {

if (list[i] == v) {

list[i] = list.back();

list.pop_back();

break;

}

}

}

优点:删除 O(1),代价是破坏邻居顺序(通常可接受)。

D. 使用 unordered_set 实现 O(1) 存在性检查

std::vector> adj_set; // 或 std::unordered_set

bool has_edge(int u, int v) {

return adj_set[u].find(v) != adj_set[u].end();

}

void add_edge(int u, int v) {

adj_set[u].insert(v);

adj_set[v].insert(u);

}

权衡:更高内存开销,但查询快。适合需要频繁查边(而非遍历邻居)的场景(如动态约束冲突检测)。

E. CSR / Offset 表示(静态图或批量构建首选)构建步骤:

统计每个节点度 deg[u]offset[0]=0; offset[i+1]=offset[i]+deg[i]填充 to[offset[u] .. offset[u+1]-1]代码片段(构建):

std::vector deg(n,0);

for(auto &e: edges) {

deg[e.u]++; deg[e.v]++;

}

std::vector offset(n+1);

for(int i=0;i

std::vector to(offset.back());

std::vector cur = offset;

for(auto &e: edges) {

to[cur[e.u]++] = e.v;

to[cur[e.v]++] = e.u;

}

优点:内存紧凑、遍历邻居更快(连续内存)、方便并行和 GPU 拷贝。缺点:不支持高效随机增删。

F. Forward-star(链式数组,节省指针)常见于竞赛/嵌入式:

const int MAXM = ...;

int head[N], to[MAXM], next[MAXM], cnt=0;

void add_edge(int u,int v) {

to[++cnt] = v; next[cnt] = head[u]; head[u] = cnt;

}

优点:低开销,适合静态或构建一次后多查询场景。缺点:删除复杂。

G. 并发场景(多线程读/写)读多写少:读无需锁,写时对单个 adj[u] 使用 mutex 或原子操作。高并发写入:使用分段锁(per-vertex mutex)或 lock-free structures(复杂)。构建阶段并行:用线程局部 buffer 收集边,最后合并到主结构(避免锁竞争)。示例(per-vertex mutex):

std::vector vertex_mutex(n);

void thread_safe_add_edge(int u,int v) {

std::scoped_lock lock(vertex_mutex[u], vertex_mutex[v]);

adj[u].push_back(v);

adj[v].push_back(u);

}

H. GPU / 大规模并行处理把邻接表转成 CSR (offset + to arrays),上传到 GPU;在 kernel 中用 offset[u]..offset[u+1] 范围遍历;位图表示(bitset)也适合 GPU 用于快速并行相似度计算(popcount)。注意 host->device 的内存对齐与数据类型大小(use 32-bit indices if possible)。I. 序列化 / 存储(磁盘 / 网络)CSR 更适合序列化(写出 offsets, to arrays)。建议写版本号、n、m、offset[], to[], weights[],按二进制写入加速。对于非常大的图(>RAM),使用外存图数据库(GraphChi/Neo4j)或 memory-mapped files。J. 转换:邻接矩阵 ↔ 邻接表矩阵 -> 邻接表:对每行扫描 O(n^2);稀疏时代价高。邻接表 -> CSR:O(n+m) 通过前述 degree->offset 构建步骤。并发/性能优化建议预分配(reserve):如果能估计度数,请 reserve 每个 vector,避免多次 reallocation。使用连续内存:如果追求最高性能,优先使用 CSR 或 flat_vector(自实现一维数组 + offset)而不是 vector>。内存对齐与类型:顶点索引用 uint32_t 优于 uint64_t(节省空间),除非 n>4e9。定期压缩/整理:对动态图,周期性合并/compact 邻接列表以减少碎片。访问模式友好:尽量按顶点顺序访问以提高 cache hit(对 BFS/DFS 有益)。使用自定义 allocator:避免小块频繁分配的开销(你之前感兴趣的 static memory pool 很适合这里)。在 SLAM / 图优化 / 路径规划 中的具体应用建议SLAM 因子图:因是稀疏且动态(随着关键帧增长),邻接表或 CSR(周期性 rebuild)为佳。每个邻接项存 factor id /信息矩阵索引 -> 在构造线性化矩阵时可直接映射到稀疏 Hessian。回环候选管理:把回环置信度、检测时间、关键点数存到边属性,快速筛选用 adj[u] 遍历并用阈值过滤。A*:邻接表加权图 + 优先队列实现,遍历邻居为 O(k) 最佳。增量优化:支持快速插入/删除边(swap-erase),并在后端维护稀疏矩阵增量更新。推荐的工程级模板类这是一个工程级的邻接表类草案(支持带权、reserve、swap-erase、CSR 导出):

template

class AdjacencyList {

public:

using Edge = std::pair;

AdjacencyList(size_t n=0) { resize(n); }

void resize(size_t n) {

adj.resize(n);

}

size_t size() const { return adj.size(); }

void reserve_node(size_t u, size_t deg) {

adj[u].reserve(deg);

}

void add_edge(int u, int v, const EdgeAttr& attr = EdgeAttr()) {

adj[u].emplace_back(v, attr);

}

// undirected convenience

void add_undirected(int u,int v,const EdgeAttr& attr=EdgeAttr()){

add_edge(u,v,attr); add_edge(v,u,attr);

}

// unordered remove (O(1))

bool remove_edge_unordered(int u, int v) {

auto &list = adj[u];

for(size_t i=0;i

if(list[i].first==v) {

list[i] = list.back();

list.pop_back();

return true;

}

}

return false;

}

// export CSR

void to_csr(std::vector& offset, std::vector& to) const {

int n = (int)adj.size();

offset.assign(n+1,0);

for(int i=0;i

to.resize(offset.back());

std::vector cur = offset;

for(int i=0;i

for(auto &e: adj[i]) to[cur[i]++] = e.first;

}

}

const std::vector& neighbors(int u) const { return adj[u]; }

private:

std::vector> adj;

};

4. 邻接矩阵 vs 邻接表对比总结项目邻接矩阵邻接表内存O(n²)O(n + m)图类型稠密图稀疏图查边✔ O(1)✘ O(k)遍历邻居O(n)O(k)增删节点❌ 难✔ 简单适用算法Floyd, DPBFS/DFS, Dijkstra, 图优化5. 在 SLAM 与图优化中的应用邻接表 —— 因子图最常用的数据结构GTSAM、Ceres、SLAM 后端优化中:

图是稀疏的每个节点仅连接少量因子使用邻接表表示 Factor Graph

示例(gtsam):

x0 → (imu, odom)

x1 → (imu, loop)

x2 → (odom)

...

每个变量只连接少量因子,所以邻接表最适合。

邻接矩阵 —— 回环检测与特征匹配中常见例如:

BoW 词典相似度矩阵(ORB-SLAM)GNSS / WiFi RTI 指纹图GPU 加速 pairwise 关系计算6. 什么时候用哪一种场景推荐结构SLAM 图优化(Factor Graph)邻接表稠密图(社交网络、全连接图)邻接矩阵BFS / DFS邻接表Floyd 全源最短路邻接矩阵Dijkstra 稠密图邻接矩阵Dijkstra 稀疏图邻接表 + 堆GPU 并行计算邻接矩阵路径规划(地图网格)邻接表(隐式图)

相关推荐

linux常用命令(超详细)查看服务器日志的各种命令
www.365bet.com亚洲版

linux常用命令(超详细)查看服务器日志的各种命令

📅 08-05 ⭐ 3396
梦幻西游山河画境单人攻略,门派选择、打法技巧及奖励领取
揭秘Android系统:如何轻松修改电池信息,解锁手机潜能!
在 Mac 上的 Pages 文稿中更改文本颜色或添加渐变
office365输入账号

在 Mac 上的 Pages 文稿中更改文本颜色或添加渐变

📅 08-01 ⭐ 2560
真实医生回答
www.365bet.com亚洲版

真实医生回答

📅 02-17 ⭐ 9899
限免的意思
www.365bet.com亚洲版

限免的意思

📅 07-02 ⭐ 5991
推荐阅读 ❤️