想象你是一座陌生城市的游客,手里只有一张地铁线路图。你站在“市中心站”,想去“科技园站”。如果按照线路序号一条条找,可能会绕远路;如果你只想知道“哪些站点可以不用换乘到达”,又是一个问法;如果你想知道“从市中心出发,哪站是交通枢纽中的枢纽”,那又是另一个问题。这些不同的问法,本质上都指向同一个动作——在这张由“站点”和“线路”组成的网络上移动。这个动作,就是图遍历。 图数据库的世界里,数据被存储为“节点”(比如一个人、一个商品)和“边”(比如“关注”、“购买”)。图遍历算法,就是在这张网络上系统化“走一遍”的方法论。它决定了你如何从一个节点出发,找到朋友的朋友、发现商品之间的关联、甚至算出整个网络中谁最重要。这篇文章将用一张“城市地铁图”作为贯穿始终的比喻,带你理解 BFS、DFS、Dijkstra 最短路径和 PageRank 中心性这四大遍历思想,并看看它们在宽列存储和大规模分布式环境下面临的挑战。

岗位关联

掌握图遍历算法,意味着你不仅会“存”数据,更懂得如何“用”数据——这是从事社交推荐、知识图谱构建、反欺诈分析等 AI 应用场景的核心基本功,也是区分初级 CRUD 工程师与高级数据架构师的关键分水岭。

目录
  1. 可视化演示
  2. 图的存储结构
  3. 广度优先搜索
  4. 深度优先搜索
  5. Dijkstra算法
  6. PageRank中心性
  7. 分布式遍历挑战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

图的存储结构

拿到一张地铁图,你第一件事是看懂它怎么画的——站点用圆点表示,线路用线段连接。在计算机里,这张图也需要一种“画法”,也就是存储结构。最直观的方式有两种:邻接表和邻接矩阵。

邻接表就像你手机里的通讯录。每个联系人(节点)的名字后面,跟着一串他认识的人(邻居节点)。要查“张三认识谁”,直接翻到张三那一页就行,非常高效。邻接矩阵则像一张班级座位表——行和列都是全班同学的名字,如果第 i 行第 j 列打了个勾,就说明 i 认识 j。这张表查起来极快(看一眼行列交叉处就知道有没有关系),但如果班级有 1000 人,这张表就要有 100 万个格子,大多数格子还空着,浪费空间。

宽列数据库(如 HBase、Cassandra)天然适合存储邻接表——每一行以节点 ID 作为行键,列族里存储所有邻居节点。当图数据规模达到千万级节点时,邻接矩阵所需的存储空间是天文数字,而邻接表只存储真实存在的关系,空间效率高出几个数量级。

1. 节点表示:每个节点(地铁站)在宽列存储中对应一行数据。

row_key=node_id\text{row\_key} = \text{node\_id}

2. 邻居列表:节点的所有出边(从该站出发能到达的站)存储在同一个列族中。

column=neighbor_id,value=edge_weight\text{column} = \text{neighbor\_id},\quad \text{value} = \text{edge\_weight}

3. 空间复杂度对比:邻接矩阵需要 \(V^2\) 个格子,邻接表只需要 \(V+E\) 个存储单元。

Matrix: O(V2)vsList: O(V+E)\text{Matrix: } O(V^2) \quad \text{vs} \quad \text{List: } O(V+E)

回到地铁图:现实中的地铁网络每条线路只连接相邻几站,没有哪条线会从“市中心站”直接连到所有其他站。这就是“稀疏图”——邻接表正是为这种场景量身定做的通讯录。而宽列存储把“通讯录”按行组织,让遍历算法在读取某个节点的全部邻居时,只需一次行读取,效率远高于传统关系型数据库的多次 JOIN 操作。

text6 行
邻接表: {A: [B, C], B: [A, C, D], C: [A, B, E], ...}邻接矩阵:  A  B  C  D        A [0, 1, 1, 0]        B [1, 0, 1, 1]        C [1, 1, 0, 1]        D [0, 1, 1, 0]

广度优先搜索

