如何创建完全不可变的树层次结构?建筑鸡肉和鸡蛋

时间:2012-02-16 01:52:08

标签: java data-structures tree immutability

我喜欢使数据类不可变以简化并发编程。但是,制作完全不可变的层次结构似乎存在问题。

考虑这个简单的树类:

public class SOTree {
    private final Set<SOTree> children = new HashSet<>();
    private SOTree parent;

    public SOTree(SOTree parent) {
        this.parent = parent;
    }

    public SOTree(Set<SOTree> children) {
        for (SOTree next : children)
            children.add(next);
    }


    public Set<SOTree> getChildren() {
        return Collections.unmodifiableSet(children);
    }

    public SOTree getParent() {
        return parent;
    }
}

现在,如果我想创建这些层次结构,当我构造它时,父节点必须存在于当前节点之前,或者子节点必须首先存在。

    SOTree root = new SOTree((SOTree)null);
    Set<SOTree> children = createChildrenSomehow(root);
    //how to add children now?  or children to the children?

    Set<SOTree> children = createChildrenSomehow(null);
    SOTree root = new SOTree(children);
    //how to set parent on children?

如果没有强制将它作为一个单独链接的树,是否有任何聪明的方法来构造这样的树并且仍然使所有节点完全不可变?

8 个答案:

答案 0 :(得分:8)

两个想法:

  1. 使用某种树工厂。您可以使用可变结构来描述树,然后有一个工厂来组装一个不可变树。在内部,工厂可以访问不同节点的字段,因此可以根据需要重新连接内部指针,但生成的树将是不可变的。

  2. 围绕可变树构建不可变树包装器。也就是说,让树构造使用可变节点,然后构建一个包装类,然后提供树的不可变视图。这类似于(1),但没有明确的工厂。

  3. 希望这有帮助!

答案 1 :(得分:8)

Eric Lippert最近发表了关于此问题的博文。查看他的博文Persistence, Facades and Roslyn's Red-Green Trees。这是一段摘录:

  

我们实际上通过保留两个解析树来做到这一点。 “绿色”树是不可变的,持久的,没有父引用,是“自下而上”构建的,每个节点都跟踪其宽度而不是绝对位置。当编辑发生时,我们只重建受编辑影响的绿树部分,通常是树中总解析节点的O(log n)。

     

“红色”树是一个围绕绿树建立的不可变 facade ;它是按需“自上而下”构建的并在每次编辑时丢弃。当您从顶部下降树时,它通过按需制造它来计算父引用。它通过从宽度计算它们来制造绝对位置,再次,当你下降时。

答案 2 :(得分:4)

构建高效,不可变的数据结构可能具有挑战性。幸运的是,有些人已经想出如何实现其中的许多功能。看看here,讨论各种不可变数据结构。

这个领域我还在努力让自己加快速度,所以我无法推荐你应该关注的这些结构的确切子集,但是一个用于处理的数据结构拉链可以非常有用的树木。

答案 3 :(得分:3)

你已经正确地说明了你的鸡蛋和鸡蛋问题。重述可能解决问题的另一种方法是你想要种树(根,树干,叶子和所有 - 一次性)。

一旦您接受计算机只能逐步处理,就会出现一系列可能的解决方案:

  1. 看看Clojure如何创建不可变数据结构。在Clojure的情况下,树上的每个操作(例如添加节点)都会返回一个新树。

  2. 使树创建成为原子。您可以创建一种特殊格式,然后反序列化树。由于所有序列化方法都是内部的,因此您不必公开任何可变方法。

  3. 在工厂返回构造树之前,先用旗帜将其锁定。这是原子操作的类比。

  4. 使用包级别方法构造树。这样,外部包就无法访问节点上的变异方法。

  5. 访问时动态创建节点。这意味着您的内部树表示永远不会更改,因为更改节点不会影响您的树结构。

答案 4 :(得分:2)

  

如果没有强制将它作为一个单独链接的树,是否有任何聪明的方法来构造这样的树并且仍然使所有节点完全不可变?

保持您的接口和实现解耦,并且不要将树的节点限制为与树本身相同的类。

此问题的一个解决方案是将节点层次结构存储在其他一些不可变表示中,并且当调用者调用getChildren()getParent()时,它会从该不可变表示中懒惰地构造子节点。如果你希望node.getChildren().get(i).getParent() == node为真(而不是.equals(node) - 即身份而不是相等),那么你必须缓存节点对象,这样你就可以相应地重新发送它们。

答案 5 :(得分:2)

构造不可变树的正确方法是让每个节点的构造函数调用子节点的构造函数,并将自身作为参数,条件是子节点的构造函数不得导致对其自身的根引用存储在任何地方,也不使用传入的参数用于任何目的,除了初始化构造函数除了接受这样的初始化之外没有用处的字段。此外,父节点的构造函数应该避免使用子节点的任何成员来取消引用“父”字段。

虽然这样的技术似乎违反了不可变对象的构造函数不应该将fledgeling对象作为其他例程的参数的规则,但“真实”规则是不可变对象的构造函数不应该允许引用以直接或间接访问尚未达到其最终价值的任何领域的方式使用的授权对象。通常,如果一个fledgeling对象向外界公开了对它自己的引用,它将无法控制外部代码可以对它做什么。但是,在调用子节点构造函数的特定情况下,假设子节点的代码符合上述要求,则除了通过父节点本身之外,不存在对父节点的有根引用。因此,任何对fledgeling节点执行任何意外操作的代码都不会获得对它的引用。

答案 6 :(得分:1)

我最近遇到了类似的问题-https://medium.com/hibob-engineering/from-list-to-immutable-hierarchy-tree-with-scala-c9e16a63cb89

方法是先自底向上构建树,先构建底部节点,然后再向上构建顶部节点。

第1阶段-按深度排序

为了从底部开始,该算法按节点在层次结构中的深度对节点进行排序(有一种O(n)的处理方式,如您在链接中所见)。

第二阶段-从底部构建树

底部节点没有子节点,因此该算法构造了底部节点。

然后,向上一层并使用之前处理过的节点构造具有子节点的节点。 该算法一直持续到到达顶部节点为止。

答案 7 :(得分:-2)

由于你希望它们是不可变的,你必须在构造时才这样做。创建一个同时包含父项和子项的构造函数,而不是两个单独的构造函数。