DFS和BFS详解

1.前言
和树的遍历类似,图的遍历也是从图中某点出发,然后按照某种方法对图中所有顶点进行访问,且仅访问一次。

但是图的遍历相对树而言要更为复杂。因为图中的任意顶点都可能与其他顶点相邻,所以在图的遍历中必须记录已被访问的顶点,避免重复访问。

根据搜索路径的不同,我们可以将遍历图的方法分为两种:广度优先搜索和深度优先搜索。

2.图的基本概念

2.1.无向图和无向图
顶点对(u,v)是无序的,即(u,v)和(v,u)是同一条边。常用一对圆括号表示。

在这里插入图片描述

顶点对<u,v>是有序的,它是指从顶点u到顶点 v的一条有向边。其中u是有向边的始点,v是有向边的终点。常用一对尖括号表示。

在这里插入图片描述

2.2.权和网
图的每条边上可能存在具有某种含义的数值,称该数值为该边上的权。而这种带权的图被称为网。

2.3.连通图与非连通图
连通图:在无向图G中,从顶点v到顶点v’有路径,则称v和v’是联通的。若图中任意两顶点v、v’∈V,v和v’之间均联通,则称G是连通图。上述两图均为连通图。

非连通图:若无向图G中,存在v和v’之间不连通,则称G是非连通图。

在这里插入图片描述

3.广度优先搜索

3.1.算法的基本思路
广度优先搜索类似于树的层次遍历过程。它需要借助一个队列来实现。如图2-1-1所示,要想遍历从v0到v6的每一个顶点,我们可以设v0为第一层,v1、v2、v3为第二层,v4、v5为第三层,v6为第四层,再逐个遍历每一层的每个顶点。

具体过程如下:

1.准备工作:创建一个visited数组,用来记录已被访问过的顶点;创建一个队列,用来存放每一层的顶点;初始化图G。

2.从图中的v0开始访问,将的visited[v0]数组的值设置为true,同时将v0入队。

3.只要队列不空,则重复如下操作:

(1)队头顶点u出队。

(2)依次检查u的所有邻接顶点w,若visited[w]的值为false,则访问w,并将visited[w]置为true,同时将w入队。

3.2.算法的实现过程
白色表示未被访问,灰色表示即将访问,黑色表示已访问。

visited数组:0表示未访问,1表示以访问。

队列:队头出元素,队尾进元素。

1.初始时全部顶点均未被访问,visited数组初始化为0,队列中没有元素。

在这里插入图片描述

2.即将访问顶点v0。

在这里插入图片描述

3.访问顶点v0,并置visited[0]的值为1,同时将v0入队。

在这里插入图片描述

4.将v0出队,访问v0的邻接点v2。判断visited[2],因为visited[2]的值为0,访问v2。

在这里插入图片描述
5.将visited[2]置为1,并将v2入队。

在这里插入图片描述
6.访问v0邻接点v1。判断visited[1],因为visited[1]的值为0,访问v1。

在这里插入图片描述

7.将visited[1]置为0,并将v1入队。

在这里插入图片描述
8.判断visited[3],因为它的值为0,访问v3。将visited[3]置为0,并将v3入队。

在这里插入图片描述

9.v0的全部邻接点均已被访问完毕。将队头元素v2出队,开始访问v2的所有邻接点。

开始访问v2邻接点v0,判断visited[0],因为其值为1,不进行访问。

继续访问v2邻接点v4,判断visited[4],因为其值为0,访问v4,如下图:

在这里插入图片描述

10.将visited[4]置为1,并将v4入队。

在这里插入图片描述

11.v2的全部邻接点均已被访问完毕。将队头元素v1出队,开始访问v1的所有邻接点。

开始访问v1邻接点v0,因为visited[0]值为1,不进行访问。

继续访问v1邻接点v4,因为visited[4]的值为1,不进行访问。

继续访问v1邻接点v5,因为visited[5]值为0,访问v5,如下图:

在这里插入图片描述

12.将visited[5]置为1,并将v5入队。

在这里插入图片描述

13.v1的全部邻接点均已被访问完毕,将队头元素v3出队,开始访问v3的所有邻接点。

开始访问v3邻接点v0,因为visited[0]值为1,不进行访问。

继续访问v3邻接点v5,因为visited[5]值为1,不进行访问。

在这里插入图片描述

14.v3的全部邻接点均已被访问完毕,将队头元素v4出队,开始访问v4的所有邻接点。

开始访问v4的邻接点v2,因为visited[2]的值为1,不进行访问。

继续访问v4的邻接点v6,因为visited[6]的值为0,访问v6,如下图:

在这里插入图片描述

15.将visited[6]值为1,并将v6入队。

在这里插入图片描述

16.v4的全部邻接点均已被访问完毕,将队头元素v5出队,开始访问v5的所有邻接点。

开始访问v5邻接点v3,因为visited[3]的值为1,不进行访问。

继续访问v5邻接点v6,因为visited[6]的值为1,不进行访问。

在这里插入图片描述

17.v5的全部邻接点均已被访问完毕,将队头元素v6出队,开始访问v6的所有邻接点。

开始访问v6邻接点v4,因为visited[4]的值为1,不进行访问。

继续访问v6邻接点v5,因为visited[5]的值文1,不进行访问。

在这里插入图片描述