想象你把一块石头扔进平静的湖面,涟漪从落水点开始,一圈一圈向外扩散。第一圈覆盖“一步就能到达”的区域,第二圈覆盖“两步才能到达”的区域。广度优先搜索(BFS)就是这种“水波式”的遍历:从起点出发,先访问所有直接邻居,再依次访问这些邻居的邻居,层层推进。

在计算机里,BFS 靠一个“队列”来维持遍历顺序。队列就像地铁站的候车通道——先进入的人先上车,先进先出(FIFO)。算法将起点放入队列,然后循环:从队首取出一个节点,访问它,把它的所有未访问邻居排到队尾。这个过程保证了你总是先处理“距离起点更近”的节点,再处理更远的节点。

回到湖面的涟漪——每一圈水波都代表了“从落水点出发,走 k 步能到达的所有位置”。BFS 在社交网络中的典型应用是“我认识的人里,谁和某个陌生人只隔了 3 层关系”?LinkedIn 的“二度人脉”推荐、Facebook 的“可能认识的人”,底层都是 BFS 在奔跑。它找到的路径,在“边的数量最少”这个意义下一定是最短的。

BFS(s):dist[s]=0, queue=[s];while queue not empty: u=dequeue; for each v∈neighbors(u): if dist[v]=∞: dist[v]=dist[u]+1; enqueue(v)\text{BFS}(s):\quad \text{dist}[s] = 0,\ \text{queue} = [s];\quad \text{while queue not empty:}\ u = \text{dequeue};\ \text{for each } v \in \text{neighbors}(u):\ \text{if } \text{dist}[v] = \infty:\ \text{dist}[v] = \text{dist}[u] + 1;\ \text{enqueue}(v)
公式说明:dist[v] 记录从起点到 v 的最少步数。由于队列先进先出的特性,第一次访问 v 时走过的路径一定是最短跳数路径。
  1. 初始化将起点 s 的距离设为 0,表示“从起点到起点需要 0 步”,放入队列。
    dist[s]=0,queue←[s]\text{dist}[s] = 0,\quad \text{queue} \leftarrow [s]
  2. 取出节点从队列头部取出一个节点 u,这就是当前“水波”到达的位置。
    u=dequeue(queue)u = \text{dequeue}(\text{queue})
  3. 扩展邻居遍历 u 的所有邻居 v,如果 v 还没被访问过(距离还是无穷大),说明这是第一次“碰到”它。
    if dist[v]=∞: dist[v]=dist[u]+1\text{if } \text{dist}[v] = \infty:\ \text{dist}[v] = \text{dist}[u] + 1
  4. 入队等待将 v 放入队尾,等待后续处理它的邻居。
    enqueue(v)\text{enqueue}(v)

深度优先搜索

有一种探险家的遍历方式:选一条路,一路走到黑,撞了南墙才回头。深度优先搜索(DFS)就是这样的策略——它不满足于浅尝辄止,而是沿着一条路径不断深入,直到无法继续,然后回溯到最近的分岔口,换一条路继续深入。

DFS 的实现依赖“栈”结构——后进先出(LIFO),就像一叠盘子,你总是先拿走最上面那个。递归是 DFS 最自然的表达形式:进入一个节点,递归地访问它的第一个未访问邻居,等那一整条分支走完,再回到上一层继续。也可以用显式栈模拟递归,避免深度过大时栈溢出。

地铁比喻的另一种玩法:DFS 就像一个强迫症游客,拿到地图后坚持“每条支线都要走到底再回来”,虽然走的路径可能绕远,但能完整地探索每个角落。DFS 在工程中的典型应用是检测图中是否存在环(比如检测课程先修关系是否成死循环)、拓扑排序(安排课程学习顺序)、以及判断两个节点是否连通(迷宫求解)。它不需要维护距离信息,空间开销通常比 BFS 更小。

