如何使用 DFS 了解节点执行(调用前、调用中、调用后)

Posted

技术标签:

【中文标题】如何使用 DFS 了解节点执行(调用前、调用中、调用后)【英文标题】:How to know node execution (before, middle, after calls) using DFS 【发布时间】:2016-09-09 16:12:19 【问题描述】:

假设我实现了一个迭代 DFS 的简单版本,如下所示:

import sys
import traceback


def dfs(graph, start):
    visited, stack = [], [start]
    while stack:
        node = stack.pop()

        if node not in visited:
            visited.append(node)
            childs = reversed(graph.get(node, list()))
            stack.extend([item for item in childs if item not in visited])

    return visited

if __name__ == "__main__":
    graphs = [
        
            'A': ['B', 'C'],
            'B': ['D']
        
    ]

    for i, g in enumerate(graphs):
        try:
            print "0Graph 12".format('-' * 40, i, '-' * 33)

            for f in [dfs]:
                print f.__name__, '-->', f(g, 'A')
                print '-' * 80
        except Exception as e:
            print "Exception in user code: 0".format(e)
            print '-' * 60
            traceback.print_exc(file=sys.stdout)
            print '-' * 60

上面sn-p的输出是这样的:

----------------------------------------Graph 0---------------------------------
dfs --> ['A', 'B', 'D', 'C']
--------------------------------------------------------------------------------

现在,我正在尝试弄清楚如何获得以下输出(而不是运行节点的方法只是打印就可以了):

A_start, B_start, D_start, D_end, B_end, A_middle, C_start, C_end, A_end

*_middle 只会在子节点执行之间执行。例如,如果一个节点没有任何子节点,或者只有一个,它永远不会被执行。这就是为什么我想要的输出在上面的例子中只有 A_middle(没有 B_middle、C_middle、D_middle)。

我该怎么做?

编辑:

试图找到我的问题的递归解决方案:

def dfs(graph, node):
    if node not in graph:
        return

    print '0_start'.format(node)

    for i, node in enumerate(graph[node]):
        if i > 0:
            print '0_middle'.format(node)

        dfs(graph, node)

    print '0_end'.format(node)

if __name__ == "__main__":
    graphs = [
        
            'A': ['B', 'C'],
            'B': ['D']
        
    ]

    for i, g in enumerate(graphs):
        try:
            print "0Graph 12".format('-' * 40, i, '-' * 33)

            for f in [dfs]:
                print f.__name__, '-->'
                f(g, 'A')
                print '-' * 80
        except Exception as e:
            print "Exception in user code: 0".format(e)
            print '-' * 60
            traceback.print_exc(file=sys.stdout)
            print '-' * 60

会给我错误的输出:

----------------------------------------Graph 0---------------------------------
dfs -->
A_start
B_start
D_end
C_middle
C_end
--------------------------------------------------------------------------------

【问题讨论】:

实际上,您的迭代实现永远在读取其子节点后立即处理任何节点。递归实现将使获得所需输出变得更加容易。否则,您将需要另一个数据结构来跟踪节点的“在读取该节点时实际添加到堆栈中的子节点”何时被处理。 【参考方案1】:

正如其他答案所示,当前递归代码的主要问题是基本情况:

if node not in graph:
    return

当节点没有子节点时,这会错误地跳过输出。摆脱这些行,只需在for 循环中使用enumerate(graph.get(start, [])) 而不是enumerate(graph[start]),它应该可以按需要工作。

让您的迭代代码工作起来相当复杂。尝试它的一种方法是将 2 元组推入堆栈。第一个值是一个节点,和以前一样,但第二个值要么是节点的前身(因此我们可以为父节点打印middle 消息),或者None 表示我们需要打印end节点的标记。

