资讯动态

从“万岁喜欢时秒”到图论思维:如何用邻接表与DFS/BFS算法优雅处理关系链

发布时间:2026/8/17 5:12:45 来源:尧图企业网站定制
最近在整理一些老项目的代码发现一个很有意思的现象很多开发者尤其是刚入行的朋友在处理数据关联、状态流转或者业务逻辑时特别喜欢写“硬编码”的关系链。比如在一个用户社交关系的模拟模块里代码里可能直接写着用户A喜欢用户B用户B喜欢用户C用户C喜欢用户D。这行代码看起来清晰明了任务也完成了。但当我问“如果‘喜欢’的关系变了或者要基于这个关系链做路径分析、推荐计算甚至只是换个展示方式你打算怎么改”得到的回答往往是“啊那我得把代码里这些写死的关系一个一个找出来改。”这个场景让我立刻想到了一个非常经典的逻辑谜题或者说是一种特定的数据结构原型。它没有高深的算法却精准地戳中了我们在设计初期最容易忽略的问题如何处理多对一、单向且可能形成环路的“偏好”或“指向”关系这个问题的简化模型就藏在一个看似娱乐化的表述里“万岁喜欢时秒时秒喜欢开心开心喜欢时分万幸喜欢妙妙”。我们今天不讨论动漫或电影情节而是把这个关系链作为一个绝佳的技术沙盘。它像一面镜子照出了从“一次性脚本”到“可维护系统”的关键分水岭。很多人写完第一版能跑通的代码就以为结束了但真正的挑战在于当关系动态变化、当需要查询任意两人间的连通性、当要找出所有“单相思”未被喜欢的节点或者检测关系环时你最初那个写死在代码里的if-else链条还扛得住吗这篇文章我们就以这个微型关系链为引子拆解一套可复用的处理思路。你会发现核心不是学会某个特定的库而是建立“将业务关系抽象为图结构并选择合适算法进行查询与分析”的思维框架。这套框架能帮你应对从社交网络、任务依赖到状态机流转的无数场景。1. 从字符串到数据结构第一层抽象陷阱当我们看到“A喜欢B”这样的描述时第一反应可能是用一个字典Map或者列表来存。比如用Python写relationships { “万岁”: “时秒”, “时秒”: “开心”, “开心”: “时分”, “万幸”: “妙妙” }看起来完美查询“万岁”喜欢谁relationships[“万岁”]立刻返回“时秒”。任务完成。但这仅仅是第一层陷阱。这种结构只方便做“一对一”的即时查询它隐含着诸多限制无法逆向查询“开心”被谁喜欢你需要遍历整个字典。无法处理多对一如果“张三”也喜欢“时秒”字典的键Key就冲突了。难以发现环路“A喜欢BB喜欢CC喜欢A”用字典存储后仅凭观察很难直观发现这个环。路径查询困难想知道“万岁”通过“喜欢”关系能否最终关联到“妙妙”你需要手动写循环或递归去跳转。所以这个字典只是一个“记录”远不是一个“模型”。它没有揭示数据内部真正的结构——有向图。1.1 识别问题背后的图结构“喜欢”是一种典型的单向关系在数学和计算机科学中这被称为有向图。顶点每个人万岁、时秒、开心、时分、万幸、妙妙。边“喜欢”这个动作就是一条从喜欢者指向被喜欢者的有向边。我们的关系链用图来表示就是万岁 - 时秒 - 开心 - 时分 万幸 - 妙妙注意“时分”没有指向其他人它是一个终点。“妙妙”也是终点且没有人指向“万幸”和“万岁”他们是起点。一旦完成这个认知转换很多问题就变成了标准的图论问题“开心被谁喜欢” - 求顶点“开心”的入度。“万岁能通过关系链找到妙妙吗” - 判断从顶点“万岁”到顶点“妙妙”是否存在路径。“有没有人陷入单相思循环” - 检测图中是否存在有向环。“谁是最受欢迎的人” - 找到入度最大的顶点。1.2 选择正确的数据结构表示图有了图的认识我们就不能再用简单的字典了。主流表示方法有两种邻接表为每个顶点维护一个列表存储它直接指向的所有顶点。这对于查找一个顶点的所有“后继”非常高效。graph { “万岁”: [“时秒”], “时秒”: [“开心”], “开心”: [“时分”], “时分”: [], # 没有喜欢的人 “万幸”: [“妙妙”], “妙妙”: [], }邻接矩阵一个二维数组或矩阵如果matrix[i][j] 1表示存在一条从顶点i到顶点j的边。这对于快速判断任意两点间是否有直接边很方便但在顶点多、边稀疏时非常浪费空间。对于“喜欢关系”这类通常稀疏的图邻接表是更通用和节省空间的选择。它不仅存储了关系更结构化地表达了图的连接性。2. 超越静态存储实现基础图算法查询数据结构升级后我们就可以利用经典的图算法来回答那些复杂问题了。这里我们实现几个最实用的函数。假设我们已经用邻接表构建了图graph。2.1 查找关系路径深度优先搜索问题“万岁”最终能否通过喜欢链关联到“时分”这需要检查是否存在一条从“万岁”到“时分”的路径。深度优先搜索是一种直观的探索方式从起点开始沿着一条路尽可能深地走走不通再回溯。def has_path_dfs(graph, start, end, visitedNone): 使用深度优先搜索判断图中是否存在从start到end的路径。 if visited is None: visited set() if start end: return True visited.add(start) for neighbor in graph.get(start, []): if neighbor not in visited: if has_path_dfs(graph, neighbor, end, visited): return True return False # 测试 print(has_path_dfs(graph, “万岁”, “时分”)) # 应返回 True print(has_path_dfs(graph, “万岁”, “妙妙”)) # 应返回 False print(has_path_dfs(graph, “万幸”, “时分”)) # 应返回 False为什么用DFS在这个例子中图很小且没有环DFS和BFS广度优先搜索都可以。DFS实现简单递归写法清晰。但如果图很深或想找最短路径BFS更合适。2.2 检测关系环拓扑排序与DFS染色法问题如果关系变成“万岁喜欢时秒时秒喜欢开心开心喜欢万岁”这就形成了一个环。在任务依赖调度中环意味着死锁。我们需要检测它。方法一DFS染色法状态标记为每个顶点定义三种状态未访问0、访问中1、已访问2。在DFS过程中如果遇到了状态为“访问中”的顶点说明发现了环。def has_cycle(graph): 检测有向图中是否有环。 state {node: 0 for node in graph} # 0未访问1访问中2已访问 def dfs(node): if state[node] 1: # 遇到访问中的节点发现环 return True if state[node] 2: # 已访问过跳过 return False state[node] 1 # 标记为访问中 for neighbor in graph.get(node, []): if dfs(neighbor): return True state[node] 2 # 标记为已访问 return False for node in graph: if state[node] 0: if dfs(node): return True return False # 测试一个无环图 graph_no_cycle {“A”: [“B”], “B”: [“C”], “C”: []} print(has_cycle(graph_no_cycle)) # False # 测试一个有环图 graph_with_cycle {“万岁”: [“时秒”], “时秒”: [“开心”], “开心”: [“万岁”]} print(has_cycle(graph_with_cycle)) # True方法二拓扑排序Kahn算法如果图可以完成拓扑排序所有顶点排成一个线性序列满足每条边从序列前的顶点指向序列后的顶点则该图是无环的。算法核心是不断移除入度为0的顶点。from collections import deque def can_topological_sort(graph): 使用Kahn算法判断图是否有环能否进行拓扑排序。 # 计算所有顶点的入度 in_degree {node: 0 for node in graph} for node in graph: for neighbor in graph[node]: in_degree[neighbor] in_degree.get(neighbor, 0) 1 # 将所有入度为0的顶点加入队列 queue deque([node for node in graph if in_degree[node] 0]) count 0 while queue: current queue.popleft() count 1 for neighbor in graph[current]: in_degree[neighbor] - 1 if in_degree[neighbor] 0: queue.append(neighbor) # 如果排序的顶点数等于总顶点数则无环 return count len(graph) # 测试 print(can_topological_sort(graph_no_cycle)) # True print(can_topological_sort(graph_with_cycle)) # False如何选择DFS染色法在只需要判断“是否有环”时编码简单。Kahn算法在需要获取拓扑序列时更直接且易于理解。2.3 分析节点特性入度、出度与连通分量基于邻接表我们可以轻松计算一些揭示节点特性的指标def analyze_graph(graph): 分析图的入度、出度并找出孤立的节点无入度也无出度。 nodes set(graph.keys()) # 收集所有出现在邻接表值里的节点 for neighbors in graph.values(): nodes.update(neighbors) in_degree {node: 0 for node in nodes} out_degree {node: 0 for node in nodes} for node, neighbors in graph.items(): out_degree[node] len(neighbors) for neighbor in neighbors: in_degree[neighbor] in_degree.get(neighbor, 0) 1 print(“顶点入度分析”) for node in sorted(nodes): print(f” {node}: 被{in_degree[node]}人喜欢喜欢{out_degree[node]}人“) # 找出“单相思”起点喜欢别人但没人喜欢自己 lonely_start [node for node in nodes if out_degree[node] 0 and in_degree[node] 0] print(f”\n‘单相思’起点无人喜欢他们: {lonely_start}“) # 找出“终点”不喜欢任何人 endpoints [node for node in nodes if out_degree[node] 0] print(f”关系链终点不喜欢任何人: {endpoints}“) # 对原始关系链进行分析 original_graph { “万岁”: [“时秒”], “时秒”: [“开心”], “开心”: [“时分”], “时分”: [], “万幸”: [“妙妙”], “妙妙”: [], } analyze_graph(original_graph)运行这段代码你会立刻得到一份数据报告“万岁”和“万幸”是单相思起点“时分”和“妙妙”是终点。这种分析能力是原始字典存储无法直接提供的。3. 从算法到工程设计可维护的关系系统掌握了图的基本算法我们已经能处理静态关系链的查询了。但真实系统是动态的、需要持久化的、并且要服务多个查询需求的。我们需要进行第二次抽象设计一个轻量级的“关系服务”。这个服务不依赖重型图数据库但具备了图数据库的核心思维。我们设计一个RelationshipGraph类class RelationshipGraph: def __init__(self): # 使用邻接表存储正向关系 self._graph {} # 可选使用逆邻接表加速“被谁喜欢”的查询 self._reverse_graph {} def add_relationship(self, from_person, to_person): 添加一条‘喜欢’关系 if from_person not in self._graph: self._graph[from_person] [] if to_person not in self._graph: self._graph[to_person] [] self._graph[from_person].append(to_person) # 维护逆邻接表 if to_person not in self._reverse_graph: self._reverse_graph[to_person] [] self._reverse_graph[to_person].append(from_person) def remove_relationship(self, from_person, to_person): 移除一条‘喜欢’关系 if from_person in self._graph and to_person in self._graph[from_person]: self._graph[from_person].remove(to_person) self._reverse_graph[to_person].remove(from_person) def get_likes(self, person): 获取某人喜欢谁 return self._graph.get(person, []).copy() # 返回副本避免外部修改 def get_liked_by(self, person): 获取谁喜欢某人使用逆邻接表O(1)复杂度 return self._reverse_graph.get(person, []).copy() def has_path(self, from_person, to_person, method’dfs’): 判断是否存在关系路径可选择DFS或BFS # 实现略参考上一节的DFS/BFS pass def find_all_paths(self, from_person, to_person): 查找所有路径如果存在多条 # 使用带回溯的DFS def dfs(current, path, visited, all_paths): if current to_person: all_paths.append(path.copy()) return visited.add(current) for neighbor in self._graph.get(current, []): if neighbor not in visited: path.append(neighbor) dfs(neighbor, path, visited, all_paths) path.pop() visited.remove(current) all_paths [] dfs(from_person, [from_person], set(), all_paths) return all_paths def detect_cycle(self): 检测图中是否有环 # 使用DFS染色法实现略 pass def get_popularity_ranking(self): 按被喜欢次数入度排序 popularity {person: len(self._reverse_graph.get(person, [])) for person in self._graph} return sorted(popularity.items(), keylambda x: x[1], reverseTrue) # 使用示例 rg RelationshipGraph() rg.add_relationship(“万岁”, “时秒”) rg.add_relationship(“时秒”, “开心”) rg.add_relationship(“开心”, “时分”) rg.add_relationship(“万幸”, “妙妙”) print(“时秒喜欢的人:”, rg.get_likes(“时秒”)) print(“喜欢开心的人:”, rg.get_liked_by(“开心”)) print(“万岁 - 时分 的所有路径:”, rg.find_all_paths(“万岁”, “时分”)) print(“受欢迎度排名:”, rg.get_popularity_ranking())这个类的设计体现了几个工程化考量封装内部数据结构对使用者隐藏。高效查询通过维护逆邻接表将“被谁喜欢”的查询从O(N)降到O(1)。原子操作提供增删改查的基本原子操作。高级功能基于原子操作构建路径查找、环检测等高级功能。数据安全返回列表副本防止外部代码意外修改内部状态。4. 模式泛化这套思维能用在哪儿“万岁喜欢时秒”这个例子看似简单但它抽象出的“有向图关系处理”模式在软件开发中无处不在。理解了这个核心你就能举一反三。4.1 场景一任务调度与依赖管理这是最直接的应用。每个任务是一个顶点任务A依赖任务B就建立一条B-A的边B完成后A才能开始。我们的RelationshipGraph稍作修改改个名字叫TaskDependencyGraph就能用来检测循环依赖detect_cycle()方法直接告诉你项目配置是否有死锁。生成执行序列对无环图进行拓扑排序can_topological_sort的输出顺序就是一种可行序列就是任务的执行顺序。关键路径分析在边上加上耗时权重就能计算项目最短完成时间。4.2 场景二社交网络与推荐系统“关注”、“好友”、“点赞”都是关系。虽然社交图更复杂可能是无向的、带权重的但底层逻辑相通。寻找共同好友这变成了求两个顶点邻居集合的交集。推荐可能认识的人朋友的朋友二度人脉。这可以通过BFS遍历深度为2的节点来实现。社区发现寻找图中联系紧密的群体聚类可以使用更高级的算法但基础依然是图遍历。4.3 场景三状态机与工作流每个状态是一个顶点状态间的转换是边。例如一个订单的状态“待支付”-“已支付”-“已发货”-“已完成”。当然可能还有“已取消”等状态。验证状态流转合法性用户请求从“已发货”状态跳到“待支付”是否允许只需检查图中是否存在这条边。可视化所有流程图结构天生适合被可视化库渲染一目了然。4.4 场景四代码与数据血缘分析在数据仓库或复杂系统中理解表与表、字段与字段的依赖关系至关重要。表级血缘表A由表B和表C加工而成则建立B-A, C-A的边。影响分析如果表B要重构会影响哪些下游表从B出发做DFS或BFS遍历即可。根因分析某个指标数据出错向上游追溯来源。从该指标顶点出发在逆邻接表上做遍历。4.5 实施建议什么时候该引入专业图数据库我们自建的RelationshipGraph类适用于关系简单、数据量不大例如顶点数在万级以下、且业务逻辑固定的场景。它的优点是轻量、无外部依赖、定制灵活。但当遇到以下情况时应考虑使用专业的图数据库如 Neo4j, JanusGraph, Nebula Graph数据量巨大顶点和边达到百万、千万甚至亿级。查询极度复杂需要频繁进行多跳查询例如“朋友的朋友的朋友”、最短路径查找、复杂模式匹配。要求高性能实时查询图数据库为图遍历做了专门的存储和索引优化。需要持久化与高可用自建类通常需要自己解决数据落盘、备份、集群等问题。核心原则是不要一开始就追求大而全的架构。用最简单的结构如字典、列表快速验证核心业务逻辑当遇到性能瓶颈或复杂查询瓶颈时再评估是否需要升级到更专业的图处理方案。我们上面构建的RelationshipGraph就是这个演进过程中的一个完美中间态。回过头看“万岁喜欢时秒”这条小小的关系链其价值远不止于一个编程练习题。它强迫我们完成一次关键的思维跃迁从看待离散的数据点到洞察数据之间连接所构成的结构。这种“图思维”是处理任何关联性系统的基础。下次当你面对一堆相互关联的实体时无论是用户、任务、状态还是数据表不妨先问自己一句这能不能抽象成一个图如果能顶点和边分别是什么想清楚了这个问题解决方案的蓝图就已经在你面前展开了一半。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价