DFS(u):visited[u]=true;for each v∈neighbors(u): if not visited[v]: DFS(v)\text{DFS}(u):\quad \text{visited}[u] = \text{true};\quad \text{for each } v \in \text{neighbors}(u):\ \text{if not } \text{visited}[v]:\ \text{DFS}(v)
公式说明:DFS 不追求“最短”,只追求“完整覆盖”。它访问节点的顺序取决于邻居的排列顺序,每次都会优先深入某一条分支到底。
  1. 标记已访问进入节点 u,立刻标记为已访问,防止后续重复进入。
    visited[u]=true\text{visited}[u] = \text{true}
  2. 遍历邻居依次检查 u 的每个邻居 v。
    for each v∈neighbors(u)\text{for each } v \in \text{neighbors}(u)
  3. 递归深入如果 v 没被访问过,就递归地对 v 执行同样的过程——这就是“一路走到黑”。
    if not visited[v]: DFS(v)\text{if not } \text{visited}[v]:\ \text{DFS}(v)

Dijkstra算法

地铁图上的线路长度各不相同,从 A 站到 B 站可能只要 2 分钟,到 C 站却要 15 分钟。如果只关心“经过几个站”,BFS 就够了;但现实中的“路径最短”往往指的是“总耗时最少”。Dijkstra 算法就是为这种带权重的图设计的寻路算法。

Dijkstra 的核心思想是“贪心”——每次都从当前已知的所有候选路径中,选择总距离最短的那一条进行扩展。算法维护一个“已确定最短距离”的集合,和一张“当前已知最短距离”的表。每次从未确定的节点中挑出距离最小的那个,把它标记为确定,然后更新它所有邻居的距离——如果经过这个节点到达邻居比之前的路径更短,就更新邻居的距离。

把你上班的通勤路线想成一次 Dijkstra:地铁每段的行驶时间是边的权重,换乘等待时间也是权重。Google Maps 的路线规划、Uber 的派单调度、物流公司的配送路径优化,背后都有 Dijkstra 或其变体(如 A* 算法)在运行。它和 BFS 的区别在于——BFS 假设每条路一样长,而 Dijkstra 尊重每条路的“真实代价”。

Dijkstra(s):dist[s]=0, others=∞;S=∅;while S≠V: u=arg⁡min⁡v∉Sdist[v]; S=S∪{u}; for each v∈neighbors(u): dist[v]=min⁡(dist[v], dist[u]+w(u,v))\text{Dijkstra}(s):\quad \text{dist}[s] = 0,\ \text{others} = \infty;\quad S = \emptyset;\quad \text{while } S \neq V:\ u = \arg\min_{v \notin S} \text{dist}[v];\ S = S \cup \{u\};\ \text{for each } v \in \text{neighbors}(u):\ \text{dist}[v] = \min(\text{dist}[v],\ \text{dist}[u] + w(u,v))
公式说明:w(u,v) 是边 (u,v) 的权重(如时间、距离、成本)。Dijkstra 要求所有边权非负——因为一旦允许负权边,贪心策略就会失效。
  1. 初始化距离起点 s 的距离为 0,其他所有节点距离为无穷大。
    dist[s]=0,dist[v]=∞ (v≠s)\text{dist}[s] = 0,\quad \text{dist}[v] = \infty\ (v \neq s)
  2. 选择当前最近节点在尚未确定最短距离的节点中,选出 dist 最小的那个 u。这是贪心策略的关键动作。
    u=arg⁡min⁡v∉Sdist[v]u = \arg\min_{v \notin S} \text{dist}[v]
  3. 标记确定将 u 加入已确定集合 S。一旦确定,dist[u] 就是真正的全局最短距离,不会被后续更新。
    S=S∪{u}S = S \cup \{u\}
  4. 松弛操作检查 u 的每个邻居 v,如果通过 u 到 v 的路径比当前记录的更短,就更新。
    dist[v]=min⁡(dist[v], dist[u]+w(u,v))\text{dist}[v] = \min(\text{dist}[v],\ \text{dist}[u] + w(u,v))

PageRank中心性

前面几种算法回答的是“怎么走”的问题,而 PageRank 回答的是“谁最重要”的问题。想象一个社交网络里,判断一个人是否重要,光看粉丝数不够——如果你有 1000 个粉丝但都是僵尸号,和一个有 100 个粉丝但都是行业大 V 的人相比,后者在网络上更有影响力。PageRank 正是捕捉这种“被重要的人关注,所以你也很重要”的递归逻辑。

