生成树中所有节点的所有后代的最有效方法


9

我正在寻找获取树的最有效算法(存储为边列表;或存储为从父节点到子节点列表的映射列表);并为每个节点生成从其派生的所有节点的列表(叶级和非叶级)。

由于规模的原因,实现必须通过循环而不是撤回。理想情况下应为O(N)。

该SO问题涵盖了一个合理合理的标准解决方案,用于为树中的一个节点找到答案。但是很明显,在每个树节点上重复该算法都是非常低效的(在我的脑海中,O(NlogN)至O(N ^ 2))。

树的根是已知的。该树具有绝对任意的形状(例如,不是N元,没有以任何方式平衡,形状或形式,深度也不统一)-有些节点有1-2个子节点,有些节点有30K个子节点。

在实际水平上(尽管它不应该影响算法),该树具有约100K-200K节点。


您可以使用循环和堆栈来模拟递归,这是否适用于您的解决方案?
乔治

@ Giorgio-当然。这就是我试图通过“通过循环而不是响应”来暗示的意思。
DVK

Answers:


5

如果您实际上想将每个列表都制作成不同的副本,那么在最坏的情况下,您不能指望获得比n ^ 2更好的空间。如果您只需要访问每个列表:

我将从根开始对树进行有序遍历:

http://en.wikipedia.org/wiki/Tree_traversal

然后,为树中的每个节点在子树中存储最小顺序号和最大顺序号(这很容易通过递归维护-如果需要,可以使用堆栈进行模拟)。

现在,将所有节点放入长度为n的数组A中,其中编号为i的节点位于位置i。然后,当您需要查找节点X的列表时,请查看A [X.min,X.max] -请注意,此间隔将包括节点X,也可以很容易地将其固定。

所有这些都是在O(n)时间内完成并占用O(n)空间。

我希望这有帮助。


2

低效的部分不是遍历树,而是建立节点列表。创建这样的列表似乎是明智的:

descendants[node] = []
for child in node.childs:
    descendants[node].push(child)
    for d in descendants[child]:
        descendants[node].push(d)

由于每个后代节点都被复制到每个父节点的列表中,因此对于平衡树,平均而言,我们的复杂度为O(n log n),而对于实际上是链表的退化树,则为O(n²)最坏的情况。

如果我们使用延迟计算列表的技巧,则取决于是否需要进行任何设置,我们可以降至O(n)或O(1)。假设我们有一个child_iterator(node)给我们该节点的子节点。然后,我们可以descendant_iterator(node)像下面这样简单地定义一个:

def descendant_iterator(node):
  for child in child_iterator(node):
    yield from descendant_iterator(child)
  yield node

由于迭代器控制流程很棘手(协程!),因此涉及到非递归解决方案更多。我将在今天晚些时候更新此答案。

由于树的遍历为O(n),并且列表的迭代也是线性的,因此该技巧完全推迟了成本,直到无论如何都要付清它。例如,打印出每个节点的后代列表具有O(n²)最坏情况的复杂性:遍历所有节点都是O(n),因此遍历每个节点的后代,无论它们存储在列表中还是临时计算。

当然,如果您需要实际的收藏夹来处理,这将不起作用。


对不起,-1。算法的全部目的是预先计算数据。惰性计算完全消除了甚至运行算法的原因。
DVK 2015年

2
@DVK好吧,我可能误会了您的要求。您如何处理结果列表?如果预计算列表是一个瓶颈(但不使用列表),则表明您未使用汇总的所有数据,因此懒惰计算将是一个成功。但是,如果您使用所有数据,则用于预计算的算法就无关紧要–使用数据的算法复杂度至少将等于构建列表的复杂度。
阿蒙(Amon)2015年

0

这个简短的算法应该可以做到,看看代码 public void TestTreeNodeChildrenListing()

该算法实际上按顺序遍历树的节点,并保留当前节点的父级列表。根据您的要求,当前节点是每个父节点的子节点,将其作为子节点添加到每个父节点中。

