1、图graph的概念:
在这一章我们学习图。图是比我们上一章学习的树更普遍的结构,事实上我们可以认为树是一种特殊的图。图可以被用来表述世界上很多有趣的事情,包括道路系统,城市间航线,因特网的联接,甚至是你完成计算机科学学位所必修的课程顺序。接下来将看到一旦我们很好地表述了某个问题,我们可以使用标准的图算法来解决一些看起来非常困难的问题。
对人类来说,看懂路线图并了解不同地点之间的关系是相当容易的事情,而计算机并没有这样的能力。但是,我们也可以将路线图看作一个图。当我们这样做的时候,可以让计算机为我们做一些有趣的事情。如果你用过地图网站,你就会知道计算机可以找到一个地方到另一个地方最短、最快捷或最简单的路径。
1.1 术语表及定义:
看完了图的一些例子,我们可以更正式地定义一个图和它的构成要素。通过我们对树的讨论,我们已经知道了一些术语:
-
顶点 Vertex
顶点(也称“节点 node”)是图的基础部分。它具有名称标识“key”。顶点也可以有附加的信息项“playload”。 -
边 Edge
边(也称“弧 arc”)是图的另一个基础组成部分。如果一条边连接两个顶点,则表示两者具有联系。边可以是单向的,也可以是双向的。如果一个图中的边都是单向的,我们就说这个图是“有向图 directed graph/digraph”。 -
权重 Weight
为了表达从一个顶点到另一个顶点的“代价”,可以给边赋权。例如,一个连接两个城市的道路图中,两个城市之间的距离就可以作为边的权重。 -
图的定义
有了这些定义,我们可以正式定义一个图。图可以用 G=(V,E)来表述。对于图 G,V是顶点的集合,E是边的集合。每个边是一个元组(v, w),w, v∈V。我们可以在边元组中加入第三个要素来表述权重。它的一个子图s就是边e和顶点v的集合,e ⊂ E并且v ⊂ V。
下图展现了另一个简单有向赋权图。这个图可以表示成6个顶点:V={V0, V1, V2, V3, V4, V5}及9条边的集合:E={(v0,v1,5),(v1,v2,4),(v2,v3,9),(v3,v4,7),(v4,v0,1),(v0,v5,2),(v5,v4,8),(v3,v5,3),(v5,v2,1)}
-
路径 Path
图中的路径,是由边依次连接起来的顶点序列。我们将路径定义为 P=(w1, w2,…, wn),其中对于所有 1<=i<=n-1, (wi, wi+1) ∈ E。无权路径的长度为边的数量,等于 n-1。带权路径的长度为所有边权重之和。如上图 中从V3到V1的一条路径是顶点序列(v3,v4,v0,v1),其边为{(v3,v4,7),(v4,v0,1),(v0,v1,5)}。
-
圈 Cycle
有向图里的圈是首尾顶点相同的路径。例如,上图里路径(V5,V2,V3,V5)就是一个圈。没有圈的图称为“无圈图 acyclic graph”,没有圈的有向图称为“有向无圈图 directed acyclic graph或 DAG”。接下来我们会发现,当问题可以被表述成 DAG时,我们可以解决一些很重要的问题。
1.2 抽象数据类型:图
图抽象数据类型(ADT)有如下定义:
Graph()
:创建一个空的图
addVertex(vert)
:将一个顶点 Vertex对象加入图中
addEdge(fromVert, toVert)
:添加一条有向边
addEdge(fromVert, toVert, weight)
:添加一条带权的有向边
getVertex(vertKey)
:查找图中名称为 vertKey的顶点
getVertices()
:返回图中所有顶点列表
in
:按照 vert in graph的语句形式,返回顶点是否存在图中。如果存在则返回True,否则返回False
有几种方法可以在 Python实现图抽象数据结构(ADT),需要在不同的应用中加以权衡。图的实现有两个著名的方法,邻接矩阵 adjacency matrix和邻接表 adjacency list。我们将说明这两种不同的选择,并作为 Python的类来实现邻接矩阵。
-
(1)邻接矩阵
图最容易的实现方法之一就是采用二维矩阵。在矩阵实现方法中,每行和每列都代表图中的顶点。如果顶点v到顶点w之间有边相连,则将值储存在矩阵的v行,w列。当两个顶点通过边来连接,我们就说它们就是邻接的。下图展现了上面图中图的邻接矩阵,每一格的值代表了从顶点 v到顶点w边的权重。
邻接矩阵的优点是简单,对于简单的图来说很容易看出节点之间的联系状态。然而,我们也注意到大部分的矩阵分量是空的,这种情况我们称矩阵是“稀疏”的。矩阵并不是一个储存稀疏数据的有效途径。事实上,在 Python里你必须不厌其烦地制造上图这样的矩阵结构。
当边的数量庞大时,邻接矩阵是图的一个良好的实现方法。但是我们所说的庞大到底是多大呢?需要多少边来填充矩阵?因为图中每个顶点对应着一行一列,填满矩阵需要的边的数量是|V|2。一个充满的矩阵是每个顶点都与其他任何顶点相连。实际的问题很少可以到达这样的连接量。因此本章我们要考虑的问题是稀疏图。
-
(2)邻接表
一个实现稀疏图的更高效的方案时使用邻接表 adjacency list。在这个实现方法中,我们维护一个包含所有顶点的主列表(master list),主列表中的每个顶点,再关联一个与自身有边连接的所有顶点的列表。在实现顶点类的方法里,我们使用字典而不是列表,此时字典中的键(key)对应顶点标识,而值(value)则可以保存顶点连接边的权重。下图展现了上图中图的邻接表。
邻接表实现方法的优点是允许我们高效地表示一个稀疏图。邻接链表还使我们很容易找到某个顶点与其他顶点的所有连接。
2、图的python实现:
在Python中使用字典将使得邻接表的实现变得很容易。在我们实现图表抽象数据类型时,我们可以创建两个类,Graph
和 Vertex
。Graph
保存了包含所有顶点的主表,Vertex
则描绘了图表中顶点的信息。每一个Vertex
使用一个字典来记录顶点与顶点间的连接关系和每条连接边的权重,这个字典被称作connectionTo
(self. connectionTo)。下面给出了Vertex
类的代码:
构造函数(__init___
)简单地初始化了(一般为字符串的)id和 connectionTo字典。addNeighbor
方法被用来添加从一个顶点到另一个顶点的连接。getConnections
方法用以返回以connectionTo
字典中的实例变量所表示的邻接表中的所有顶点。getWeight
方法可以通过一个参数返回顶点与顶点之间的边的权重。
下面是
Vertex
顶点类的实现代码:
class Vertex:
def __init__(self, key):
self.id = key # 键
self.connnectedTo = {
} # 记录节点的指向
def addNeighbor(self, nbr, weitht=0):
"""
作用:增加邻居节点
:param nbr:
:param weitht: 不写,默认为0
:return:
"""
self.connnectedTo[nbr] = weitht
def __str__(self):
return str(self.id) + 'connectedTo:' + str([x.id for x in self.connnectedTo])
def getConnections(self):
"""
作用:得到与当前节点相连的所有其他节点
:return:
"""
return self.connnectedTo.keys()
def getId(self):
"""
作用:返回当前节点的键
:return:
"""
return self.id
def getWeight(self, nbr):
"""
作用:得到当前节点和传进来的节点的权重
:return:
"""
return self.connnectedTo[nbr]
下面实现的是
Graph
类,包含了一个将顶点名称映射到顶点对象的字典。Graph
也提供了向图中添加顶点和将一个顶点与另一个连接起来的方法。getVertices
方法可以返回图中所有顶点的名称。另外我们可以通过实现__iter__
方法简化对特定图中所有顶点对象的遍历。这两种方法允许我们通过顶点名称或顶点对象本身去遍历图中的顶点。
class Graph:
def __init__(self):
self.vertList = {
} # 存顶点的字典
self.numVertices = 0 # 顶点的数量
def addVertex(self, key):
"""
增加一个顶点
:param key: 顶点的键
:return:
"""
self.numVertices += 1
newVertex = Vertex(key) # 实例化顶点对象,创建一个顶点
self.vertList[key] = newVertex
return newVertex
def getVertex(self, n):
"""
通过键获取此节点的信息
:param n: 键
:return:
"""
if n in self.vertList:
return self.vertList[n]
else:
return None
def __contains__(self, n):
"""实现了in方法"""
return n in self.vertList
def addEdge(self, f, t, cost=0):
"""增加两个顶点的边,要是顶点不存在,则创建顶点"""
if f not in self.vertList:
nv = self.addVertex(f)
if t not in self.vertList:
nv = self.addVertex(t)
self.vertList[f].addNeighbor(self.vertList[t], cost)
def getVertices(self):
"""得到所有的顶点的键"""
return self.vertList.keys()
def __iter(self):
"""图中所有顶点对象的遍历"""
return iter(self.vertList.values())
运用刚刚定义的
Graph
和Vertex
类,我们通过下面的 Phython代码可以实现下图所表示的图。
首先,我们创造六个顶点并将其从 0 编码到 5,然后我们显示Vertex
字典。注意到从 0 到 5 的每一个 key 我们都已经创建了一个Vertex
实例。接下来,我们添加边来将顶点之间连接起来。最后,用一个嵌套循环来核实图表的每条边都被正确地储存了起来。在这个过程的最后还应该检查“边”列表的输出是否与上图 一致。
# 测试代码
if __name__ == "__main__":
g = Graph() # 创建图
for i in range(6): # 将其从 0 编码到 5
g.addVertex(i)
print(g.vertList)
# 添加边
g.addEdge(0, 1, 5)
g.addEdge(0, 5, 2)
g.addEdge(1, 2, 4)
g.addEdge(2, 3, 9)
g.addEdge(3, 4, 7)
g.addEdge(3, 5, 3)
g.addEdge(4, 0,