【问题标题】:Breadth First Vs Depth First广度优先与深度优先
【发布时间】:2010-10-15 19:43:56
【问题描述】:

在遍历树/图时,广度优先和深度优先有什么区别?任何编码或伪代码示例都会很棒。

【问题讨论】:

  • 你查过维基百科(depth first,breadth first)吗?这些页面上有代码示例,还有很多漂亮的图片。
  • 我也有这个想法,但是给出的例子比维基百科上的例子要好一些......
  • 看到这个视觉example。虽然我想将此作为答案发布,但因为它只是一个链接,它会否决投票,因此是评论。

标签: algorithm recursion depth-first-search breadth-first-search tree-traversal


【解决方案1】:

理解条款:

这张图片应该让您了解使用breadth和depth这两个词的上下文。


深度优先搜索:

  • 深度优先搜索算法的行为就像它想走得越远 尽快从起点出发。

  • 它一般使用Stack来记住它到达死胡同时应该去哪里。

  • 遵循的规则:将第一个顶点 A 推到 Stack

    1. 如果可能,访问相邻的未访问顶点,将其标记为已访问,然后将其压入堆栈。
    2. 如果您不能遵循规则 1,则尽可能从堆栈中弹出一个顶点。
    3. 如果您不能遵循规则 1 或规则 2,那么您就完了。
  • Java 代码:

    public void searchDepthFirst() {
        // Begin at vertex 0 (A)
        vertexList[0].wasVisited = true;
        displayVertex(0);
        stack.push(0);
        while (!stack.isEmpty()) {
            int adjacentVertex = getAdjacentUnvisitedVertex(stack.peek());
            // If no such vertex
            if (adjacentVertex == -1) {
                stack.pop();
            } else {
                vertexList[adjacentVertex].wasVisited = true;
                // Do something
                stack.push(adjacentVertex);
            }
        }
        // Stack is empty, so we're done, reset flags
        for (int j = 0; j < nVerts; j++)
            vertexList[j].wasVisited = false;
    }
    
  • Applications:深度优先搜索通常用于模拟游戏(以及现实世界中的类似游戏的情况)。在典型的游戏中,您可以选择几个可能的动作之一。每个选择都会导致进一步的选择,每个选择都会导致进一步的选择,依此类推,形成一个不断扩大的可能性树形图。


广度优先搜索:

  • 广度优先搜索算法喜欢尽可能靠近 到起点。
  • 这种搜索一般使用Queue实现。
  • 要遵循的规则:将起始顶点 A 设为当前顶点
    1. 访问与当前顶点相邻的下一个未访问的顶点(如果有的话),对其进行标记,并将其插入队列中。
    2. 如果由于没有更多未访问的顶点而无法执行规则 1,请从队列中移除一个顶点(如果可能)并将其设为当前顶点。
    3. 如果您因为队列为空而无法执行规则 2,那么您就完成了。
  • Java 代码:

    public void searchBreadthFirst() {
        vertexList[0].wasVisited = true;
        displayVertex(0);
        queue.insert(0);
        int v2;
        while (!queue.isEmpty()) {
            int v1 = queue.remove();
            // Until it has no unvisited neighbors, get one
            while ((v2 = getAdjUnvisitedVertex(v1)) != -1) {
                vertexList[v2].wasVisited = true;
                // Do something
                queue.insert(v2);
            }
        }
        // Queue is empty, so we're done, reset flags
        for (int j = 0; j < nVerts; j++) 
            vertexList[j].wasVisited = false;
    }
    
  • Applications:广度优先搜索首先找到距离起点一条边的所有顶点,然后找到距离起点两条边的所有顶点,以此类推。如果您试图找到从起始顶点到给定顶点的最短路径,这将非常有用。

希望这足以理解广度优先和深度优先搜索。为了进一步阅读,我会推荐 Robert Lafore 的一本优秀的数据结构书籍中的 Graphs 章节。