PageRank 的数学模型来自“随机游走”:假设一个用户随机点击链接,从当前网页跳到下一个网页。经过足够多次跳转后,用户停留在每个网页上的概率,就是该网页的 PageRank 值。这个值越高,说明网页被访问的概率越大,也就越“重要”。计算方式是迭代的:每一轮,每个节点把自己当前的权重平均分给所有出边邻居,同时接收所有入边邻居分来的权重。

回到城市的比喻:PageRank 是在回答“哪些地铁站是真正的交通枢纽”。一个站即使连接线路不多,但如果它是核心换乘站(像北京西直门),它的重要性会通过“被其他重要站点连接”而放大。Google 当年靠 PageRank 在搜索质量上碾压对手,今天它被广泛应用于社交网络的影响力分析(找到意见领袖)、反欺诈(发现异常密集的资金往来节点)、以及知识图谱中的实体重要性排序。

PR(u)=(1−d)+d∑v∈in(u)PR(v)∣out(v)∣\text{PR}(u) = (1-d) + d \sum_{v \in \text{in}(u)} \frac{\text{PR}(v)}{|\text{out}(v)|}
公式说明:这就像一场“重要性投票”——每个节点把自己手中的票(权重)分给所有指向的人,而被重要节点投出的票,含金量更高。迭代多轮后,权重分布收敛到稳定值。
  1. 权重传播每个节点 v 将自己的 PageRank 值平均分给它的所有出边邻居。
    PR(v)∣out(v)∣\frac{\text{PR}(v)}{|\text{out}(v)|}
  2. 累加入链节点 u 的 PageRank 值等于所有指向它的节点(入链邻居)分配的权重之和。
    ∑v∈in(u)PR(v)∣out(v)∣\sum_{v \in \text{in}(u)} \frac{\text{PR}(v)}{|\text{out}(v)|}
  3. 阻尼因子d(通常取 0.85)模拟用户“继续点击”的概率,(1-d) 表示用户随机跳到任意新页面的概率。
    PR(u)=(1−d)+d×(入链贡献和)\text{PR}(u) = (1-d) + d \times (\text{入链贡献和})

分布式遍历挑战

当图数据增长到十亿节点、百亿边时,单台机器的内存已经无法容纳整张图。怎么办?把图切碎,分布到几十台甚至上千台机器上。但“切碎”本身带来一个棘手的问题:原本相邻的节点,现在住在不同的机器上。

数据分片主要有两种策略:按节点 ID 哈希分片(把 A 的邻居 B 分到另一台机器),或者按社区/子图分片(尽量把联系紧密的节点放在同一台机器)。前者负载均衡,但遍历时需要频繁跨机器通信——每访问一个邻居,如果它不在本地,就要发一次网络请求。后者能减少通信,但可能造成某些机器负载过高。

这就像你要画一张全国地铁图——但每个城市的地图分别由不同的人保管。要回答“从北京到上海坐地铁怎么走”,你得不断打电话向上海那边的同事确认“下一站在你那儿吗?”每打一次电话就有一秒延迟,路线越长、涉及城市越多,电话费越贵。这正是 GraphLab、Pregel 等分布式图计算框架试图解决的问题:通过批量同步(BSP 模型)和减少通信轮次来摊薄开销,让“打电话”的次数尽量少。

通信成本=O(跨分区边数×网络往返延迟)≈遍历过程中最大的性能瓶颈\text{通信成本} = O(\text{跨分区边数} \times \text{网络往返延迟}) \approx \text{遍历过程中最大的性能瓶颈}
公式说明:分布式环境中,网络通信比内存访问慢数个数量级。一次跨机器访问的延迟(毫秒级)是本地内存访问(纳秒级)的百万倍。
  1. 本地计算优先每台机器先遍历存储在本地的那部分节点和边。
    local_step=遍历本地子图\text{local\_step} = \text{遍历本地子图}
  2. 跨机消息传递当遍历到一条边的另一端不在本机时,需要将“当前状态”打包发送到对方机器。
    remote_step=消息发送+等待响应\text{remote\_step} = \text{消息发送} + \text{等待响应}
  3. 迭代收敛代价像 PageRank 这样的迭代算法,每轮全局迭代都需要所有机器同步一次,等待最慢的那台。
    total_time=iterations×round_time\text{total\_time} = \text{iterations} \times \text{round\_time}

