一般来讲,实现图的过程中需要有两个自定义的类进行支撑:顶点(Vertex)类,和图(Graph)类。按照这一架构,Vertex类至少需要包含名称(或者某个代号、数据)和邻接顶点两个参数,前者作为顶点的标识,后者形成顶点和顶点相连的边,相应地必须有访问获取和设定参数的方法加以包装。Graph类至少需要拥有一个包含所有点的数据结构(列表或者map等),相应地应该有新增顶点、访问顶点、新增连接边等方法。当然,为了实现Dijkstra算法(一种基本的最短路径算法),除了可以在Graph类里增加一个执行Dijkstra算法的方法以外,还需要在Vertex类里增加用于Dijkstra算法的一些参数:某一个顶点距离Dijkstra搜索起点的距离,以及一旦完成Dijkstra搜索需要回溯路径时,前驱顶点的信息。

在这里记录用python实现以上基本方法和Dijkstra算法的代码,因为Python中的现成的数据结构类型便于使用,比如字典dict类,很方便地能够构造一个类似于map的映射,而且Python的sort方法也特别好用。首先是自定义类顶点(Vertex)的代码,如上所述,为了满足BFS算法的执行和回溯的需要,Vertex类一共有四个参数:id(标记)、connections(邻接顶点字典,键值为邻接顶点,对应值为权重或者边长)、前驱顶点pre和从起点的距离distance。这二者在Dijkstra算法的执行过程中被赋值,在回溯时需要利用pre的信息。

Vertex类的方法中多数为基本的访问参数(get)-设定参数(set)的方法对,比如访问/设定邻接顶点信息的 add_neighbour(neighbour, weight) 和 get_connection() ,访问/设定前驱顶点信息的 get_pre()  和 set_pre(prev) ,以及访问/设定从起点起距离的 get_distance()  和 set_distance(dist) 。除此之外重载了字符串化方法,即__str__,这便于print函数和str函数将Vertex类转化为一个有意义的字符串。

 class Vertex:
# 初始化构造函数,name为字符串,connections字典为<Vertex(class), weight(fl)>
# 前驱顶点pre,从起点距离distance,在Dijkstra执行赋值后有意义
def __init__(self, name):
self.id = name
self.pre = None
self.distance = float('inf')
self.connections = dict() # 重载字符串化函数,返回字符串
def __str__(self):
return str(self.id) + " connected to: " + str([x.id for x in self.connections]) # 增加相邻顶点,neighbour为Vertex类,weight为浮点型边权重
def add_neighbour(self, neighbour, weight=0):
self.connections[neighbour] = weight # 获取顶点id函数
def get_id(self):
return self.id # 获取顶点邻接点的函数,返回键值(Vertex)列表
def get_connections(self):
return self.connections.keys() # 获取顶点与邻接点边权重,传入Vertex类对象neighbour,返回weight(fl)
def get_weight(self, neighbour):
return self.connections[neighbour] # 获取顶点的距离(在BFS执行后使用)
def get_distance(self):
return self.distance # 获取前驱顶点(在Dijkstra执行后使用)
def get_pre(self):
return self.pre # 设定顶点的距离(在Dijkstra执行过程中调用)
def set_distance(self, dist):
self.distance = dist # 设定前驱顶点(在Dijkstra执行过程中调用)
def set_pre(self, prev):
self.pre = prev

图(Graph)类:拥有两个参数:顶点字典(顶点id:顶点Vertex类)和顶点数量。顶点数量这个参数似乎没什么用,除非是增加一个判断图是否为空的 isEmpty() 方法和 getSize() 方法可能用得着。

方法中包含新增顶点的 add_vertex(name) ,按照id获取顶点的 get_vertex(name) 。dict的数据结构给按照id访问顶点在空间上和时间上都创造了极大的方便,也为此参数中为顶点字典而不用顶点列表。添加边的方法 add_edge(vertex1, vertex2, weight) ,其中需要调用Vertex类中设定邻接点的方法。除此之外,Graph类还有一些重载的方法,比如重载contains,这个函数在类似于 3 in list(range(5)) 这样的语句中被调用,比如经常用的,判断某个元素是否在列表内等。重载迭代器__iter__,类似于 for item in list(range(10)): 这样的语句中被调用,有助于图中所有Vertex的遍历。基于迭代器还可以重载字符串化方法__str__。

