图的遍历
和树的遍历一样,我们希望从图中某一顶点出发遍历图中其余顶点,且使的每一个顶点仅被访问一次。这一过程称之为图的遍历。
图的遍历算法是求解图的连通性、拓扑排序和求解关键路径等算法的基础。
图的遍历通常有两种方法:广度优先搜索(BFS) 和 深度优先搜素(DFS);他们对 无向图 和 有向图 都适用。
广度优先搜索(BFS)
广度优先搜索(BFS): 类似于树的按层次遍历过程。其算法过程如下:
- 假设从图中某个 V 1 V_1 V1 出发,在访问了 V 1 V_1 V1 之后,依次访问 V 1 V_1 V1 的各个未曾访问的相连点 { $V_2、V_6、V_9、V_{12} $ };
- 然后分别从这些相连点出发依次访问它们的连接点,直至所有的连接点被访问到;访问顺序如下:
- { V 3 , V 4 , V 5 } \{ V_3, V_4, V_5 \} {V3,V4,V5};
- { V 7 , V 8 } \{ V_7, V_8 \} {V7,V8};
- { V 1 0 , V 1 1 } \{ V_10, V_11 \} {V10,V11};
- { V 1 3 , V 1 4 } \{ V_13, V_14 \} {V13,V14};
假设有如下的一个有向图,我们需要检索从"you" 能否找到 "mongo":
我们用字典来表示如上有向图:
graph = {}
graph["you"] = ["alice", "bob", "claire"]
graph["bob"] = ["anuj", "peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom", "jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
算法的实现原理:
代码实现如下:
from collections import deque
search_queue = deque() // 创建一个双端队列
search_queue += graph["you"] // 将顶点 "you" 的邻居加入到这个搜索队列
while search_queue: // 只有队列不为空
person = search_queue.popleft() // 就取出第一个人
if person == "mango": # 检查这个节点是不是 "mongo"
print(person + " is a mango seller!") # 是 "mongo"
return True
else:
search_queue += graph[person] // 将这个人的邻居加入搜索队列
return False # 如果到达这里,就说明队列中没有 "mongo"
上述代码存在一个问题,即有些节点被多次添加队列中,例如 “PEGGY”,即会存在多次访问一个节点的情况;如果出现环,会造成无限循环访问部分节点的情况,如下所示:
检查一个人之前,要确认之前没检查过他,这很重要。为此,你可使用一个列表来记录检查过的人。
上述代码修改为如下:
from collections import deque
def BFSearch(name): // 从节点 name 处开始访问
search_queue = queue()
search_queue += graph[name]
searched = [] // 这个数组用于记录检查过的人
while search_queue:
person = search_queue.popleft() # 取出队列中第一个节点
if person not in searched: # 仅当这个人没检查过时才检查
print(person + " is a mango seller!")
return True
else:
search_queue += graph[person] // 将这个人的邻居加入搜索队列
searched.apped[person] # 将这个人标记为检查过
return False
BFSearch("you") # 从节点 "you" 开始搜索
上述代码并不适合含有多个子图的图结构。
运行时间
如果你在你的整个人际关系网中搜索 “mongo”,就意味着你将沿每条边前行(记住,边是从一个节点到另一个节点的箭头或连接),因此运行时间至少为
O
(
边
数
)
O(边数)
O(边数)。
你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,即为
O
(
1
)
O(1)
O(1),因此对每个人都这样做需要的总时间为
O
(
人
数
)
O(人数)
O(人数)。所以,广度优先搜索的运行时间为
O
(
人
数
+
边
数
)
O(人数 + 边数)
O(人数+边数),这通常写作
O
(
V
+
E
)
O(V + E)
O(V+E),其中
V
V
V 为顶点(vertice)数,
E
E
E 为边数。
深度优先搜索
深度优先搜索是优先从一个节点的深度进行搜索,这个过程需要用到栈;
算法的实现过程如下:
序号 | 操作 | 栈 |
---|---|---|
1 | 创建一个栈,用来存储节点 | “you” |
2 | 从栈中取出一个节点,例如:“you” | |
3 | 判断节点是否符合要求,如果符号要求,则退出;如果不符合要求,进入 5 5 5 | |
4 | 不符合要求,把此节点的邻居加入栈 | “alice” “bob” “claire” |
5 | 回到第二步 | “alice” “bob” “claire” |
6 | 如果栈空,说明并没有想要的节点 | None |
代码实现如下:
def DFSearch(name): // 从节点 name 处开始访问
search_stack = [] # 用列表实现栈
search_stack = graph[name] + search_stack # 给栈添加元素
searched = [] // 这个数组用于记录检查过的人
while search_stack:
person = search_stack.pop(0) # 取栈顶节点
if person not in searched: # 仅当这个人没检查过时才检查
print(person + " is a mango seller!")
return True
else:
search_stack = graph[person] + search_stack // 将这个人的邻居加入栈中
searched.apped[person] # 将这个人标记为检查过
return False
DFSearch("you") # 从节点 "you" 开始搜索
上述代码并不适合含有多个子图的图结构。
时间复杂度: O ( V + E ) O(V+E) O(V+E)
小结
- 图的搜索指出是否有从节点 A A A 到节点 B B B 的存在路径;
- BFS 用一个队列来维护待检查节点;DFS 用一个栈来维护待检查节点;
- 对于检查过的人,务必不要再检查,否则可能导致无限循环;
参考资料
- 《算法新解》
- 《数据结构(C语言版)》