非递归深度优先搜索算法


173

我正在寻找一种针对非二叉树的非递归深度优先搜索算法。很感谢任何形式的帮助。


1
@Bart Kiers根据标记判断,通常是一棵树。
biziclop 2011年

13
深度优先搜索是一种递归算法。下面的答案是递归地探索节点,它们只是不使用系统的调用堆栈来进行递归,而是使用显式堆栈。
空值

8
@Null设置不,这只是一个循环。根据您的定义,每个计算机程序都是递归的。(从某种意义上讲,这就是它们的意思。)
biziclop 2011年

1
@空集:树也是递归数据结构。
Gumbo

2
@MuhammadUmer在认为迭代的可读性较差时,与迭代方法相比,迭代的主要好处是可以避免大多数系统/编程语言为保护堆栈而实现的最大堆栈大小/递归深度约束。在内存堆栈中,堆栈仅受程序允许使用的内存量限制,这通常允许堆栈远大于最大调用堆栈大小。
约翰B

Answers:


313

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()删除并返回列表中的第一个元素。


11
+1表示非递归处理时两者的相似性(好像它们在递归时完全不同,但是仍然...)
corsiKa 2011年

3
然后增加对称性,如果使用最小优先级队列作为边缘,则有一个单源最短路径查找器。
马克·彼得斯

10
顺便说一句,该.first()功能还将元素从列表中删除。就像shift()许多语言一样。pop()也起作用,并以从右到左的顺序而不是从左到右的顺序返回子节点。
Ariel

5
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)
蝙蝠侠

3
@learner我可能会误解您的示例,但是如果它们都相互连接,那实际上不是一棵树。
biziclop 2013年

40

您将使用一个堆栈来保存尚未访问的节点:

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

2
@Gumbo我想知道它是否是有cycyles的图形。能行吗?我想我可以避免将重复的节点添加到堆栈中,并且可以正常工作。我要做的是标记弹出的节点的所有邻居,并添加一个,if (nodes are not marked)以判断是否适合将其推入堆栈。能行吗?
Alston 2014年

1
@Stallman您可能会记得已经访问过的节点。如果然后您仅访问尚未访问的节点,则不会执行任何循环。
Gumbo 2014年

@Gumbo是什么意思doing cycles?我想我只想要DFS的命令。是吗,谢谢。
Alston 2014年

只是想指出,使用堆栈(LIFO)意味着深度优先遍历。如果要使用广度优先,请改为使用队列(FIFO)。
Per Lundberg '18

3
值得注意的是,要具有与最受欢迎的@biziclop答案等效的代码,则需要以相反的顺序(for each node.childNodes.reverse() do stack.push(stack) endfor)推送子注释。这可能也是您想要的。这段视频为什么要这样解释:youtube.com/watch ?
v

32

如果您有指向父节点的指针,则无需额外的内存即可完成操作。

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

这是一个很好的解决方案,因为它不使用额外的内存或对列表或堆栈进行操作(避免递归的一些很好的理由)。但是,只有在树节点具有指向其父节点的链接时才有可能。
joeytwiddle

谢谢。这个算法很棒。但是在此版本中,您无法在访问功能中删除节点的内存。该算法可以使用“ first_child”指针将树转换为单链列表。比您可以遍历它并释放节点的内存而无需递归。
puchu 2014年

6
“如果您有指向父节点的指针,则无需额外的内存就可以做到这一点”:存储指向父节点的指针确实会使用一些“附加内存” ...
rptr 2014年

1
@ rptr87(如果不清楚),除了那些指针外没有其他内存。
Abhinav Gauniyal,2016年

对于节点不是绝对根但可以通过轻松修复的部分树,这将失败while not node.next_sibling or node is root:
巴塞尔Shishani

5

使用堆栈来跟踪您的节点

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)

}

1
@DaveO。否,因为您将已访问节点的子节点推回所有已经存在的节点之前。
biziclop 2011年

那时我一定误解了push_back的语义。
Dave O.

@戴夫,你有一个很好的观点。我当时认为应该“将其余的队列推回去”,而不是“向后推”。我将进行适当的编辑。
corsiKa 2011年

如果要推到最前面,那应该是一堆。
航班

@Timmy是的,我不确定我在想什么。@quasiverse我们通常将队列视为FIFO队列。堆栈定义为LIFO队列。
corsiKa 2011年

4

尽管“使用堆栈” 可以作为人为设计的面试问题的答案,但实际上,它只是在明确地执行递归程序在后台执行的操作。

递归使用程序内置堆栈。当您调用一个函数时,它将参数传递给该函数到堆栈上,而当函数返回时,它会通过弹出程序堆栈来执行。


7
重要的区别在于线程堆栈受到严格限制,并且非递归算法将使用可伸缩性更高的堆。
Yam Marcovic

1
这不仅是人为的情况。我在C#和JavaScript中几次使用了这样的技术,以通过现有的递归调用等价物获得显着的性能提升。通常情况下,使用堆栈而不是使用调用堆栈来管理递归会更快,资源占用更少。将调用上下文放置到堆栈上会涉及很多开销,而程序员却可以对放置在自定义堆栈上的内容做出实际决定。
杰森·杰克逊

4

基于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);
  }
}


3
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()并打印()值。然后,如果它有子级(左和右),则将它们推入堆栈-首先推入Right,这样您将首先访问Left子级(在访问节点本身之后)。当stack为empty()时,您将访问Pre-Order中的所有节点。


2

使用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 
  }    
}

它不同于典型的非递归DFS,可以轻松检测给定节点的所有可到达后代何时被处理,并在列表/堆栈中维护当前路径。


1

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

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的递归实现将始终达到最后。


1

完整示例工作代码,无堆栈:

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


0

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

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();
        }
    }
}

0

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;
}

问题明确要求使用非二叉树
user3743222

您需要访问过的地图来避免无限循环
spiralmoon 2015年

0

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()
}

0

使用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;
}

0
        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);
            }
        }

0

基于@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

0

这是一个指向Java程序的链接,该程序显示了DFS,它遵循递归和非递归方法,还计算发现完成时间,但不进行边缘处理。

    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);
                }
            }
        }
    }
}

完整的源代码在这里


0

只是想将我的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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.