【问题标题】:Non-recursive depth first search algorithm [closed]非递归深度优先搜索算法
【发布时间】:2011-07-13 19:26:06
【问题描述】:

我正在为非二叉树寻找一种非递归深度优先搜索算法。非常感谢任何帮助。

【问题讨论】:

  • @Bart Kiers 从标签来看,一般来说是一棵树。
  • 深度优先搜索是一种递归算法。下面的答案是递归探索节点,它们只是不使用系统的调用堆栈进行递归,而是使用显式堆栈。
  • @Null Set 不,这只是一个循环。根据您的定义,每个计算机程序都是递归的。 (从某种意义上说,它们是。)
  • @Null Set:树也是一种递归数据结构。
  • @MuhammadUmer 当迭代被认为可读性较差时,迭代优于递归方法的主要好处是,您可以避免大多数系统/编程语言为保护堆栈而实施的最大堆栈大小/递归深度限制。使用内存堆栈,您的堆栈仅受允许您的程序使用的内存量的限制,这通常允许堆栈比最大调用堆栈大小大得多。

标签: algorithm tree


【解决方案1】:

DFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.prepend( currentnode.children );
  //do something
}

BFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.append( currentnode.children );
  //do something
}

两者的对称性相当酷。

更新:正如所指出的,take_first() 删除并返回列表中的第一个元素。

【讨论】:

  • +1 表示两者在非递归完成时有多么相似(就好像它们在递归时完全不同,但仍然......)
  • 然后为了增加对称性,如果你使用最小优先级队列作为边缘,你就有一个单源最短路径查找器。
  • 顺便说一句,.first() 函数也会从列表中删除元素。喜欢多种语言的shift()pop() 也有效,它以从右到左的顺序返回子节点,而不是从左到右。
  • IMO,DFS 算法有点不正确。想象一下 3 个顶点都相互连接。进度应该是:gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st)。但是您的代码会产生:gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
  • @learner 我可能会误解您的示例,但如果它们都相互关联,那并不是真正的树。
【解决方案2】:

您将使用 stack 来保存尚未访问的节点:

stack.push(root)
while !stack.isEmpty() do
    node = stack.pop()
    for each node.childNodes do
        stack.push(stack)
    endfor
    // …
endwhile

【讨论】:

  • @Gumbo 我想知道它是否是带有循环的图。这能行吗?我想我可以避免将重复的节点添加到堆栈中并且它可以工作。我要做的就是标记所有弹出的节点的邻居,并添加一个if (nodes are not marked)来判断是否适合推入堆栈。这行得通吗?
  • @Stallman 你可以记住你已经访问过的节点。如果你只访问你还没有访问过的节点,你就不会做任何循环。
  • @Gumbo doing cycles 是什么意思?我想我只想要DFS的顺序。对不对,谢谢。
  • 只是想指出,使用堆栈 (LIFO) 意味着深度优先遍历。如果您想使用广度优先,请使用队列 (FIFO)。
  • 值得注意的是,要拥有与最流行的@biziclop 答案相同的代码,您需要以相反的顺序推送子注释 (for each node.childNodes.reverse() do stack.push(stack) endfor)。这也可能是你想要的。这个视频很好地解释了为什么会这样:youtube.com/watch?v=cZPXfl_tUkAendfor
【解决方案3】:

如果你有指向父节点的指针,你可以在没有额外内存的情况下做到这一点。

def dfs(root):
    node = root
    while True:
        visit(node)
        if node.first_child:
            node = node.first_child      # walk down
        else:
            while not node.next_sibling:
                if node is root:
                    return
                node = node.parent       # walk up ...
            node = node.next_sibling     # ... and right

请注意,如果子节点存储为数组而不是通过兄弟指针,则可以找到下一个兄弟节点:

def next_sibling(node):
    try:
        i =    node.parent.child_nodes.index(node)
        return node.parent.child_nodes[i+1]
    except (IndexError, AttributeError):
        return None