AI 应用场景

社交推荐系统

阶段:特征工程与召回

解决的问题:传统协同过滤只能基于用户-物品二部图做浅层匹配,无法捕捉“朋友的朋友喜欢什么”这类多跳关系。BFS 遍历能在毫秒级找到用户二度、三度网络内的兴趣热点,显著提升召回质量。

具体案例:LinkedIn 的“可能认识的人”功能。当用户注册后,系统以该用户为起点执行 BFS,遍历到二度人脉(朋友的朋友),再按共同好友数排序推荐。整个过程需要在分布式图存储上每秒执行数十万次 BFS 查询。

金融反欺诈检测

阶段:图特征提取与异常检测

解决的问题:欺诈团伙往往通过复杂的转账网络洗钱,单笔交易看起来正常,但整个网络的结构异常。通过 PageRank 中心性计算,可以识别出资金网络中“异常重要”的节点——它们可能是资金归集账户。

具体案例:某银行构建了包含 5 亿节点、20 亿边的大规模转账图谱。每天运行一次 PageRank 迭代计算,标记中心性显著高于正常水平的账户,再结合规则引擎人工审核。该系统将团伙欺诈的召回率提升了 40%。

知识图谱问答

阶段:知识检索与推理路径生成

解决的问题:用户提问“乔布斯创办了哪家公司”,知识图谱中需要从实体“乔布斯”出发,沿着“创办”关系边遍历到目标实体。Dijkstra 算法可以找到语义上最相关的关系路径,避免被无关关系干扰。

具体案例:搜索引擎的知识卡片功能。当用户搜索一个实体时,系统在知识图谱上执行带权重的图遍历——关系越紧密(权重越小)的边优先被遍历,最终返回最相关的实体和关系路径作为答案。

Python 演示

python62 行
import heapqfrom collections import deque  class GraphTraversal:    """图遍历算法演示: BFS, DFS, Dijkstra"""     def __init__(self):        # 邻接表存储: {node: [(neighbor, weight), ...]}        self.graph = {            'A': [('B', 4), ('C', 2)],            'B': [('A', 4), ('C', 1), ('D', 5)],            'C': [('A', 2), ('B', 1), ('D', 8), ('E', 10)],            'D': [('B', 5), ('C', 8), ('E', 2)],            'E': [('C', 10), ('D', 2)]        }     def bfs(self, start):        """广度优先搜索: 水波式层层扩散, 计算最短跳数"""        dist = {node: float('inf') for node in self.graph}        dist[start] = 0        queue = deque([start])        while queue:            node = queue.popleft()            for neighbor, _ in self.graph[node]:                if dist[neighbor] == float('inf'):                    dist[neighbor] = dist[node] + 1                    queue.append(neighbor)        return dist     def dfs(self, start, visited=None):        """深度优先搜索: 一条路走到黑, 递归实现"""        if visited is None:            visited = set()        visited.add(start)        for neighbor, _ in self.graph[start]:            if neighbor not in visited:                self.dfs(neighbor, visited)        return visited     def dijkstra(self, start):        """Dijkstra最短路径: 贪心策略, 优先队列实现"""        dist = {node: float('inf') for node in self.graph}        dist[start] = 0        pq = [(0, start)]        while pq:            cur_dist, node = heapq.heappop(pq)            if cur_dist > dist[node]:                continue            for neighbor, weight in self.graph[node]:                new_dist = cur_dist + weight                if new_dist < dist[neighbor]:                    dist[neighbor] = new_dist                    heapq.heappush(pq, (new_dist, neighbor))        return dist  if __name__ == '__main__':    gt = GraphTraversal()    print('BFS from A:', gt.bfs('A'))    print('DFS from A:', gt.dfs('A'))    print('Dijkstra from A:', gt.dijkstra('A'))
目的:用 Python 实现了 BFS、DFS、Dijkstra 三种核心图遍历算法,展示它们在同一张图上的不同计算结果。
  1. self.graph = {'A': [('B', 4), ...]}用字典实现邻接表,键是节点名,值是 (邻居,权重) 列表。
  2. queue = deque([start])BFS 使用双端队列作为先进先出的“候车通道”。
  3. dist[neighbor] = dist[node] + 1BFS 中每经过一条边,跳数加 1,第一次到达即最短跳数。
  4. visited.add(start)DFS 递归前先标记已访问,防止死循环。
  5. heapq.heappop(pq)Dijkstra 用优先队列每次取出当前距离最小的节点,这是贪心策略的体现。
  6. if new_dist < dist[neighbor]松弛操作:发现更短路径就更新记录。

