【问题标题】:How to implement a Tree data structure with multiple roots如何实现具有多个根的树数据结构
【发布时间】:2021-05-19 12:58:01
【问题描述】:

我需要实现一个 Tree 数据结构,它应该有多个根,而不仅仅是 1 个根。看看这个场景,假设我必须为“书籍内容”实现树数据结构。分别是“Chapters > Sections > Sub-Sections”等。主要问题是:这里有多个根,第1章,第2章,第3章等等。根节点一定要从章节开始,因为从章节开始内容的类型和功能都是一样的。

我的任务需要什么:

  • 多根树
  • 节点在同一父级之间按水平顺序排列
  • 它是一棵非二叉树,这意味着可以有任意数量的根和任意数量的子树。

我提出了一个解决方案,但我认为这是一种混乱的方法。我做了一个类,就像通常为树数据结构做的那样。此类是“SimpleTree”,适用于单个章节作为根节点。为了使多个根节点成为可能,我创建了另一个类“TopWrapperForSimpleTree”。这个顶级包装器类有一个 Array 以便将多个“SimpleTree”元素存储到它(基本上是多个根)。这里混乱的部分是我必须复制“SimpleTree”的每个函数并为包装类定义它。例如,“遍历函数”将遍历“SimpleTree”中的所有元素。但是现在我必须为“TopWrapperForSimpleTree”类实现一个“遍历函数”,它必须遍历所有调用遍历函数的根并连接结果。其他功能也是如此,例如查找节点、删除节点等。

总而言之,我需要一个可以有多个根的树数据结构。它也应该被订购。顺序很重要。

Image showing Tree with multiple roots

【问题讨论】:

  • “多根树”不是树。你似乎描述了一个森林。但是你应该只添加一个根节点。章节属于书,书是根节点。您不需要多个根的数据结构。您需要一个数据结构,其中节点是多功能的,可以表示一本书、一章、一节等,而无需复制代码。 OOP 非常适合(继承)。
  • @trincot 但它不是相同的节点类型。我不能只是把书扔在那里。假设数据为titleText 和pageNumbers。所有的章、节和小节都有这种数据,但根节点没有任何这种数据。这里的根“书”基本上是一个局外人,包含它是没有意义的。
  • @trincot 如果我确实将 Book 设为 root。我将如何制作构造函数?如果我为普通节点创建构造函数,它将包含一些书籍节点不允许的变量和函数。如果构造函数是为 Book Node 创建的,反之亦然。
  • 通过 OOP 设计,您可以覆盖构造函数,该构造函数本身将在继承链中调用其父构造函数。创建一个具有共同特征的泛型类,并让所有其他类继承自它,以便它们可以在此基础上实现自己的细节。
  • @trincot 基本上,这就是这里没有共同特征的问题。 Book 节点应该能够创建子节点,但不能参与函数。继承基本上会使它们相同。

标签: data-structures tree


【解决方案1】:

“多根树”不是一棵树。当您将每个章节视为一棵树时,章节的集合就是一片森林。但是你可以只添加一个根节点。章节属于书,书就是根节点。

您不需要用于多个根的数据结构。您需要一个数据结构,其中节点是多功能的,可以代表一本书、一章、一节等,而无需复制代码。 OOP 非常适合(继承)。

这个想法是定义一个类,该类具有所有对象共有的所有共同特征。例如,一本书、一章、一节……都有一个名字,他们都可以有“孩子”。树的迭代可以作为这个类的一个方法来实现。

那么一本书就是这个基类的扩展:例如,一本书可以有一个作者属性。节也可以是基类的扩展,但可以具有页码作为额外属性。一个章节可以是一个章节的扩展,因为它也有一个页码,但还可以有一个章节号,......等等。

这是执行此操作的众多方法之一。我在这里使用 JavaScript,但它在其他 OOP 语言中的工作方式类似:

class Node {
    constructor(name) {
        this.name = name;
        this.children = [];
    }
    add(...children) {
        this.children.push(...children);
        return this;  // Return the instance, so method calls can be chained...
    }
    toString() {
        return this.name; 
    }
    * iter(depth=0) { 
        // A pre-order iteration through the whole tree that this node represents  
        yield [depth, this];
        for (let child of this.children) {
            yield * child.iter(depth+1);
        }
    }
    * iterWithout(depth=0) { 
        // A pre-order iteration through the whole tree that this node represents 
        // ...but excluding the node on which the original call is made: 
        for (let child of this.children) {
            yield [depth, child];
            yield * child.iterWithout(depth+1);
        }
    }
}

class Book extends Node {
    constructor(name, author) {
        super(name);
        this.author = author; // specific property for Book instance
    }
    toString() {
        return super.toString() + ", by " + this.author; 
    }
}

class Section extends Node {
    constructor(name, page) {
        super(name);
        this.page = page; // specific property for any section (also chapter)
    }
    toString() {
        return super.toString() + ", page " + this.page; 
    }
}

class Chapter extends Section {
    constructor(id, name, page) {
        super(name, page);
        this.id = id; // specific property for Chapter instance
    }
    toString() {
        return "Chapter " + this.id + ". " + super.toString(); 
    }
}

// Illustration of how it could be used:
function main() { // Demo
    let book = new Book("The Perfect Theory", "Pedro G. Ferreira").add(
        new Chapter(1, "If a Person Falls Freely", 1).add(
            new Section("The Autumn of 1907", 1),
            new Section("The Article in the Yearbook", 4),
            new Section("Isaac Newton", 6),
            new Section("Gravity", 9),
        ),
        new Chapter(2, "The Most Valuable Discovery", 12),
        new Chapter(3, "Correct Mathematics, Abominable Physics", 28),
        new Chapter(4, "Collapsing Stars", 47)
    );
    for (let [depth, item] of book.iterWithout()) {
        console.log("  ".repeat(depth) + item.toString());
    }
}

main();

【讨论】:

  • 谢谢。我面临的大多数问题是因为我试图从 add() 函数内部创建 Node。像: class Node { add( data ){ this.children.push( new Node(data) ) } } 这涉及到强制使用相同的构造函数格式。我现在将测试您的解决方案。
  • 它似乎没有解决问题。它实际上与我原来的帖子中的实现相同。问题是:对于某些事情,我需要 Root 节点中的 THE FUNCTION,但 Root 不应该参与 THE FUNCTION。例如,在遍历函数的情况下(根应该有一个遍历方法,但它不应该参与遍历的结果)。或者 findElement 函数,该函数沿遍历检查每个节点值但不能涉及根节点。
  • 我有 2 个选项:要么创建一个类并包含一个检查函数以在每个函数中排除 root(如果当前实例是 root 实例,则不要在结果中使用它)。或者我最初在帖子中使用的方法。我在这里创建了另一个 Wrapper 类,它基本上像你说的那样创建了一个森林。
  • 如果要排除调用该方法的节点,这很简单:只需更改方法即可进行此转变。我已经用一种名为iterWithout 的附加方法更新了我的答案,现在是在演示中调用的方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-29
  • 1970-01-01
相关资源
最近更新 更多