【讨论】:

  • 这是一个很好的解决方案,因为它不使用额外的内存或操作列表或堆栈(避免递归的一些好理由)。但是,只有当树节点具有到其父节点的链接时才有可能。
  • 谢谢。这个算法很棒。但是在这个版本中,您不能在访问功能中删除节点的内存。该算法可以通过使用“first_child”指针将树转换为单链表。比您可以遍历它并释放节点的内存而无需递归。
  • “如果你有指向父节点的指针,你可以在没有额外内存的情况下做到这一点”:存储指向父节点的指针确实会使用一些“额外的内存”......
  • @rptr87 如果不清楚,除了这些指针之外没有额外的内存。
  • 这对于节点不是绝对根的部分树会失败,但可以通过while not node.next_sibling or node is root: 轻松修复。
【解决方案4】:

使用堆栈来跟踪您的节点

Stack<Node> s;

s.prepend(tree.head);

while(!s.empty) {
    Node n = s.poll_front // gets first node

    // do something with q?

    for each child of n: s.prepend(child)

}

【讨论】:

  • @Dave O. 不,因为您将访问节点的子节点推到已经存在的所有内容的前面。
  • 那我一定是误解了push_back的语义。
  • @Dave 你有一个很好的观点。我在想应该是“把队列的其余部分推回去”而不是“推到后面”。我会适当地编辑。
  • 如果你推到前面,它应该是一个堆栈。
  • @Timmy 是的,我不确定我在想什么。 @quasiverse我们通常将队列视为FIFO队列。堆栈被定义为 LIFO 队列。
【解决方案5】:

一个基于 biziclops 的 ES6 实现很好的答案:

root = {
  text: "root",
  children: [{
    text: "c1",
    children: [{
      text: "c11"
    }, {
      text: "c12"
    }]
  }, {
    text: "c2",
    children: [{
      text: "c21"
    }, {
      text: "c22"
    }]
  }, ]
}

console.log("DFS:")
DFS(root, node => node.children, node => console.log(node.text));

console.log("BFS:")
BFS(root, node => node.children, node => console.log(node.text));

function BFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...nodesToVisit,
      ...(getChildren(currentNode) || []),
    ];
    visit(currentNode);
  }
}

function DFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...(getChildren(currentNode) || []),
      ...nodesToVisit,
    ];
    visit(currentNode);
  }
}