常见误区

误区“BFS 和 DFS 都是找最短路径的,只是遍历顺序不同而已。”很多初学者以为两种搜索方法只是“顺序偏好”,结果完全一样。

正解只有无权图(所有边权重都视为 1)中,BFS 才能保证找到最少边数的路径。DFS 找到的只是“某一条可行路径”,完全可能绕远路。类比地铁:BFS 是“一圈圈扩散”,第一圈碰到目标就一定是最短换乘次数;DFS 是“一路走到黑”,走错支线就得折返,路径长度没保障。对于有权图,BFS 直接失效,必须用 Dijkstra。

为何常见:“最短路径”这个概念在无权图里容易让人忽略“边的权重”,而初学者接触的示例图大多是无权图,BFS 恰好总能找到最短路径,形成错误泛化。

误区“邻接矩阵查找两个节点是否相连只需要 O(1) 时间,所以比邻接表更高效,应该优先使用。”

正解邻接矩阵的 O(1) 查询是在“浪费大量存储空间”的前提下达成的。对于 V=1000 万的社交网络,邻接矩阵需要 10^14 个格子(约 100TB),而邻接表只存储真实存在的边(假设每人 200 个好友,约 20 亿条边,约 16GB)。宽列数据库和大多数图数据库都采用邻接表思想存储。类比地铁图:你要一张“本站直接连接的站点”清单,查通讯录(邻接表)比查一本包含所有站点两两关系的巨型表格(邻接矩阵)现实得多。

为何常见:算法教科书喜欢用邻接矩阵讲解图论,因为数学表达简洁,导致初学者误以为它是工程首选。

误区“PageRank 就是把指向某个网页的链接数数一遍,链接越多排名越高。”

正解PageRank 的核心创新在于“加权投票”——被重要节点指向的权重更高。一个被 10 个大网站指向的页面,排名远高于被 1000 个垃圾站指向的页面。就像社交影响力:你有 1000 个粉丝但全是机器人,和一个有 100 个粉丝但都是行业领袖的人,后者在网络上更有话语权。统计入链数量相当于只数粉丝数,PageRank 则考虑了“粉丝的质量”。

为何常见:PageRank 名字里的“Rank“容易让人只想到排序,而忽略其背后的随机游走数学模型中”权重传递”的精髓。

误区“分布式遍历就是把 BFS 代码放在集群上跑,用 MapReduce 处理就行,没什么大不了的。”

正解分布式图遍历面临的核心挑战是“数据局部性”——相邻节点可能存储在不同机器上,每跨一次机器就是一次毫秒级网络通信,比本地内存访问慢百万倍。简单的 MapReduce 模型需要多轮 MR 作业,每轮都要全量读写磁盘,效率极低。Pregel、GraphLab 等框架采用"Think Like a Vertex"的 BSP 模型,让每个顶点在自己的机器上执行计算,通过批量消息传递减少通信轮次。就像全国地铁调度:每个城市(机器)在自己的地盘内调度没问题,但跨城市协调需要统一的通信协议和高昂的电话费。

为何常见:初学者习惯把分布式想象成“多台电脑一起算”,忽略了分布式系统中最本质的网络通信瓶颈。

误区“求最短路径用 Dijkstra 就对了,它是唯一且最优的算法。”

正解Dijkstra 适用于“单源 + 正权边”的场景。如果只需要求两个特定点之间的路径,A* 算法用启发式函数引导搜索方向,通常更快;如果图中有负权边,需要 Bellman-Ford;如果要求所有点对之间的最短路径,Floyd-Warshall 更直接。类比导航:Dijkstra 是“没看地图硬算所有路线的总距离”,A* 是“朝着目标方向优先找路”——后者在目标明确时效率高得多。

