2021 继往开来,继续学习数据结构。
9.1一节简述图的基本概念及存储结构,这一节我们深入看一下图的存储,学习图的遍历方法,实现图的存储和遍历。
图的存储
首先说最容易理解的临接矩阵,矩阵需要n平方个元素的存储空间,声明的又是连续的空间地址。由于计算机内存的限制,存储的顶点数目也是有限的。对于简单的问题和场景,可以考虑使用。对于庞大的图结构,可以使用稀疏矩阵表示,但是应用起来就不太容易了。
所以,还是主要了解临接链表方法。虽然实现稍微复杂,但是有他独特的应用价值,无论是扩展性、应用性还是性能,都是优秀的。
邻接链表由数组 + 链表组成,顶点可以保持在数组中,邻接关系则用链表实现,利用链表高效的插入和删除,实现内存的充分利用。但是查找的时候,只能是全部遍历一次了。
邻接链表的存储也可以进行一定程度的优化或变种。A 使用set
来保存顶点信息,方便顶点的查找和插入删除,甚至直接用map存储顶点及临接点点对应关系;B list代替链表存储顶点的临接点,对于某些操作比较方便,而且相比矩阵型形式,还是节省空间的。为了查找方便,
临接点也可以用set存储。
图的遍历基本法
遍历就是从图中某一个顶点出发遍历途中其余顶点,每一个顶点仅被访问一次。
-
基本思路
(1)树有根节点,每个节点延伸方向最多两个。而图的情况是,没有一个初始节点,每个节点邻接方向也不止一个,顺着一个点,极有可能最后又找到自己,形成回路导致死循环。所以要设置一个数组visit[n],n是图中顶点个数,初值为0,当该顶点被遍历后,修改数组元素的值为1
(2)基于此,图的遍历也是2种遍历方案:深度优先遍历和广度优先遍历。 -
深度优先遍历(DFS)
深度遍历,一个原则就是,对于当前顶点当我们发现有多个出度时,按照统一标准选择下一个遍历的顶点,比如在链表中,选择第一个,当该顶点已遍历过,选择第二个;当所有临接的点都已遍历,则回退,看当前节点入度的其他出度。直到所有的点都被遍历。
-
广度优先遍历(BFS)
类似树的层次遍历,我们先准备一个队列,先入队一个顶点,弹出队列顶端的1个点访问,并把它连接的顶点入队;重复以上过程,直到队列为空。注意弹出的点才能访问,并且确保该点未被访问。
代码实现
我们根据上图,实践一下图的存储
# 临接链表大法存储图
class node:
def __init__(self, val):
self.data = val # 顶点的id/信息
#... 其他信息都可以存
self.adj = None # 出度顶点链表
class linknode:
def __init__(self, v, w=1):
self.val = v
self.weight = w
self.next_ = None
class graph:
def __init__(self, n):
self.node_num = n
self.node_map = dict()
def create(self, edge_lst):
for a, b in edge_lst:
# 创建时 a,b点都要加入到顶点map
if a not in self.node_map.keys():
nodea = node(a)
nodea.adj = linknode(b)
self.node_map[a] = nodea
else:
nodea = self.node_map[a]
adj_link = nodea.adj
# 遍历链表 添加到在哪儿都行 注意判断head
if not adj_link:
nodea.adj = linknode(b)
else:
next_ = adj_link.next_ # 插入到第二位
linkb = linknode(b)
adj_link.next_ = linkb
linkb.next_ = next_
if b not in self.node_map.keys():
nodeb = node(b)
self.node_map[b] = nodeb
def add_edge(self, a, b):
pass
def del_edge(self, a, b):
pass
我们输入顶点的数量和所有边,构造图
# 顶点 8
# 边 9
edges = [(5,11),
(7,11),
(11,2),
(11,9),
(11,10),
(8,9),
(3,10),
(7,8),
(3,8)]
# 我们的代码的用法是 根据顶点数量初始化,然后调用create函数 输入所有边的信息
gra1 = graph(8)
gra.create(edges)
下面我们遍历该图,输出顶点和边,看看是否正确
def traverse(graph):
dq = list() # 队列
visit = set() # 已访问的
dq.extend(graph.node_map.keys())
while len(visit) < graph.node_num:
cur = dq.pop()
if cur not in visit:
node = graph.node_map[cur]
val = node.data
adj = node.adj
print("点: ", val)
if adj is None:
pass
else:
while adj:
print(val, "->", adj.val, "(%d)"%adj.weight)
adj = adj.next_
visit.add(cur)
调用方法看结果,打印出了所有的点和边,成功~
traverse(g1)
out:
点: 3
3 -> 10 (1)
3 -> 8 (1)
点: 8
8 -> 9 (1)
点: 10
点: 9
点: 2
点: 7
7 -> 11 (1)
7 -> 8 (1)
点: 11
11 -> 2 (1)
11 -> 10 (1)
11 -> 9 (1)
点: 5
5 -> 11 (1)
总结两句:
图的实现,大致是顶点列表+邻接关系,把点的集合和临接关系的集合存储好就可以,不必拘泥。临接关系用个list就很好,可以不用链表。
对于遍历,我们既然有顶点列表,用类似广度优先的方式遍历各点及边,倒是容易的很。
对于没有顶点列表的情况,往往只给出一个顶点(类似树 只给一个根节点),而且保证所有的点连通的(还有一些其他的限定条件),这时需要DFS、BFS遍历访问所有节点。同时也可以解决其他场景的问题。我们接下来结合LeetCode题目继续学习。
9.2.2 图的遍历LeetCode题目 —— Find the Town Judge & Clone Graph & Keys and Rooms