18.队列为空,退出循环,全部顶点均访问完毕。

在这里插入图片描述

最后贴上两个小Demo

DFS:

from collections import deque
from collections import namedtuple


def bfs(start_node, end_node, graph):
    node = namedtuple('node', 'name, from_node')
    search_stack = deque()  # 这里当作栈使用
    name_search = deque()
    visited = {}

    search_stack.append(node(start_node, None))
    name_search.append(start_node)
    path = []
    path_len = 0

    print('开始搜索...')
    while search_stack:
        print('待遍历节点: ', name_search)
        current_node = search_stack.pop()  # 使用栈模式,即后进先出,这是DFS的核心
        name_search.pop()
        if current_node.name not in visited:
            print('当前节点: ', current_node.name, end=' | ')
            if current_node.name == end_node:
                pre_node = current_node
                while True:
                    if pre_node.name == start_node:
                        path.append(start_node)
                        break
                    else:
                        path.append(pre_node.name)
                        pre_node = visited[pre_node.from_node]
                path_len = len(path) - 1
                break
            else:
                visited[current_node.name] = current_node
                for node_name in graph[current_node.name]:
                    if node_name not in name_search:
                        search_stack.append(node(node_name, current_node.name))
                        name_search.append(node_name)
    print('搜索完毕,路径为:', path[::-1], "长度为:", path_len)  # 这里不再是最短路径,深度优先搜索无法一次给出最短路径


if __name__ == "__main__":

    graph = dict()
    graph[1] = [3, 2]
    graph[2] = [5]
    graph[3] = [4, 7]
    graph[4] = [6]
    graph[5] = [6]
    graph[6] = []
    graph[7] = [8]
    graph[8] = []
    bfs(1, 8, graph)

# python的deque根据pop还是popleft可以当成栈或队列使用,
# DFS的能够很快给出解,但不一定是最优解。
# 深度优先搜索的适用场景: 针对深度很深或者深度不确定的图或者权值不相同的图可以适用DFS,
#                        优势在于节省资源,但想要得到最优解需要完整遍历后比对所有路径选取最优解。

BFS:

# 从collections文件中引入deque包
from collections import deque
# tuple元组的item只能通过index访问,
# collections模块的namedtuple子类不仅可以使用item的index访问item, 还可以通过item的name进行访问
# 从collections文件中引入nametuple包
from collections import namedtuple


def bfs(start_node, end_node, graph):  # 开始节点  目标节点 图字典
    node = namedtuple('node', 'name, from_node')    # 使用namedtuple定义节点,用于存储前置节点 node是结点 name, from node 为它的来源的结点的名字
    search_queue = deque()  # 使用双端队列,这里当作队列使用,根据先进先出获取下一个遍历的节点
    name_search = deque()   # 存储队列中已有的节点名称
    visited = {}            # 存储已经访问过的节点

    search_queue.append(node(start_node, None))  # 填入初始节点,从队列后面加入
    name_search.append(start_node)               # 填入初始节点名称
    path = []               # 用户回溯路径
    path_len = 0            # 路径长度

    print('开始搜索...')
    while search_queue:     # 只要搜索队列中有数据就一直遍历下去
        print('待遍历节点: ', name_search)
        current_node = search_queue.popleft()  # 从队列前边获取节点,即先进先出,这是BFS的核心
        name_search.popleft()                  # 将名称也相应弹出
        if current_node.name not in visited:   # 当前节点是否被访问过
            print('当前节点: ', current_node.name, end=' | ')
            if current_node.name == end_node:  # 退出条件,找到了目标节点,接下来执行路径回溯和长度计算
                pre_node = current_node        # 路径回溯的关键在于每个节点中存储的前置节点 先记录下当前结点
                while True:                    # 开启循环直到找到开始节点
                    if pre_node.name == start_node:  # 退出条件:前置节点为开始节点
                        path.append(start_node)      # 退出前将开始节点也加入路径,保证路径的完整性
                        break
                    else:
                        path.append(pre_node.name)   # 不断将前置节点名称加入路径
                        pre_node = visited[pre_node.from_node]  # 取出前置节点的前置节点,依次类推
                path_len = len(path) - 1       # 获得完整路径后,长度即为节点个数-1
                break
            else:
                visited[current_node.name] = current_node   # 如果没有找到目标节点,将节点设为已访问,并将相邻节点加入搜索队列,继续找下去
                for node_name in graph[current_node.name]:  # 遍历相邻节点,判断相邻节点是否已经在搜索队列
                    if node_name not in name_search:        # 如果相邻节点不在搜索队列则进行添加
                        search_queue.append(node(node_name, current_node.name))
                        name_search.append(node_name)
    print('搜索完毕,最短路径为:', path[::-1], "长度为:", path_len)  # 打印搜索结果

# 调用主函数
if __name__ == "__main__":

    # 使用字典表示有向图
    graph = dict()
    graph[1] = [3, 2]
    graph[2] = [5]
    graph[3] = [4, 7]
    graph[4] = [6]
    graph[5] = [6]
    graph[6] = [8]
    graph[7] = [8]
    graph[8] = []
    # 执行搜索
    bfs(1, 8, graph)

# 广度优先搜索的适用场景:只适用于深度不深且权值相同的图,搜索的结果为最短路径或者最小权值和
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值