为何常见:Dijkstra 在算法课上的存在感太强,“解决最短路径”这个印象被过度简化,忽略了算法的适用边界。

面试题

Q1. BFS 和 DFS 在无权图中都能完成图遍历,如果要找从起点到终点的最少边数路径,应该选择哪种算法?

单选题 · mid

  • A.BFS,因为它逐层扩散,第一次到达目标即走最少边数← 正确答案
  • B.DFS,因为它沿路径深入,能快速找到一条完整路径
  • C.两种都可以,结果相同
  • D.两种都不行,必须用 Dijkstra 算法

答案:A。BFS 按“距离起点的跳数”逐层扩展,水波式扩散保证第一次碰到目标节点时走的边数一定最少。DFS 沿一条路走到底,找到的路径完全可能绕远路。Dijkstra 是处理“带权重边”的算法,在无权图中退化为 BFS 的逻辑,但没有必要额外使用。

Q2. 邻接表和邻接矩阵相比,最大的优势是什么?

单选题 · easy

  • A.查询两个节点是否相邻更快
  • B.在稀疏图上存储空间效率更高← 正确答案
  • C.遍历所有邻居时无需额外操作
  • D.占用内存大小与节点数无关

答案:B。邻接表只存储真实存在的边,空间复杂度为 O(V+E);邻接矩阵固定需要 V² 个格子。在真实图(社交网络、地铁图)中 E 远小于 V²,邻接表空间优势巨大。邻接矩阵查询“两点是否相邻”确实只要 O(1),但代价是海量空间浪费。宽列数据库如 HBase 正是采用类似邻接表的存储方式来支持大规模图数据。

Q3. Dijkstra 算法为什么要求图中不能有负权边?

单选题 · hard

  • A.负权边会导致算法内存溢出
  • B.负权边会破坏贪心策略的正确性,已确定最短距离的节点可能被更短的路径再次更新← 正确答案
  • C.负权边会导致图不再是连通图
  • D.负权边会导致 BFS 队列死循环

答案:B。Dijkstra 的贪心假设是“已确定最短距离的节点不会再被更新”。一旦有负权边,可能出现“先确定的路径更长,后来经过负权边反而更短”的情况——比如 A→B 权重 10,A→C 权重 1,C→B 权重 -5,则通过 C 到 B 只要 -4。这个假设崩塌后,整个算法的正确性基础就不存在了。处理负权边需要 Bellman-Ford 算法。

Q4. PageRank 计算中,阻尼因子 d 通常取 0.85,它的含义是什么?

单选题 · mid

  • A.用户有 85% 的概率点击当前网页上的链接继续浏览,15% 的概率跳到任意新页面← 正确答案
  • B.用户有 85% 的概率关闭浏览器,15% 的概率继续浏览
  • C.网页有 85% 的概率被搜索引擎收录
  • D.网页有 15% 的概率被黑客攻击

答案:A。阻尼因子模拟真实用户的浏览行为——大多数时候用户会顺着当前页面的链接继续跳转(对应 d=0.85),但也有可能直接输入新网址或从书签进入任意页面(对应 1-d=0.15)。这个“随机跳转项”保证了 PageRank 迭代的收敛性,避免某些没有任何出链的“黑洞页面”把权重全部吸收。

Q5. 在分布式图计算中,以下哪些是面临的真实挑战?

多选题 · mid

  • A.数据分片后,相邻节点可能存储在不同机器上,遍历时需要跨机通信← 正确答案
  • B.迭代算法每轮都需要全局同步,速度受制于最慢的机器← 正确答案
  • C.单机内存无法容纳整个大图的必然性与数据访问局部性的矛盾← 正确答案
  • D.分布式系统无法实现 BFS 算法

答案:ABC。A 是数据局部性问题——哈希分片可能导致一条边的两端在不同机器上,每次跨机访问都是毫秒级网络延迟。B 是 BSP 模型的同步屏障代价,PageRank 每轮迭代都要所有机器完成同步才能进入下一轮。C 是分布式图计算的根本原因和核心矛盾。D 显然是错的——分布式系统完全能实现 BFS,只是需要精心设计消息传递和状态管理。

