非递归DFS实现

问题描述 投票:4回答:7

最近,我需要实现非递归DFS作为更复杂的算法(准确地说,是Tarjan算法)的一部分。递归实现非常优雅,但不适用于大型图。当我实现迭代版本时,我对它最终变得多么精致感到震惊,我想知道自己做错了什么。

迭代DFS有两种基本方法。首先,您可以一次将一个节点的所有子节点推入堆栈(似乎更为常见)。或者,您也可以按一个。我将专注于第一个,因为每个人似乎都是这样做的。

我在使用此算法时遇到各种问题,最终我意识到要高效地执行该操作,我不需要1,而不是2,而是3个布尔值(我不一定表示您需要三个显式的布尔变量,您可以存储通过特殊的变量值(通常是整数)间接获取信息,但您需要以一种或另一种方式访问​​这3条信息,这三个标志分别是:1)已访问。这是为了防止将孩子过多地推入堆栈。 2)完成。防止对同一节点进行冗余处理。 3)升/降。指示孩子是否已经被推入堆栈。伪代码如下所示:

while(S)
    if S.peek().done == True
        S.pop()
        continue

    S.peek().visited = True

    if S.peek().descending == True
        S.peek().descending = False
        for c in S.peek().children
            if c.visited == False
                S.push(c)
        doDescendingStuff()    
    else
        w = S.pop()
        w.done = True
        doAscendingStuff()

一些注意事项:1)您不需要技术上的升序/降序,因为您可以看到孩子是否全部完成。但是在密集图中它效率很低。

2),主要的作用:似乎没有必要访问/完成操作。这就是为什么(我认为)您需要它。在堆栈上访问它们之前,您不能标记访问过的事物。如果这样做,则可能以错误的顺序处理事物。例如。假设A链接到B和C,B链接到D,D链接到C。然后从A,将B和C压入堆栈。从B您将D压入堆栈...然后呢?如果您在将访问的事物压入堆栈时标记了访问的事物,则此处不会将C压入堆栈。但这是错误的,此图中的C应该从D访问,而不是从A访问(假设A在C之前访问B)。因此,在处理之前,您不会标记访问过的事物。但是随后,您将在堆栈上两次获得C。因此,您需要另一个标志来表明您已完全完成它,因此您无需再次处理C。

我看不出如何避免所有这些事情,而拥有一个完全正确的非递归DFS,该DFS支持收尾动作。但是本能地感觉很脆弱。有没有更好的办法?我在网上咨询的几乎每个地方都真正掩盖了如何实际实现非递归DFS,并说可以做到这一点并提供一个非常基本的算法。当算法正确时(就正确地支持到同一节点的多条路径而言),这种情况很少见,它很少正确地支持在绕线和展开时进行操作。

algorithm depth-first-search iteration non-recursive
7个回答
2
投票

我认为最优雅的基于堆栈的实现将在堆栈上具有子代迭代器,而不是节点。可以将迭代器视为在其子节点中存储节点和位置。

while (!S.empty)
  Iterator i = S.pop()
  bool found = false
  Iterator temp = null
  while (i.hasNext())
    Node n = i.next()
    if (n.visited == false)
      n.visited = true
      doDescendingStuff(n)
      temp = n.getChildrenIterator()
      break
  if (!i.hasNext())
    doAscendingStuff(i.getNode())
  else
    S.push(i)
  if (temp != null)
    S.push(temp)

以上可以通过将节点和位置分离到2个堆栈上来优化i.t.o存储空间。


1
投票

您的代码没有完全模仿递归DFS实现的情况。在递归DFS实现中,每个节点在任何时候在堆栈中仅出现一次。

Dukeling提供的解决方案是一种迭代方式。基本上,您一次只需要推送一个节点,而不是一次推送全部。]

您断言这将需要更多存储的说法是错误的:在您的实现中,一个节点可以在堆栈上多次。实际上,如果您从非常密集的图形(所有顶点上的完整图形)开始,则会发生这种情况。使用Dukeling解决方案,堆栈的大小为O(顶点数)。在您的解决方案中,它是O(边数)。


1
投票

算法BFS(G,v)


0
投票

Robert Sedgewick在cpp书中的算法讨论了一种特殊的堆栈,该堆栈仅保留一个项目的一个副本,而忘记了旧副本。不能完全确定如何执行此操作,但是可以消除堆栈中有多个项目的问题。


0
投票

Tl; dr是您不需要多个标志。


0
投票

这里是到Java程序的链接,该Java程序同时显示了递归和非递归方法的DFS,并且还计算了discovery


-1
投票

为了使用堆栈进行DFS遍历,请从堆栈中弹出一个节点(记住将初始节点压入堆栈中,并检查是否已被访问。如果已经访问过,则忽略然后弹出,否则输出弹出节点,将其标记为已访问并将其所有邻居推入堆栈。继续这样做,直到堆栈为空。

© www.soinside.com 2019 - 2024. All rights reserved.