如何在Shell中快速计算有向图节点的成本总和?
高效处理大规模有向图路径成本计算的Awk方案
你的问题很典型——纯Shell脚本处理这类图遍历任务时,频繁的子进程调用(比如反复用grep/cut)和嵌套循环,在节点数达到2000级时肯定会慢得离谱。改用Awk是最佳选择,它能在单进程内完成所有数据加载、图遍历和计算,完全规避Shell的性能瓶颈。
核心思路
- 内存化数据加载:把三个输入文件的内容全部加载到Awk的内存数组中,避免反复读写文件:
- 邻接表:用
adj[from]存储所有从from出发的目标节点(用逗号拼接,方便后续拆分) - 成本映射:
cost[node]直接存储节点对应的成本数值 - 起始节点列表:
heads数组标记所有需要遍历的起始节点
- 邻接表:用
- 递归遍历路径:从每个起始节点出发,递归遍历所有子节点:
- 遇到无出边的叶子节点时,输出完整路径、成本表达式和累计总和
- 遍历过程中检查并跳过循环路径,避免无限递归
- 单进程高效运算:所有操作在Awk进程内完成,无需频繁fork子进程,数组查找和遍历的效率远高于Shell循环
完整Awk脚本
#!/usr/bin/awk -f BEGIN { # 读取起始节点列表 while ((getline < ARGV[1]) > 0) { heads[$1] = 1 } close(ARGV[1]) # 读取边数据,构建邻接表 while ((getline < ARGV[2]) > 0) { adj[$1] = adj[$1] ? adj[$1] "," $2 : $2 } close(ARGV[2]) # 读取节点成本映射 while ((getline < ARGV[3]) > 0) { cost[$1] = $2 } close(ARGV[3]) # 遍历所有起始节点,启动路径追踪 for (start in heads) { traverse(start, start, cost[start], cost[start]) } } # 递归遍历函数:参数依次为当前节点、路径字符串、成本表达式、累计成本 function traverse(current, path, expr, sum, children, n, i, child, visited) { # 检查当前节点是否为叶子(无出边) if (!(current in adj)) { printf("%s\t%s\t%d\n", path, expr, sum) return } # 拆分当前节点的子节点列表 split(adj[current], children, ",") n = length(children) # 检查循环:用数组记录当前路径的所有节点 split(path, visited, "->") for (k in visited) { visited_map[visited[k]] = 1 } for (i = 1; i <= n; i++) { child = children[i] # 如果子节点已在当前路径中,跳过循环 if (child in visited_map) { continue } # 递归处理子节点 traverse(child, path "->" child, expr "+" cost[child], sum + cost[child]) } # 清理当前路径的访问标记,避免影响其他分支 delete visited_map }
使用方法
- 把上述代码保存为
script.sh,添加执行权限:
chmod +x script.sh
- 运行脚本:
./script.sh heads.txt connections.txt cost.txt
性能优势说明
- 无冗余IO:一次性加载所有数据到内存,避免反复读取文件
- 单进程运算:全程在Awk进程内执行,没有Shell脚本常见的频繁fork子进程开销
- 高效数据结构:数组查找为O(1)复杂度,邻接表遍历的效率远高于Shell的文本匹配操作
循环处理细节
脚本通过visited_map数组记录当前路径的所有节点,遇到已存在的子节点直接跳过,彻底避免进入循环路径。这个逻辑比字符串匹配更可靠,即使节点名包含特殊字符也能正常工作。
示例测试输出
用你提供的示例文件测试,输出完全符合预期:
str1->str2->str3->str4 1+5+10+548 564 str100->str2->str3->str4 57+5+10+548 620 str100->str101->str102 57+39+23 119
内容的提问来源于stack exchange,提问作者KamilCuk
相关产品推荐
相关产品推荐