最后就是Dijkstra算法。关于这个算法的信息准备记录在另一篇随笔中,基本思路是创建一个优先队列(即距离起始点路程从小到大的队列),每次查看列表中路程最短的点A,观察在这个顶点A的邻接点中,是否有可能因为通过顶点A而使得该邻接点的路程缩减。每次这样一轮操作结束以后就从队列中删去这个点A,然后把队列重新排列一遍。在以前课上的学习中,优先队列使用的是二叉堆(Binary Heap)实现,在这里我直接调用了Python内置的sort函数,虽然计算复杂度不敢保证,但是从后面用的实际例子的效率来说也没有任何影响。

 class Graph:
# 无参数构造函数,vertex_dict为<id(str),Vertex>映射字典
def __init__(self):
self.vertex_dict = dict()
self.vertex_num = 0 # 增加顶点函数,传入新增顶点id
def add_vertex(self, name):
if name not in self.vertex_dict.keys():
new_vertex = Vertex(name)
self.vertex_dict[name] = new_vertex
self.vertex_num = self.vertex_num + 1 # 增加边函数, 传入顶点1名称、顶点2名称、权重
def add_edge(self, vertex1, vertex2, weight):
if vertex1 not in self.vertex_dict:
self.add_vertex(vertex1)
if vertex2 not in self.vertex_dict:
self.add_vertex(vertex2)
self.vertex_dict[vertex1].add_neighbour(self.vertex_dict[vertex2], weight)
self.vertex_dict[vertex2].add_neighbour(self.vertex_dict[vertex1], weight) # 按照id检索顶点函数,传入id,返回Vertex类
def get_vertex(self, name):
if name in self.vertex_dict.keys():
return self.vertex_dict[name]
else:
return None # 重载contains方法,传入id,返回bool值
def __contains__(self, item):
return item in self.vertex_dict # 重载迭代器,返回对应迭代器
def __iter__(self):
return iter(self.vertex_dict.values()) # 重载字符串化方法,返回字符串
def __str__(self):
o_str = str()
for item in self:
o_str = o_str + str(item) + '\n'
return o_str # Dijkstra算法,传入起点
def dijkstra_search(self, start):
# 优先队列priority
priority = list(self.vertex_dict.values())
# 起点距离置零
start.set_distance(0)
# 优先队列重排
priority.sort(key=lambda x: x.get_distance(), reverse=True)
while priority:
# 重排标记changed,若存在顶点发生distance变化则标记为True
changed = False
# 弹出最高优先顶点current
current = priority.pop()
# 遍历current邻接顶点
for vertex_tmp in current.get_connections():
dist_tmp = current.get_distance() + current.get_weight(vertex_tmp)
# 若发现优势路径则更改邻接顶点的distance和前驱顶点pre
if dist_tmp < vertex_tmp.get_distance():
vertex_tmp.set_distance(dist_tmp)
vertex_tmp.set_pre(current)
changed = True
# 若有更改则重排优先队列
if changed:
priority.sort(key=lambda x: x.get_distance(), reverse=True)

最后,和Dijkstra算法配合还需要一个路径回溯的方法。本来把回溯作为图类的一个方法也可以,但是由于通过访问顶点的pre参数已经可以回溯到上一个Vertex类,中间不涉及到图的操作,因此可以安全地把它写成一个静态方法的形式,即不放在Graph类中:

 # 回溯路径函数,传入参数终点destination,返回路径列表list(Vertex)
def reverse_trace(destination):
current = destination
trace = [destination]
while current.get_pre():
current = current.get_pre()
trace.append(current)
trace.reverse()
return trace

这个函数返回的是一个路径中顺序出现的顶点Vertex类列表。