【讨论】:

  • 如果我还有十个投票权,我会这样做。
  • @snr 你可以奖励赏金 ;)
  • 谢谢@Snow,很高兴你们发现我的回答很有用。
  • @YogeshUmeshVaity 很好的答案——这是一个很好解释的顶级 StackOverflow/StackExchange 回复的经典示例。 id="dmid://uu745gurulevel1622917502"
  • @mcvkr,不用担心。只是因为您认为答案值得奖励这一事实,我感到受宠若惊。
【解决方案2】:

给定这个二叉树:

广度优先遍历:
从左到右遍历每个级别。

“我是 G,我的孩子是 D 和 I,我的孙子是 B、E、H 和 K,他们的孙子是 A、C、F”

- Level 1: G 
- Level 2: D, I 
- Level 3: B, E, H, K 
- Level 4: A, C, F

Order Searched: G, D, I, B, E, H, K, A, C, F

深度优先遍历:
遍历不是一次在整个关卡中完成。相反,遍历首先深入到树的深度(从根到叶)。但是,它比简单的上下移动要复杂一些。

有三种方法:

1) PREORDER: ROOT, LEFT, RIGHT.
You need to think of this as a recursive process:  
Grab the Root. (G)  
Then Check the Left. (It's a tree)  
Grab the Root of the Left. (D)  
Then Check the Left of D. (It's a tree)  
Grab the Root of the Left (B)  
Then Check the Left of B. (A)  
Check the Right of B. (C, and it's a leaf node. Finish B tree. Continue D tree)  
Check the Right of D. (It's a tree)  
Grab the Root. (E)  
Check the Left of E. (Nothing)  
Check the Right of E. (F, Finish D Tree. Move back to G Tree)  
Check the Right of G. (It's a tree)  
Grab the Root of I Tree. (I)  
Check the Left. (H, it's a leaf.)  
Check the Right. (K, it's a leaf. Finish G tree)  
DONE: G, D, B, A, C, E, F, I, H, K  

2) INORDER: LEFT, ROOT, RIGHT
Where the root is "in" or between the left and right child node.  
Check the Left of the G Tree. (It's a D Tree)  
Check the Left of the D Tree. (It's a B Tree)  
Check the Left of the B Tree. (A)  
Check the Root of the B Tree (B)  
Check the Right of the B Tree (C, finished B Tree!)  
Check the Right of the D Tree (It's a E Tree)  
Check the Left of the E Tree. (Nothing)  
Check the Right of the E Tree. (F, it's a leaf. Finish E Tree. Finish D Tree)...  
Onwards until...   
DONE: A, B, C, D, E, F, G, H, I, K  

3) POSTORDER: 
LEFT, RIGHT, ROOT  
DONE: A, C, B, F, E, D, H, K, I, G

用法(又名,我们为什么要关心):
我真的很喜欢这个关于深度优先遍历方法及其常用方法的简单 Quora 解释:
“按顺序遍历将打印值 [按 BST(二叉搜索树)的顺序]”
“前序遍历用于创建[二叉搜索树]的副本。”
“后序遍历用于删除【二叉搜索树】。”
https://www.quora.com/What-is-the-use-of-pre-order-and-post-order-traversal-of-binary-trees-in-computing

【讨论】:

    【解决方案3】:

    我认为将它们都写成一种方式会很有趣首先。

    我个人喜欢将 BFS 解释为淹没景观:首先淹没低海拔地区,然后才会淹没高海拔地区。如果您将景观高度想象为我们在地理书籍中看到的等值线,那么很容易看出 BFS 同时填充了同一等值线下的所有区域,就像物理学一样。因此,将高度解释为距离或按比例计算的成本可以让您对算法有一个非常直观的想法。

    考虑到这一点,您可以轻松地调整广度优先搜索背后的思想,以轻松找到最小生成树、最短路径以及许多其他最小化算法。

    我还没有看到对 DFS 的任何直观解释(只有关于迷宫的标准解释,但它没有 BFS 和洪水那么强大),所以对我来说,BFS 似乎与所描述的物理现象相关性更好上面,而 DFS 与理性系统上的选择困境(即人或计算机决定在国际象棋游戏中采取哪一步或走出迷宫)具有更好的相关性。

    所以,对我来说,区别在于哪种自然现象最符合它们在现实生活中的传播模型(横向)。

    【讨论】:

    • 您可以使用类似的算法来实现它们,只需将堆栈用于 DFS,将队列用于 BFS。 BFS 的问题是您需要跟踪到目前为止看到的所有节点。物理学中的 DFS .. 我想象另一个宇宙,你想要一个有生命的宇宙,所有根的孩子,都是不同的大爆炸,你一直到宇宙死亡,没有生命?你回到上一个分岔处,再试一次,直到所有的人都筋疲力尽,然后你进入下一个大爆炸,为新的宇宙设定新的物理定律。超级直观。一个很好的问题是在棋盘上找到马的方法。
    【解决方案4】:

    这两个术语区分了两种不同的走树方式。

    展示差异可能是最简单的。考虑树:

        A
       / \
      B   C
     /   / \
    D   E   F
    

    深度第一次遍历将按此顺序访问节点

    A, B, D, C, E, F
    

    请注意,在继续前行之前,您要一直向下一条腿。

    广度第一次遍历会按此顺序访问节点

    A, B, C, D, E, F
    

    在这里,我们一路跨越每个级别,然后再下楼。

    (请注意,遍历顺序存在一些歧义,并且我已经作弊以维持树的每一级的“阅读”顺序。在任何一种情况下,我都可以在 C 之前或之后到达 B,同样我可以在 F 之前或之后到达 E。这可能重要也可能不重要,取决于您的应用程序...)


    两种遍历都可以用伪代码实现:

    Store the root node in Container
    While (there are nodes in Container)
       N = Get the "next" node from Container
       Store all the children of N in Container
       Do some work on N
    

    两种遍历顺序的区别在于Container的选择。

    • 深度优先使用堆栈。 (递归实现使用调用堆栈...)
    • 对于广度优先,请使用队列。

    递归实现看起来像

    ProcessNode(Node)
       Work on the payload Node
       Foreach child of Node
          ProcessNode(child)
       /* Alternate time to work on the payload Node (see below) */
    

    当你到达一个没有孩子的节点时递归结束,所以它保证结束 有限的无环图。


    在这一点上,我还是有点作弊。稍微聪明一点,您还可以按以下顺序处理节点:

    D, B, E, F, C, A
    

    这是深度优先的一种变体,我不会在每个节点上进行工作,直到我走回树上。然而,我访问了在向下寻找他们的孩子的路上更高的节点。

    这种遍历在递归实现中是相当自然的(使用上面的“Alternate time”行而不是第一个“Work”行),如果您使用显式堆栈,则不会太困难,但是我会把它留作练习。

    【讨论】:

    • @dmckee 谢谢!我相信您的意思是“在 Node 上处理有效负载”,对吗?
    • 可能值得注意的是,您可以修改深度优先版本以获取前缀(A, B, D, C, E, F - 出现的第一个),中缀(D, B, A, E, C, F - 用于排序:添加为 AVL 树然后阅读中缀)或后缀(D, B, E, F, C, A 提供的替代方案)遍历。名称由处理根的位置给出。应该注意的是,中缀只对二叉树有意义。 @batbrat 这些是名字......考虑到你问的时间,你可能已经知道了。
    • @Theraot 感谢您添加它!是的,我确实知道这些类型的遍历以及为什么中缀只对二叉树有意义。
    • 如何决定哪个解的空间复杂度或时间复杂度更好?
    • @IgorGanapolsky 原则上两者应该是相同的(毕竟,它们使用基本相同的代码)。一个更有趣的问题是它们如何影响缓存和工作集,但我认为这将取决于树的形态。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 2011-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多