但是,跟踪哪些节点已被访问变得有点复杂。我使用的是从节点到整数的字典映射,而不是单个节点列表。不存在的值意味着尚未访问该节点。 1 表示该节点已被访问并且它的start 消息已被打印。 2 表示该节点的至少一个子节点已被访问,并且每个其他子节点都应代表父节点打印middle 消息。 3 表示 end 消息已打印。

def dfs(graph, start):
    visited = 
    stack = [(start, "XXX_THIS_NODE_DOES_NOT_EXIST_XXX")]
    while stack:
        node, parent = stack.pop()
        if parent is None:
            if visited[node] < 3:
                print "_end".format(node)
            visited[node] = 3

        elif node not in visited:
            if visited.get(parent) == 2:
                print "_middle".format(parent)
            elif visited.get(parent) == 1:
                visited[parent] = 2

            print "_start".format(node)
            visited[node] = 1
            stack.append((node, None))
            for child in reversed(graph.get(node, [])):
                if child not in visited:
                    stack.append((child, node))

因为我使用的是visited 的字典,所以最后返回它可能不合适,所以我删除了return 语句。我认为如果你真的想恢复它,可以使用collections.OrderedDict 而不是普通的dict,并返回它的keys()

【讨论】:

谢谢,很完美。它解决了我将这个算法设计为迭代版本的主要问题。我在与验证答案决定相关的问题中添加了一些 cmets 另一个与此相关的question【参考方案2】:

实际上,您的递归尝试非常接近。 我为我所做的小调整添加了 cmets。

import sys, traceback

def dfs(graph, node):
    print '0_start'.format(node)  # need this right at the top
    if node not in graph:
        print '0_end'.format(node)  # need to record the end if we can't find
        return

    for i, nd in enumerate(graph[node]):  # need a different `node` variable here!!!
        if i > 0:
            print '0_middle'.format(node)

        dfs(graph, nd)

    print '0_end'.format(node)

if __name__ == "__main__":
    graphs = [
        
            'A': ['B', 'C'],
            'B': ['D']
        
    ]

    for i, g in enumerate(graphs):
        try:
            print "0Graph 12".format('-' * 40, i, '-' * 33)

            for f in [dfs]:
                print f.__name__, '-->'
                f(g, 'A')
                print '-' * 80
        except Exception as e:
            print "Exception in user code: 0".format(e)
            print '-' * 60
            traceback.print_exc(file=sys.stdout)
            print '-' * 60

这会产生您正在寻找的输出。

【讨论】:

【参考方案3】:

我怀疑这是你想要的。

graphs = [
        
            'A': ['B', 'C'],
            'B': ['D']
        
    ]

def dfs(graph, start):
    print '_start'.format(start)
    try:
        for child in graph[start]:
            dfs(graph, child)
            print '_middle'.format(start)
    except KeyError:
        # We found a leaf node, it has no children.
        pass
    print '_end'.format(start)   

# Test it for one graph
dfs(graphs[0], 'A')

# Output:

# A_start
# B_start
# D_start
# D_end
# B_middle
# B_end
# A_middle
# C_start
# C_end
# A_middle
# A_end

【讨论】:

缩进有错误,请看编辑。此外,如果此答案符合您的要求,请记住将其标记为已接受! 为了满足规定的要求,您只需要在同一节点的不同子节点之间打印中间消息。 @KennyOstrom 你是说这不是那样的吗?我看到的唯一边缘情况是某物只有一个孩子。如果您希望它显式打印 两个兄弟节点之间,则需要在 for 循环中对子节点进行迭代。

以上是关于如何使用 DFS 了解节点执行(调用前、调用中、调用后)的主要内容,如果未能解决你的问题,请参考以下文章

如何在退出前重新附加线程或等待线程完成

如何并行调用多个 API 进行负载测试(使用 Gatling)?

react生命周期函数

DeprecationWarning:不推荐调用不带回调的异步函数。 - 如何找到“功能:”在哪里?

(菜鸟)了解一下MFC程序如何进入WinMain的

递归实现DFS全排列