作为图的一个练习和测试,我拿了北京市的地铁信息。首先我在某一个文件夹里新建了编号为line1到line10的10个.txt文件,录入了10条地铁线的信息。后来又新增了13号线和15号线的信息。这些txt里的信息是这样储存的:a)一行一个站名,表示起点站;b)一行一个站名+一个数字,表示一个站和前一站的距离;c)一行两个站名+一个数字,表示两个站之间的距离。然后可以写一个读入的函数,把这些信息读进来,每个站名就是一个id,形成一张图,选定一个起点S以后执行Dijkstra算法,然后选定一个终点D回溯,就得到了从S到D的最短路径。

如果只是想得到站与站之间的最短距离,这个方法当然是合适的;不过,如果考虑到换乘因素,就会发现这个算法没有考虑到换乘的时间代价,这会使得搜索出一些需要换乘很多次的结果,而这在现实生活中并不是最快的。一个改进成本很小的方案是,把每个站所属的线路号码加在站名后面作为id的一部分,比如用“王府井1”作为一个id。同时,利用存储形式c来规定换乘站的间距,比如规定“海淀黄庄4”和“海淀黄庄10”的距离。这样一来就可以考虑换乘代价了。在这个思路下,Graph类相应的读入函数:

 class Graph:
# 读取顶点文件(适用于地铁路线例子的方法),传入文件路径path和线路编号subway
def read_in(self, path, subway):
with open(path, 'r') as file:
# 按行读取文件
line = file.readline()
station = None
while line:
info_list = line.split()
# 当行中仅含有1个元素时,仅创建
if len(info_list) == 1:
station = info_list[0]
station = station + str(subway)
self.add_vertex(station)
# 当行中含有2个元素时,第1个元素为车站名称,第2个元素为距上个车站距离
elif len(info_list) == 2:
pre_station = station
[station, distance] = info_list
distance = int(distance)
station = station + str(subway)
self.add_vertex(station)
self.add_edge(pre_station, station, distance)
# 否则当行中含有3个元素,前2个元素为车站,第3个元素为前二者间距
else:
[station1, station2, distance] = info_list
distance = int(distance)
self.add_edge(station1, station2, distance)
line = file.readline()

最后是初始化函数和个人偏爱的交互式菜单代码,仅供参考:

 # 初始化函数(适用于地铁路线例子的方法)
# 读入在file_path所示文件夹下的北京地铁线数据,返回生成的图
def initialize():
graph = Graph()
file_path = 'D:\Personal Documents\Project\BeijingSubway\line'
for i in range(1, 11):
path = file_path + str(i) + '.txt'
graph.read_in(path, i)
path = file_path + '13.txt'
graph.read_in(path, 13)
path = file_path + '15.txt'
graph.read_in(path, 15)
return graph # 交互式菜单函数(适用于地铁路线例子的方法),传入图
def route_find_menu(graph):
print('>> 寻找乘地铁最短路线,输入0退出')
start = input('>> 输入起始站名+地铁线(如“王府井1”):')
while start != '':
graph.breadth_first_search(graph.get_vertex(start))
destination = input('>> 输入终点站名+地铁线(如“王府井1”):')
if destination == '':
break
trace_list = reverse_trace(graph.get_vertex(destination))
print([x.get_id() for x in trace_list])
start = input('>> 输入起始站名:')

最后只要分别调用 graph1 = initialize() 创建实例,并且用 route_find_menu(graph1) 进入菜单即可。

