Python数据结构:图的实现(转)
2008-03-26 15:00
原作:Python.org
翻译:nasi
图是由边或者弧连接起来的节点的网络。在有向图中,节点之间的连接是有方向的,叫做弧(arcs)。在无向图中,节点间的连接没有方向,叫做边 (edge)。图算法包括查找两点间路径、两点间的最短路径、判断一个图中是否存在环(环是从一个节点可以沿一条非空路径回到它自己)、找到可以遍历所有 节点的路径(著名的TSP问题,即旅行商问题)等等。有些情况下节点或者图的弧是带有权重(weight)或者消耗(cost)的,我们一般需要寻找这类 图上的最小消耗路径。
图算法作为离散数学中的一个重要部分,有很多文献可供参考。图在计算机算法中有很多的应用,一个很明显的例子就是网络管理。而且图还在其它的很多领 域有广泛的应用,再比如在一个计算机程序之内的函数叫与被叫关系就可以看成是一个图,如果出现了环(cycle)就表示递归,而不能到达的节点就是“死代 码”。
几乎没有编程语言把图作为一项直接支持的数据类型,Python也不例外。然而,图很容易通过列表和词典来构造。比如说,这有一张简单的图:
A -> B
A -> C
B -> C
B -> D
C -> D
D -> C
E -> F
F -> C
这个图有6个节点(A-F)和8个弧。它可以通过下面的Python数据结构来表示:
graph = {'A': ['B', 'C'],
'B': ['C', 'D'],
'C': ['D'],
'D': ['C'],
'E': ['F'],
'F': ['C']}
这是一个词典,每个key都是图的节点。每个key都对应一个列表,列表里面存的是直接通过一个弧和这个节点连接的节点。这个图非常简单了,不过更简单的是用数字来代替字母来表示一个节点。不过用名字(字母)来表示很方便,而且也便于扩展,比如可以改成城市的名字等等。
我们来写一个函数来判断两个节点间的路径。它的参数是一个图、一个起始节点和一个终点。它会返回一个列表,列表里面存有组成这条路径的节点(包括起 点和终点)。如果两个节点之间没有路径的话,那就返回None。相同的节点不会在返回的路径中出现两次或两次以上(就是说不会包括环)。这个算法用到了一 个很重要的技术,叫做回溯:它会去尝试每一种可能,直到找到结果。
def find_path(graph, start, end, path=[]):
path = path + [start]
if start == end:
return path
if not graph.has_key(start):
return None
for node in graph[start]:
if node not in path:
newpath = find_path(graph, node, end, path)
if newpath: return newpath
return None
运行的结果(上面的那张图):
>>> find_path(graph, 'A', 'D')
['A', 'B', 'C', 'D']
>>>
代码中的第二个if(译者注:是指if not graph.has_key(start):这句)仅仅在遇到一类 特殊的节点的时候才有用,这类节点有其他的节点指向它,但是它没有任何弧指向其他的节点,所以就并不会在图这个词典中作为key被列出来。也可以这样来处 理,即这个节点也作为一个key,但是有一个空的列表来表示其没有指向其他节点的弧,不过不列出来会更好一些。
注意,当我们调用find_graph()的时候,使用了3个参数,但是实际上使用了4个参数:还有一个是当前已经走过的路径。这个参数的默认值是 一个空列表,“[]”,表示还没有节点被访问过。这个参数用来避免路径中存在环(for循环中的第一个if语句)。path这个参数本身不会修改,我们用 “path = path + [start]”只是创建了一个新的列表。如果我们使用“path.append(start)”的话,那我们就修改了path的值,这样会产生灾难性后 果了。如果使用元组的话,我们可以保证这个是不会发生的。在使用的时候要写“path = path + (start,)”,注意“(start)”并不是一个单体元组,只是一个括号表达式而已。
很容易修改这个函数来实现返回一个节点到另一个节点的所有路径,而不仅仅只查找第一条路径:
def find_all_paths(graph, start, end, path=[]):
path = path + [start]
if start == end:
return [path]
if not graph.has_key(start):
return []
paths = []
for node in graph[start]:
if node not in path:
newpaths = find_all_paths(graph, node, end, path)
for newpath in newpaths:
paths.append(newpath)
return paths
>>> find_all_paths(graph, 'A', 'D')
[['A', 'B', 'C', 'D'], ['A', 'B', 'D'], ['A', 'C', 'D']]
>>>
还可以改成查找最短路径:
def find_shortest_path(graph, start, end, path=[]):
path = path + [start]
if start == end:
return path
if not graph.has_key(start):
return None
shortest = None
for node in graph[start]:
if node not in path:
newpath = find_shortest_path(graph, node, end, path)
if newpath:
if not shortest or len(newpath) < len(shortest):
shortest = newpath
return shortest
运行结果:
>>> find_shortest_path(graph, 'A', 'D')
['A', 'C', 'D']
这些函数都非常简单,但是却已经接近最优了(用Python写成的代码中)。在另一篇文章中,我将尝试去分析它们的运行速度,并改进它们的性能。
另外还可以引入更多的数据抽象:用一个类来表示一个图,并通过各种方法来实现各种算法。如果通过结构化编程来做这个事情的话,其实对代码的效率提升 没什么帮助(有时恰恰相反)。很容易把节点或者弧加上一个命名,就可以来解决实际问题了(比如在地图上查找两个城市中的最短路)。这个也会在另一篇文章中 讨论。