Q6. 在无权图中,BFS 计算出的距离 dist[v] 的含义是什么?

单选题 · easy

  • A.从起点到 v 的总路径长度(边权累加)
  • B.从起点到 v 经过的最少边数← 正确答案
  • C.从起点到 v 经过的最大边数
  • D.从起点到 v 经过的节点总数减 1 的相反数

答案:B。BFS 的核心性质是“逐层扩散”,第一次到达节点 v 时经过的边数一定是最少的。dist[v] 被定义为从起点到 v 的最少跳数。在无权图中每条边视为等权重,所以“最少边数”就是“最短路径”的度量。类比地铁换乘:dist[v]=3 意味着最少坐 3 段地铁能到达 v 站。

Q7. 关于 PageRank 的迭代计算,以下说法正确的是哪些?

多选题 · hard

  • A.每个节点将自己的 PageRank 值平均分给所有出边邻居← 正确答案
  • B.节点的入链来自重要节点的投票权重更高,形成“重要性传递”← 正确答案
  • C.经过足够多轮迭代后,各节点 PageRank 值收敛到稳定分布← 正确答案
  • D.PageRank 值只与入链数量有关,与出链数量无关

答案:ABC。A 是 PageRank 的权重传播规则——出边越多,每条边分到的权重越少。B 是核心创新——权重的“质量”会通过入链节点的 PR 值传递。C 是数学保证——在连通图上迭代必然收敛。D 错误:出链数量直接影响该节点的权重大小。类比口碑传播:你认识 10 个朋友和认识 100 个朋友,向每个人推荐某个产品时的影响力浓度是不同的。

Q8. 哪些数据结构分别支撑 BFS 和 DFS 的遍历顺序?

单选题 · mid

  • A.BFS 用栈,DFS 用队列
  • B.BFS 用队列,DFS 用栈← 正确答案
  • C.BFS 和 DFS 都用栈
  • D.BFS 和 DFS 都用队列

答案:B。BFS 需要先进先出的队列,保证先访问的节点的邻居也先被访问,实现水波式扩散。DFS 需要后进先出的栈(或递归调用栈),保证沿着一条路径深入到底再回溯。类比地铁站:BFS 是“候车通道”先来先走;DFS 是“一叠盘子”后来的反而先拿走。

Q9. 宽列数据库(如 HBase)天然适合存储哪种图结构?

单选题 · easy

  • A.邻接矩阵,因为列族可以映射为矩阵的列
  • B.邻接表,因为每一行可以存储一个节点的所有邻居← 正确答案
  • C.边列表,因为每一行只存储一条边
  • D.邻接矩阵和邻接表都完全不适合

答案:B。宽列数据库以行键为索引,行键就是节点 ID,列族中的每一列可以存一个邻居节点(列名=邻居 ID,值=边权重)。读取一行就能拿到某个节点的全部邻居,这与邻接表的“通讯录”结构完美匹配。这种设计让 BFS 等遍历算法在读取节点邻居时只需要一次行读取操作,效率远高于关系型数据库的多次 JOIN。

Q10. 已知从 A 到 B 的最短路径经过节点序列 A→C→D→B,总权重为 10。以下哪种情况一定不会出现?

单选题 · hard

  • A.存在路径 A→E→B,总权重为 15
  • B.A 到 C 的最短距离是 3,C 到 D 的最短距离是 4,D 到 B 的最短距离是 3
  • C.A 到 C 的最短距离是 5,但 A→C→D→B 仍是 A 到 B 的最短路径
  • D.存在路径 A→F→B,总权重为 8← 正确答案

答案:D。Dijkstra 算法的核心性质是“最短路径的子路径也是最短路径”——如果 A→C→D→B 是 A 到 B 的最短路径(权重 10),那么 A→C 这一段也必须是 A 到 C 的最短路径。若存在总权重 8 的路径 A→F→B,则 A 到 B 的最短距离至少是 8,与题目已知的 10 矛盾(除非边权为负,但 Dijkstra 不允许)。D 选项中总权重 8 < 10,直接违反“已知最短为 10”的前提。