图:无向图(Graph)基本方法及Dijkstra算法的实现 [Python]的更多相关文章

  1. 分析函数调用关系图(call graph)的几种方法

    绘制函数调用关系图对理解大型程序大有帮助.我想大家都有过一边读源码(并在头脑中维护一个调用栈),一边在纸上画函数调用关系,然后整理成图的经历.如果运气好一点,借助调试器的单步跟踪功能和call sta ...

  2. 最短路径——Dijkstra算法和Floyd算法

    Dijkstra算法概述 Dijkstra算法是由荷兰计算机科学家狄克斯特拉(Dijkstra)于1959 年提出的,因此又叫狄克斯特拉算法.是从一个顶点到其余各顶点的最短路径算法,解决的是有向图(无 ...

  3. 最短路径Dijkstra算法和Floyd算法整理、

    转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijks ...

  4. 【转】最短路径——Dijkstra算法和Floyd算法

    [转]最短路径--Dijkstra算法和Floyd算法 标签(空格分隔): 算法 本文是转载,原文在:最短路径-Dijkstra算法和Floyd算法 注意:以下代码 只是描述思路,没有测试过!! Di ...

  5. HTTP协议漫谈 C#实现图(Graph) C#实现二叉查找树 浅谈进程同步和互斥的概念 C#实现平衡多路查找树(B树)

    HTTP协议漫谈   简介 园子里已经有不少介绍HTTP的的好文章.对HTTP的一些细节介绍的比较好,所以本篇文章不会对HTTP的细节进行深究,而是从够高和更结构化的角度将HTTP协议的元素进行分类讲 ...

  6. CSharpGL(28)得到高精度可定制字形贴图的极简方法

    CSharpGL(28)得到高精度可定制字形贴图的极简方法 回顾 以前我用SharpFont实现了解析TTF文件从而获取字形贴图的功能,并最终实现了用OpenGL渲染文字. 使用SharpFont,美 ...

  7. 看开源代码利器—用Graphviz + CodeViz生成C/C++函数调用图(call graph)

    一.Graphviz + CodeViz简单介绍 CodeViz是<Understanding The Linux Virtual Memory Manager>的作者 Mel Gorma ...

  8. 最短路径之Dijkstra算法和Floyd-Warshall算法

    最短路径算法 最短路径算法通常用在寻找图中任意两个结点之间的最短路径或者是求全局最短路径,像是包括Dijkstra.A*.Bellman-Ford.SPFA(Bellman-Ford的改进版本).Fl ...

  9. 【转载】Dijkstra算法和Floyd算法的正确性证明

      说明: 本文仅提供关于两个算法的正确性的证明,不涉及对算法的过程描述和实现细节 本人算法菜鸟一枚,提供的证明仅是自己的思路,不保证正确,仅供参考,若有错误,欢迎拍砖指正   ----------- ...

随机推荐

  1. Java集合----概述、Collection接口、Iterator接口

    Java 集合概述 Java 集合就像一种容器,可以把多个对象的引用放入容器中. Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组 Java 集合可分为 Set.Li ...

  2. python 数据类型详解(转)

    转自:http://www.cnblogs.com/linjiqin/p/3608541.html 目录1.字符串2.布尔类型3.整数4.浮点数5.数字6.列表7.元组8.字典9.日期 1.字符串1. ...

  3. python2.0_day19_后台数据库设计思路

    from django.db import models # Create your models here. from django.contrib.auth.models import User ...

  4. Spring new对象时注解失效

    新建了一个新类(A)后,在类中用注解将属性注入.类名(A)上用 @Component ,类中的一个属性(name)上用 @Autowired或@Resource, 方法(fun)中通过 属性(name ...

  5. @synthesize obj=_obj的意义详解 @property和@synthesize

    本文转载至 http://blog.csdn.net/ztp800201/article/details/9231969 http://hi.baidu.com/feng20068123/item/c ...

  6. Docker源码分析(九):Docker镜像

    1.前言 回首过去的2014年,大家可以看到Docker在全球刮起了一阵又一阵的“容器风”,工业界对Docker的探索与实践更是一波高过一波.在如今的2015年以及未来,Docker似乎并不会像其他昙 ...

  7. Linux学习(四)档案与目录管理

    1. 目录与路径  1.1 相对路径与绝对路径  1.2 目录的相关操作: cd, pwd, mkdir, rmdir  1.3 关于执行文件路径的变量: $PATH2. 档案与目录管理  2.1 档 ...

  8. strace命令的使用

    author: headsen  chen date: 2018-08-28   21:25:48 跟踪一个命令的过程: [root@zabbix-test ~]# yum -y install st ...

  9. 单用户模式进入centos

    修改root密码----------------单用户模式操作 个人原创博客,转载请注明,否则追究法律责任 author: headsen chen date: 2017-9-30 1,开机后,迅速按 ...

  10. Python - 3.6 学习第一天

    开始之前 基础示例 Python语法基础,python语法比较简单,采用缩紧方式. # print absolute value of a integer a = 100 if a >= 0: ...