用Python实现树的BFS与DFS

一、BFS与DFS简介

理解动态规划、BFS和DFS一文中,已经集合具体例子,介绍了图的BFS与DFS。但是比较复杂,这里就写的简单点吧。

BFS:每次输出一行,所用数据结构为队列
设想我们每次都从左到右、从上到下的去遍历一个图,那么就需要把一行中最左边先进来的先输出,最右边后进来的后输出。所以会用到队列。

DFS:每次深挖到底,所用数据结构为栈
设想我们从图的最上边先按照一条道深挖到最下面,在挖到底以后就需要再逐个返回到上面的顶点,再去遍历父节点是不是还有别的子节点。后进先出的模式,所以需要用到栈。

因为这是在图中,所以,一个顶点的相邻点可能包含着已经搜索过的点,因此这两个遍历都需要加一个集合nodeSet,里面是已经遍历过的点。在搜索过程中,如果发现新的节点已经存在于nodeSet中,那么就不对新节点进行搜索了。

而,如果是在树中用BFS与DFS,因为一个节点顶多有两个子节点,我们已经明确知道这个节点除了子节点以外不会再有相邻节点,因此在搜索过程中也不会遇到重复的节点,所以不需要加nodeSet。只需要按照BFS与DFS的思想与所用数据结构,遍历即可。

二、代码实现

参考 图的广度优先搜索(BFS)与深度优先搜索(DFS) Python实现

2.1、树的广度优先搜索

因为是树,每个node至多有两个子节点,而下面代码中语句是对图中不确定的子节点个数来说的,因此下面的这句

for next in cur.nexts:

都可以用对左右两个子节点的遍历来代替,参考前序遍历那一篇文章。
不过为了体现图的思想,这里都用上面这句代码来实现吧。

  • 1.利用队列实现
  • 2.从源节点开始依次按照宽度进队列,然后弹出
  • 3.每弹出一个节点,就把该节点所有没有进过队列的邻接点放入队列
  • 4.直到队列变空

这里如果需要按照BFS的顺序输出二叉树的元素的话,那么就可以把nodeSet去掉了。

def bfs(node):
    if node is None:
        return
    queue = []
    #nodeSet = set() 
    queue.insert(0,node)
    #nodeSet.add(node)
    while queue:
        cur = queue.pop()               # 弹出元素
        print(cur.val)                # 打印元素值
        for next in cur.nexts:          # 遍历元素的邻接节点
            #if next not in nodeSet:     # 若邻接节点没有入过队,加入队列并登记
                #nodeSet.add(next)
                queue.insert(0,next)

2.2、树的深度优先搜索

2.2.1、用递归的方法进行dfs

这里递归不需要nodeSet

因为想到有栈的思想,那么就不可避免的想到递归。

  • 递归结束条件:节点是None,结束函数调用
  • 递归改变:每次都要把节点添加到节点集合当中去
  • 递归调用:对于每一个当前节点的相邻节点,只要不在节点集合中,就调用dfs进行搜索
#nodeSet = set()
def dfs1(node):
    if node is None:
        return
    #nodeSet.add(node)
    print(node.val)
    #相当于树的前序遍历了,只不过这里把左右子节点放到了nexts的列表中
    for next in node.nexts:
        #if next not in nodeSet:
            dfs1(next)

2.2.2、用循环的方法进行dfs

这个方法其实是图的遍历方法,对于树的DFS,其实不需要用nodeSet,可以参考树3,用循环实现树的三种遍历

若是按照如下方法进行dfs,那么还是要用nodeSet来保证不会重复遍历一些节点了。

用循环的方法,就一定会用到栈了。
对于下面代码,有两个地方需要解释:
1、为什么弹出了cur以后还要把cur压入栈中?

答:为了保证出入栈的顺序,若是cur没有子节点,则直接弹出栈。如果有子节点,那么就要再把cur压入栈,再把cur的子节点压入栈。这样,下一次栈弹出的会是cur的子节点,在子节点遍历完并且再次弹出栈以后,当前节点的索引又会回到父节点cur上。

2、为什么最后要用break语句?

**保持深度优先的顺序,在遍历完cur的左子节点以后,下一步会跳出循环,

  • 你可以看看,若是不用break会是什么效果:以树为例,在搜索完根节点的左子节点以后,紧接着会把右子节点压入栈中。而不是我们预想的把左子节点的左子节点压入栈中。
  • 在对root搜索到左子节点L1以后 ,此时我们想对L1进行搜索,而我们又想到此时此时此时,L1是在栈顶的,那我们何不直接break出循环,直接到对L1进行搜索。
    所以循环中用了一个break来保持深度优先
def dfs(node):
    if node is None:
        return
    nodeSet = set()
    stack = []
    print(node.val)
    nodeSet.add(node)
    stack.append(node)
    while len(stack) > 0:
        cur = stack.pop()               # 弹出最近入栈的节点
        for next in cur.nexts:         # 遍历该节点的邻接节点
            if next not in nodeSet:    # 如果邻接节点不重复
                stack.append(cur)       # 把节点压入
                stack.append(next)      # 把邻接节点压入
                nodeSet.add(next)           # 登记节点
                print(next.val)       # 打印节点值
                break                   # 退出,保持深度优先

用二叉树来检验上述算法

对于TreeNode类,在构造函数中又添加了一个next属性近似代替图中的临近节点列表。

#实现一个二叉树,并且用BFS或DFS去遍历她
class TreeNode:
    def __init__(self,x):
        self.val = x
        self.left = None
        self.right = None
        self.nexts = []

        
root_node = TreeNode(1)
node_2 = TreeNode(2)
node_3 = TreeNode(3)
node_4 = TreeNode(4)
node_5 = TreeNode(5)
node_6 = TreeNode(6)
node_7 = TreeNode(7)
node_8 = TreeNode(8)
node_9 = TreeNode(9)
node_10 = TreeNode(10)

def littleTree(root,left,right):
    root.left = left
    root.right = right
    if root.left:
        root.nexts.append(root.left)
    if root.right:
        root.nexts.append(root.right)

littleTree(root_node, node_2, node_3)
littleTree(node_2, node_4, node_5)
littleTree(node_3, node_6, node_7)
littleTree(node_4, node_8, node_9)
littleTree(node_5, node_10, None)
    原文作者:小碧小琳
    原文地址: https://www.jianshu.com/p/d2125448270b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