【讨论】:

    【解决方案6】:

    虽然“使用堆栈”可能可以作为人为面试问题的答案,但实际上,它只是明确地执行递归程序在幕后所做的事情。

    递归使用程序内置堆栈。当你调用一个函数时,它会将函数的参数压入堆栈,当函数返回时,它会通过弹出程序堆栈来实现。

    【讨论】:

    • 重要的区别是线程堆栈受到严格限制,非递归算法将使用可扩展性更高的堆。
    • 这不仅仅是人为的情况。我曾在 C# 和 JavaScript 中使用过几次这样的技术,以比现有的递归调用等效项获得显着的性能提升。通常情况下,使用堆栈而不是使用调用堆栈来管理递归会更快且资源消耗更少。将调用上下文放置到堆栈上涉及大量开销,而程序员能够就在自定义堆栈上放置什么做出实际决定。
    【解决方案7】:
    PreOrderTraversal is same as DFS in binary tree. You can do the same recursion 
    taking care of Stack as below.
    
        public void IterativePreOrder(Tree root)
                {
                    if (root == null)
                        return;
                    Stack s<Tree> = new Stack<Tree>();
                    s.Push(root);
                    while (s.Count != 0)
                    {
                        Tree b = s.Pop();
                        Console.Write(b.Data + " ");
                        if (b.Right != null)
                            s.Push(b.Right);
                        if (b.Left != null)
                            s.Push(b.Left);
    
                    }
                }
    

    一般逻辑是,将一个节点(从根开始)推入堆栈,Pop() 和 Print() 值。然后,如果它有孩子(左和右)将它们推入堆栈 - 首先推右,以便您首先访问左孩子(在访问节点本身之后)。当 stack 为 empty() 时,您将访问 Pre-Order 中的所有节点。

    【讨论】:

      【解决方案8】:

      使用 ES6 生成器的非递归 DFS

      class Node {
        constructor(name, childNodes) {
          this.name = name;
          this.childNodes = childNodes;
          this.visited = false;
        }
      }
      
      function *dfs(s) {
        let stack = [];
        stack.push(s);
        stackLoop: while (stack.length) {
          let u = stack[stack.length - 1]; // peek
          if (!u.visited) {
            u.visited = true; // grey - visited
            yield u;
          }
      
          for (let v of u.childNodes) {
            if (!v.visited) {
              stack.push(v);
              continue stackLoop;
            }
          }
      
          stack.pop(); // black - all reachable descendants were processed 
        }    
      }
      

      它不同于typical non-recursive DFS,以便轻松检测给定节点的所有可到达后代何时被处理并维护列表/堆栈中的当前路径。

      【讨论】:

        【解决方案9】:

        假设您想在访问图中的每个节点时执行通知。简单的递归实现是:

        void DFSRecursive(Node n, Set<Node> visited) {
          visited.add(n);
          for (Node x : neighbors_of(n)) {  // iterate over all neighbors
            if (!visited.contains(x)) {
              DFSRecursive(x, visited);
            }
          }
          OnVisit(n);  // callback to say node is finally visited, after all its non-visited neighbors
        }
        

        好的,现在您需要一个基于堆栈的实现,因为您的示例不起作用。例如,复杂的图表可能会导致程序堆栈崩溃,您需要实现一个非递归版本。最大的问题是知道何时发出通知。

        以下伪代码有效(Java 和 C++ 混合使用以提高可读性):

        void DFS(Node root) {
          Set<Node> visited;
          Set<Node> toNotify;  // nodes we want to notify
        
          Stack<Node> stack;
          stack.add(root);
          toNotify.add(root);  // we won't pop nodes from this until DFS is done
          while (!stack.empty()) {
            Node current = stack.pop();
            visited.add(current);
            for (Node x : neighbors_of(current)) {
              if (!visited.contains(x)) {
                stack.add(x);
                toNotify.add(x);
              }
            }
          }
          // Now issue notifications. toNotifyStack might contain duplicates (will never
          // happen in a tree but easily happens in a graph)
          Set<Node> notified;
          while (!toNotify.empty()) {
          Node n = toNotify.pop();
          if (!toNotify.contains(n)) {
            OnVisit(n);  // issue callback
            toNotify.add(n);
          }
        }
        

        它看起来很复杂,但存在发出通知所需的额外逻辑,因为您需要以访问的相反顺序进行通知 - DFS 从根开始但最后通知它,这与 BFS 不同,实现起来非常简单。

        对于踢球,请尝试以下图表: 节点是 s、t、v 和 w。 有向边是: s->t、s->v、t->w、v->w 和 v->t。 运行您自己的 DFS 实现,访问节点的顺序必须是: w, t, v, s DFS 的笨拙实现可能会首先通知 t,这表明存在错误。 DFS 的递归实现总是最后到达 w。

        【讨论】:

          【解决方案10】:

          完整的示例工作代码,没有堆栈:

          import java.util.*;
          
          class Graph {
          private List<List<Integer>> adj;
          
          Graph(int numOfVertices) {
              this.adj = new ArrayList<>();
              for (int i = 0; i < numOfVertices; ++i)
                  adj.add(i, new ArrayList<>());
          }
          
          void addEdge(int v, int w) {
              adj.get(v).add(w); // Add w to v's list.
          }
          
          void DFS(int v) {
              int nodesToVisitIndex = 0;
              List<Integer> nodesToVisit = new ArrayList<>();
              nodesToVisit.add(v);
              while (nodesToVisitIndex < nodesToVisit.size()) {
                  Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
                  for (Integer s : adj.get(nextChild)) {
                      if (!nodesToVisit.contains(s)) {
                          nodesToVisit.add(nodesToVisitIndex, s);// add the node to the HEAD of the unvisited nodes list.
                      }
                  }
                  System.out.println(nextChild);
              }
          }
          
          void BFS(int v) {
              int nodesToVisitIndex = 0;
              List<Integer> nodesToVisit = new ArrayList<>();
              nodesToVisit.add(v);
              while (nodesToVisitIndex < nodesToVisit.size()) {
                  Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
                  for (Integer s : adj.get(nextChild)) {
                      if (!nodesToVisit.contains(s)) {
                          nodesToVisit.add(s);// add the node to the END of the unvisited node list.
                      }
                  }
                  System.out.println(nextChild);
              }
          }
          
          public static void main(String args[]) {
              Graph g = new Graph(5);
          
              g.addEdge(0, 1);
              g.addEdge(0, 2);
              g.addEdge(1, 2);
              g.addEdge(2, 0);
              g.addEdge(2, 3);
              g.addEdge(3, 3);
              g.addEdge(3, 1);
              g.addEdge(3, 4);
          
              System.out.println("Breadth First Traversal- starting from vertex 2:");
              g.BFS(2);
              System.out.println("Depth First Traversal- starting from vertex 2:");
              g.DFS(2);
          }}
          

          输出: 广度优先遍历 - 从顶点 2 开始: 2 0 3 1 4 深度优先遍历 - 从顶点 2 开始: 2 3 4 1 0

          【讨论】:

          • nodesToVisit.contains(s)nodesToVisit 中的元素数量上花费线性时间。替代方法包括使用 O(1) 访问时间和 O(numOfVertices) 额外空间跟踪布尔数组中已访问的节点。
          【解决方案11】:

          您可以使用堆栈。我用邻接矩阵实现了图:

          void DFS(int current){
              for(int i=1; i<N; i++) visit_table[i]=false;
              myStack.push(current);
              cout << current << "  ";
              while(!myStack.empty()){
                  current = myStack.top();
                  for(int i=0; i<N; i++){
                      if(AdjMatrix[current][i] == 1){
                          if(visit_table[i] == false){ 
                              myStack.push(i);
                              visit_table[i] = true;
                              cout << i << "  ";
                          }
                          break;
                      }
                      else if(!myStack.empty())
                          myStack.pop();
                  }
              }
          }
          

          【讨论】:

            【解决方案12】:

            Java 中的 DFS 迭代:

            //DFS: Iterative
            private Boolean DFSIterative(Node root, int target) {
                if (root == null)
                    return false;
                Stack<Node> _stack = new Stack<Node>();
                _stack.push(root);
                while (_stack.size() > 0) {
                    Node temp = _stack.peek();
                    if (temp.data == target)
                        return true;
                    if (temp.left != null)
                        _stack.push(temp.left);
                    else if (temp.right != null)
                        _stack.push(temp.right);
                    else
                        _stack.pop();
                }
                return false;
            }
            

            【讨论】:

            • 问题明确要求非二叉树
            • 你需要一个访问过的地图来避免死循环
            【解决方案13】:

            http://www.youtube.com/watch?v=zLZhSSXAwxI

            刚刚观看了此视频并实施了。我看起来很容易理解。请批评。

            visited_node={root}
            stack.push(root)
            while(!stack.empty){
              unvisited_node = get_unvisited_adj_nodes(stack.top());
              If (unvisited_node!=null){
                 stack.push(unvisited_node);  
                 visited_node+=unvisited_node;
              }
              else
                 stack.pop()
            }
            

            【讨论】:

              【解决方案14】:

              使用Stack,步骤如下:然后将第一个顶点压入堆栈,

              1. 如果可能,访问相邻的未访问顶点,标记它, 并将其压入堆栈。
              2. 如果您无法按照步骤 1 进行操作,那么,如果可能,从 堆栈。
              3. 如果您无法按照第 1 步或第 2 步操作,那么您就完成了。

              这是按照上述步骤编写的 Java 程序:

              public void searchDepthFirst() {
                  // begin at vertex 0
                  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;
              }
              

              【讨论】:

                【解决方案15】:

                基于@biziclop 回答的伪代码:

                • 仅使用基本构造:变量、数组、if、while 和 for
                • 函数getNode(id)getChildren(id)
                • 假设已知节点数N

                注意:我使用从 1 开始的数组索引,而不是 0。

                广度优先

                S = Array(N)
                S[1] = 1; // root id
                cur = 1;
                last = 1
                while cur <= last
                    id = S[cur]
                    node = getNode(id)
                    children = getChildren(id)
                
                    n = length(children)
                    for i = 1..n
                        S[ last+i ] = children[i]
                    end
                    last = last+n
                    cur = cur+1
                
                    visit(node)
                end
                

                深度优先

                S = Array(N)
                S[1] = 1; // root id
                cur = 1;
                while cur > 0
                    id = S[cur]
                    node = getNode(id)
                    children = getChildren(id)
                
                    n = length(children)
                    for i = 1..n
                        // assuming children are given left-to-right
                        S[ cur+i-1 ] = children[ n-i+1 ] 
                
                        // otherwise
                        // S[ cur+i-1 ] = children[i] 
                    end
                    cur = cur+n-1
                
                    visit(node)
                end
                

                【讨论】:

                  【解决方案16】:

                  这是一个 java 程序的链接,它显示了 DFS 遵循递归和非递归方法以及计算 discoveryfinish 时间,但没有边缘标记。

                      public void DFSIterative() {
                      Reset();
                      Stack<Vertex> s = new Stack<>();
                      for (Vertex v : vertices.values()) {
                          if (!v.visited) {
                              v.d = ++time;
                              v.visited = true;
                              s.push(v);
                              while (!s.isEmpty()) {
                                  Vertex u = s.peek();
                                  s.pop();
                                  boolean bFinished = true;
                                  for (Vertex w : u.adj) {
                                      if (!w.visited) {
                                          w.visited = true;
                                          w.d = ++time;
                                          w.p = u;
                                          s.push(w);
                                          bFinished = false;
                                          break;
                                      }
                                  }
                                  if (bFinished) {
                                      u.f = ++time;
                                      if (u.p != null)
                                          s.push(u.p);
                                  }
                              }
                          }
                      }
                  }
                  

                  完整来源here

                  【讨论】:

                    【解决方案17】:

                    只是想将我的 python 实现添加到一长串解决方案中。这种非递归算法具有发现和完成事件。

                    
                    worklist = [root_node]
                    visited = set()
                    while worklist:
                        node = worklist[-1]
                        if node in visited:
                            # Node is finished
                            worklist.pop()
                        else:
                            # Node is discovered
                            visited.add(node)
                            for child in node.children:
                                worklist.append(child)
                    

                    【讨论】:

                      【解决方案18】:
                      Stack<Node> stack = new Stack<>();
                      stack.add(root);
                      while (!stack.isEmpty()) {
                          Node node = stack.pop();
                          System.out.print(node.getData() + " ");
                      
                          Node right = node.getRight();
                          if (right != null) {
                              stack.push(right);
                          }
                      
                          Node left = node.getLeft();
                          if (left != null) {
                              stack.push(left);
                          }
                      }
                      

                      【讨论】:

                        猜你喜欢
                        • 2015-12-12
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-04-14
                        • 1970-01-01
                        • 2013-08-02
                        相关资源
                        最近更新 更多