树的广度优先搜索

1,451 阅读4分钟

广度优先搜索

理解

广度优先搜索(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单向广度可能效率更高。

广度/深度优先搜索 两者比较

  • 广度优先搜索,相对于深度优先搜索,没有函数的嵌套调用和回溯操作,所以运行速度比较快。但是,随着搜索过程的进行,广度优先需要在队列中存放新遇到的所有结点,因此占用的存储空间通常比深度优先搜索多。
  • 相比之下,深度优先搜索法只保留用于回溯的结点,而扩展完的结点会从栈中弹出并被删除。所以深度优先搜索占用空间相对较少。不过,深度优先搜索的速度比较慢,而并不适合查找结点之间的最短路径这类的应用。