你如何迭代树?

时间:2008-11-26 08:26:23

标签: python algorithm

遍历树数据结构的首选方法是什么,因为在某些情况下递归方法调用效率非常低。我只是使用像上面那样的发电机。你有什么提示让它更快吗?

def children(self):
    stack = [self.entities]
    while stack: 
        for e in stack.pop():
            yield e
            if e.entities:
                stack.append(e.entities) 

这是一些测试数据。 第一个是递归的,第二个使用生成器:

s = time.time()
for i in range(100000):
    e.inc_counter()
print time.time() - s

s = time.time()
for i in range(100000):
    for e in e.children():
        e.inc_counter_s()
print time.time() - s

结果:

0.416000127792
0.298999786377

测试代码:

import random

class Entity():
    def __init__(self, name):
        self.entities = []
        self.name = name
        self.counter = 1
        self.depth = 0

    def add_entity(self, e):
        e.depth = self.depth + 1
        self.entities.append(e)

    def inc_counter_r(self):
        for e in self.entities:
            e.counter += 1
            e.inc_counter_r()

    def children(self):
        stack = [self.entities]
        while stack:
            for e in stack.pop():
                yield e
                if e.entities:
                    stack.append(e.entities)

root = Entity("main")
def fill_node(root, max_depth):
    if root.depth <= max_depth:
        for i in range(random.randint(10, 15)):
            e = Entity("node_%s_%s" % (root.depth, i))
            root.add_entity(e)
            fill_node(e, max_depth)
fill_node(root, 3)

import time
s = time.time()
for i in range(100):
    root.inc_counter_r()
print "recursive:", time.time() - s

s = time.time()
for i in range(100):
    for e in root.children():
        e.counter += 1
print "generator:",  time.time() - s

8 个答案:

答案 0 :(得分:5)

除非你的树真的很大或你对速度有很高的(实际)要求,否则我会选择递归方法。更易于阅读,更易于编码。

答案 1 :(得分:5)

我无法想到任何大的算法改进,但是你可以做的一个简单的微优化是将频繁调用的方法(例如stack.append / stack.pop)绑定到本地(这节省了字典查找)

def children(self):
    stack = [self.entities]
    push = stack.append
    pop = stack.pop
    while stack: 
        for e in pop():
            yield e
            if e.entities:
                push(e.entities)

这通过我的测试得到了一个小的(~15%)加速(使用100个遍历的8个深度树,每个节点有4个孩子,给我下面的时间:)

children     :  5.53942348004
children_bind:  4.77636131253

不是很大,但如果速度很重要,那就值得做。

答案 2 :(得分:4)

我不确定你是否可以在完全按顺序遍历树时减少开销,如果你使用递归调用堆栈会增长一些,否则你必须手动使用堆栈以在访问每个节点时推送子项的引用。哪种方式最快并且使用更少的内存,取决于调用堆栈与普通堆栈的昂贵程度。 (我猜测callstack更快,因为它应该针对其使用进行优化,并且递归更容易实现)

如果您不关心访问节点的顺序,树的某些实现实际上存储在动态数组或链表或堆栈中,如果您不关心它遍历的顺序,您可以线性遍历。

但是为什么快速遍历很重要呢?树很适合搜索,数组/链表有利于完全遍历。如果您经常需要完整的有序遍历,但搜索和插入/删除很少,那么有序链表可能是最好的,如果搜索是您最常使用的树。如果数据真的很大,那么内存开销可能导致递归不可能,那么你应该使用数据库。

答案 3 :(得分:4)

递归函数调用并不是非常低效,这是一个古老的编程神话。 (如果它们执行得很糟糕,它们可能会产生比必要的更大的开销,但称它们“极其低效”是完全错误的。)

请记住:不要过早优化,从不优化而不首先进行基准测试。

答案 4 :(得分:3)

如果您有大量RAM并且树不经常更改,您可以缓存调用的结果:

def children(self):
    if self._children_cache is not None:
        return self._children_cache
    # Put your code into collectChildren()
    self._children_cache = self.collectChildren()
    return self._children_cache

每当树发生变化时,将缓存设置为None。在这种情况下,使用递归调用可能更有效,因为结果会更快地累积。

答案 5 :(得分:1)

我过去编写过迭代树遍历代码:它非常难看,而且速度不快,除非你知道完全不仅每个子树都有多少个孩子,而是那里有多少个是

答案 6 :(得分:0)

我对Python函数调用的内部结构知之甚少,但我真的无法想象你的代码片段比递归遍历树更快。

调用堆栈(用于函数调用,包括递归调用)通常非常快。转到下一个对象只需要花费一个函数调用。但是在你的代码片段中 - 你使用堆栈对象,转到下一个对象将花费你stack.append(可能在堆上分配内存),stack.push(可能从堆释放内存)和yield。

递归调用的主要问题是如果树太深,你可能会破坏堆栈。这不太可能发生。

答案 7 :(得分:0)

这是一对小修正。

def children(self):
    stack = [self.entities]
    for e in stack:
        yield e
        if e.entities:
            stack.extend(e.entities)

我实际上认为使用append的生成器不会访问所有节点。我认为你的意思是extend堆栈与所有实体,而不是append一个简单的实体列表到堆栈。

此外,当for循环终止时,原始示例中的while循环也将终止,因为for循环后空堆栈没有变化。