广度优先搜索
理解
广度优先搜索(Breadth First Search),也叫宽度优先搜索。简称BFS
- 在遍历树或者图的时候,如果使用深度优先的策略,被发现的结点数量可能呈指数级增长。如果我们不需要全量遍历,而 更关心的是最近的相连结点,比如社交关系中的二度好友,那么这种情况下,广度优先策略更高效。
- 我们不能再使用 递归编程 或者 栈 的数据结构来实现广度优先,而是需要用到具有先进先出特点的 队列。
案例
构建关系网络,按顺序排列好友(1度好友、2度好友。。。)。 (注意:需要去重,防止回路)
# 构造一个关系网络:
import random
class Node:
def __init__(self,user_id,degree=0):
self.user_id = user_id
self.friends = set()
self.degree = degree
def __str__(self):
return "{}:{}".format(self.user_id,self.friends)
user_num = 10
relation_num = 15
user_nodes = []
# 生成所有表示用户的结点
for i in range(user_num):
user_nodes.append(Node(i))
# 生成所有表示好友关系的边
for i in range(relation_num):
a_id = random.randint(0,user_num-1)
b_id = random.randint(0,user_num-1)
if a_id == b_id:
continue
a_node = user_nodes[a_id]
b_node = user_nodes[b_id]
a_node.friends.add(b_id)
b_node.friends.add(a_id)
for user_node in user_nodes:
print(user_node)
# 运行结果:
0:set([1, 7])
1:set([0, 9, 6])
2:set([8, 3, 4, 5])
3:set([2, 5])
4:set([2, 7])
5:set([2, 3])
6:set([1])
7:set([0, 4])
8:set([2])
9:set([1])
import queue
def bfs(user_nodes, user_id):
"""
@Description: 通过广度优先搜索,查找好友
@param user_nodes- 用户的结点;user_id- 给定的用户 ID,我们要为这个用户查找好友
@return void
"""
if user_id > len(user_nodes):
return
q = queue.Queue()
q.put(user_id)
visited = set()
visited.add(user_id)
print(user_id)
while not q.empty():
current_id = q.get_nowait()
if current_id is None or not user_nodes[current_id]:
continue
if user_nodes[current_id].friends:
for friend_id in user_nodes[current_id].friends:
if friend_id in visited:
continue
visited.add(friend_id)
q.put(friend_id)
user_nodes[friend_id].degree = user_nodes[current_id].degree+1
print("{}度好友:{}".format(user_nodes[friend_id].degree,friend_id))
bfs(user_nodes,2)
# 好友排列结果:
1度好友:8
1度好友:3
1度好友:4
1度好友:5
2度好友:7
3度好友:0
4度好友:1
5度好友:9
5度好友:6
应用
- 最短路径问题(shorterst-path problem)
- 数据分析中的聚合操作:嵌套型聚合
嵌套型聚合
- 广度优先策略可以帮助我们大幅优化数据分析中的聚合操作,聚合是数据分析中一个很常见的操作。比如sql中的“group by”。往往只需要前若干个结果就足以满足需求了,在这种情况下,完全基于排列的设计就有优化的空间了。对于只需要返回前若干结果的应用场景,我们可以对图中的树状结构进行剪枝,去掉绝大部分不需要的结点和边,这样就能节省大量的内存和 CPU 计算。
- 比如,如果我们只需要返回前 100 个参与项目最多的用户,那么就没有必要按照深度优先的策略,去扩展树中高度为 2 和 3 的结点了,而是应该使用广度优先策略,首先找出所有高度为 1 的结点,根据项目数量进行排序,然后只取出前 100 个,把计数器的数量从 5 万个一下子降到 100 个。
双向广度优先搜索
定义
- 巧妙地运用了两个方向的广度优先搜索,大幅降低了搜索的度数,避免数量是呈指数级增长
案例
如何高效地求两个用户的最短路径?
# 构造一个关系网络:
import random
class Node:
def __init__(self,user_id,degree=0):
self.user_id = user_id
self.friends = set()
self.degree = {user_id,0}
def __str__(self):
return "{}:{}".format(self.user_id,self.friends)
user_num = 10
relation_num = 15
user_nodes = []
# 生成所有表示用户的结点
for i in range(user_num):
user_nodes.append(Node(i))
# 生成所有表示好友关系的边
for i in range(relation_num):
a_id = random.randint(0,user_num-1)
b_id = random.randint(0,user_num-1)
if a_id == b_id:
continue
a_node = user_nodes[a_id]
b_node = user_nodes[b_id]
a_node.friends.add(b_id)
b_node.friends.add(a_id)
for user_node in user_nodes:
print(user_node)
import queue
def get_next_degree_friend(user_nodes, que, visited):
"""
:param user_nodes: 用户节点网络
:param que: 某一层用户节点 即第几度好友
:param visited: 已访问的所有用户节点
:return:
"""
que_return = queue.Queue() # 只保存某个用户的第几度好友
visited_return = set() # 保存从某个用户开始到第几度好友
while not que.empty():
current_user_id = que.get()
if user_nodes[current_user_id] is None:
continue
for friend_id in user_nodes[current_user_id].friends:
if user_nodes[friend_id] is None:
continue
if friend_id in visited:
continue
que_return.put(friend_id)
visited_return.add(friend_id) # 记录已经访问过的节点
return que_return, visited_return
def hasOverlap(visited_a,visited_b):
return len(visited_a & visited_b) > 0
def bi_bfs(user_nodes, user_id_a, user_id_b):
"""
@Description: 通过双向广度优先搜索,查找两人的最短通路的长度
@param user_nodes- 用户的结点;user_id_a- 给定的用户a的ID;user_id_b- 给定的用户b的ID
@return void
"""
# 边界检查:
if user_id_a>len(user_nodes) or user_id_b>len(user_nodes):
return -1
if user_id_a == user_id_b:
return 0
# 初始化两个队列以及去重集合
q_a = queue.Queue()
q_b = queue.Queue()
visited_a = set()
visited_b = set()
q_a.put(user_id_a)
q_b.put(user_id_b)
visited_a.add(user_id_a)
visited_b.add(user_id_b)
#
degree_a = 0
degree_b = 0
max_degree = 20
while degree_a + degree_b < max_degree:
degree_a+=1
q_a, visited_a = get_next_degree_friend(user_nodes, q_a, visited_a)
if hasOverlap(visited_a, visited_b):
return degree_a + degree_b
degree_b+=1
q_b, visited_b = get_next_degree_friend(user_nodes, q_b, visited_b)
if hasOverlap(visited_a, visited_b):
return degree_a + degree_b
return -1
bi_bfs(user_nodes,1,3)
应用:
- 解决最短路径问题(shorterst-path problem)
注意:
- 如果两个出发点a和b,如果a出发的图平均连接度明显大于b出发的图,那么从b单向广度可能效率更高。
广度/深度优先搜索 两者比较
- 广度优先搜索,相对于深度优先搜索,没有函数的嵌套调用和回溯操作,所以运行速度比较快。但是,随着搜索过程的进行,广度优先需要在队列中存放新遇到的所有结点,因此占用的存储空间通常比深度优先搜索多。
- 相比之下,深度优先搜索法只保留用于回溯的结点,而扩展完的结点会从栈中弹出并被删除。所以深度优先搜索占用空间相对较少。不过,深度优先搜索的速度比较慢,而并不适合查找结点之间的最短路径这类的应用。