最终结果存储在字典中。

    [TestFixture]
    public class TreeNodeChildrenListing
    {
        private TreeNode _root;

        [SetUp]
        public void SetUp()
        {
            _root = new TreeNode("root");
            int rootCount = 0;
            for (int i = 0; i < 2; i++)
            {
                int iCount = 0;
                var iNode = new TreeNode("i:" + i);
                _root.Children.Add(iNode);
                rootCount++;
                for (int j = 0; j < 2; j++)
                {
                    int jCount = 0;
                    var jNode = new TreeNode(iNode.Value + "_j:" + j);
                    iCount++;
                    rootCount++;
                    iNode.Children.Add(jNode);
                    for (int k = 0; k < 2; k++)
                    {
                        var kNode = new TreeNode(jNode.Value + "_k:" + k);
                        jNode.Children.Add(kNode);
                        iCount++;
                        rootCount++;
                        jCount++;

                    }
                    jNode.Value += " ChildCount:" + jCount;
                }
                iNode.Value += " ChildCount:" + iCount;
            }
            _root.Value += " ChildCount:" + rootCount;
        }

        [Test]
        public void TestTreeNodeChildrenListing()
        {
            var iteration = new Stack<TreeNode>();
            var parents = new List<TreeNode>();
            var dic = new Dictionary<TreeNode, IList<TreeNode>>();

            TreeNode node = _root;
            while (node != null)
            {
                if (node.Children.Count > 0)
                {
                    if (!dic.ContainsKey(node))
                        dic.Add(node,new List<TreeNode>());

                    parents.Add(node);
                    foreach (var child in node.Children)
                    {
                        foreach (var parent in parents)
                        {
                            dic[parent].Add(child);
                        }
                        iteration.Push(child);
                    }
                }

                if (iteration.Count > 0)
                    node = iteration.Pop();
                else
                    node = null;

                bool removeParents = true;
                while (removeParents)
                {
                    var lastParent = parents[parents.Count - 1];
                    if (!lastParent.Children.Contains(node)
                        && node != _root && lastParent != _root)
                    {
                        parents.Remove(lastParent);
                    }
                    else
                    {
                        removeParents = false;
                    }
                }
            }
        }
    }

    internal class TreeNode
    {
        private IList<TreeNode> _children;
        public string Value { get; set; }

        public TreeNode(string value)
        {
            _children = new List<TreeNode>();
            Value = value;
        }

        public IList<TreeNode> Children
        {
            get { return _children; }
        }
    }
}

在我看来,这非常像O(n log n)到O(n²)的复杂度,并且仅比DVK在其问题中链接的答案有所改善。因此,如果这没有改善,它将如何回答问题?这个答案唯一增加的价值是展示天真的算法的迭代表达式。
阿蒙(Amon)2015年

它是O(n),如果您仔细观察一下算法,则会在节点上进行一次迭代。同时,它同时为每个父节点创建子节点的集合。
低飞鹈鹕2015年

1
您遍历所有节点,即O(n)。然后循环遍历所有子项,我们暂时将其忽略(让我们想象这是一个恒定因素)。然后,您遍历当前节点的所有父节点。在余额树中,这是O(log n),但是在简并的情况下,我们的树是一个链表,它可能是O(n)。因此,如果将遍历所有节点的成本乘以遍历其父节点的成本,我们得到的时间复杂度为O(n log n)到O(n²)。没有多线程,就不会有“同时”。
阿蒙(Amon)2015年

“同时”表示它在同一循环中创建集合,并且不涉及其他循环。
低飞的鹈鹕

0

通常,您只使用递归方法,因为它允许您切换执行顺序,这样您就可以计算从叶子向上开始的叶子数量。由于您必须使用递归调用的结果来更新当前节点,因此将花费大量精力来获得尾部递归版本。当然,如果您不花大力气,那么这种方法只会使一棵大树的堆栈爆炸。

既然我们意识到主要的想法是获得一个从叶子开始并回到根部的循环顺序,那么想到的自然想法就是在树上执行拓扑排序。可以线性遍历所得的节点序列以求出叶子的总数(假设您可以验证中的节点是叶子O(1))。拓扑排序的总时间复杂度为O(|V|+|E|)

我假设您N的节点数|V|通常是(根据DAG命名法)。E另一方面,的大小在很大程度上取决于树的硬度。例如,二叉树每个节点最多具有2条边,因此O(|E|) = O(2*|V|) = O(|V|)在这种情况下,这将导致整体O(|V|)算法。请注意,由于树的整体结构,您不能具有O(|E|) = O(|V|^2)。实际上,由于每个节点都有一个唯一的父节点,所以当您仅考虑父关系时,每个节点最多可以有一个边缘要计数,因此对于树,我们可以保证O(|E|) = O(|V|)。因此,上述算法在树的大小上总是线性的。

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.