【问题标题】:Why No Cycles in Eric Lippert's Immutable Binary Tree?为什么 Eric Lippert 的不可变二叉树中没有循环?
【发布时间】:2010-08-27 20:45:19
【问题描述】:

我只是在查看 Eric Lippert 对 immutable binary tree 的简单实现,对此我有疑问。在展示了实现之后,Eric 指出

请注意,另一个不错的功能 不可变的数据结构是它 不可能意外(或 故意!)创建一棵树 包含一个循环。

Eric 实现的这一特性似乎不仅仅来自于不变性,还来自于树是由叶子组成的事实。这自然会阻止节点将其任何祖先作为子节点。似乎如果你在另一个方向构建树,你就会引入循环的可能性。

我的想法是否正确,或者在这种情况下,循环的不可能性仅来自不变性?考虑到来源,我想知道我是否遗漏了什么。

编辑:在考虑了更多之后,似乎从叶子开始构建可能是创建不可变树的唯一方法。我对吗?

【问题讨论】:

  • 既然数据结构是不可变的,那么从另一个方向创建树是不是不可能,因此您的假设等于Eric的陈述?
  • 是的,这就是我最初没有意识到的。
  • 为什么每个人都认为树节​​点必须保存关于其子节点而不是其父节点的信息?文章中某处是否有这种效果的定义?在我看来,子节点指向其父节点的树结构是一种非常自然且常见的设计方式。并不是说指向自己的孩子有什么问题,但这不是唯一的方法。
  • 我想你在理论上是正确的,但在实践中,这种形式的树并没有多大用处。通常你想从根节点开始遍历一棵二叉树,而不是各种叶子中的任何一个。
  • @LarsH 如果你有一个孩子指向父母的树结构(仅此而已),那么你如何找到所有孩子?至少,您必须保留所有叶子的另一个列表,才能遍历该结构。对于只有孩子指向父母的树,你不能做很多有用的事情。

标签: algorithm data-structures tree immutability


【解决方案1】:

如果您真的想努力尝试,您可以创建一棵树,其中包含不可变的循环。例如,您可以定义一个不可变的图形类,然后说:

Graph g = Graph.Empty
  .AddNode("A")
  .AddNode("B")
  .AddNode("C")
  .AddEdge("A", "B")
  .AddEdge("B", "C")
  .AddEdge("C", "A");

嘿,你有一个“树”,里面有“循环”——因为当然你一开始没有树,你有一个有向图。

但是对于实际使用二叉树的传统“左右子树”实现的数据类型,则无法制作循环树(模数当然是偷偷摸摸的技巧,例如使用反射或不安全代码。)

【讨论】:

    【解决方案2】:

    如果您使用不可变数据结构,在严格(相对于惰性)语言中,则不可能创建循环;因为您必须按某种顺序创建元素,并且一旦创建了元素,就不能将其更改为指向稍后创建的元素。因此,如果您创建了节点 n,然后创建了指向 n 的节点 m(可能是间接的),那么您将永远无法通过导致n 指向 m,因为你不能改变 n,也不能改变 n 已经指向的任何东西。

    是的,你是正确的,你只能通过从叶子上构建一棵不可变的树;如果您从根开始,则必须在创建它们时修改根以指向其子级。只有从叶子开始,创建每个节点指向它的子节点,才能从不可变节点构造一棵树。

    【讨论】:

    • @Brian,如果每个节点都指向其父节点,而不是指向其子节点,您可以从根开始构建不可变树。 (在二叉树的情况下,每个节点都必须知道它是其父节点的 L 还是 R 子节点。)
    • @LarsH 我想你可以这样做,但你永远无法以任何有用的方式遍历那棵树。
    • @Brian 相反......只需保留所有节点的单独列表。 (如果您的列表必须是不可变的,请随时使用 cons 构建它。)当然,您会发现自上而下的遍历效率低下……但有时您并不关心那种遍历。现实生活中的例子:你的叶子节点是按值索引的,你需要能够查询它们的祖先。无论应用程序或有用性如何,关键是,您永远只能通过从叶子构建一棵不可变的树来创建一个不可变的树,这是不正确的。这个问题是一个理论问题,理论上你可以。
    • +1 表示第 1 段正确,-1 表示第 2 段错误(提供更正时不修改)
    • @LarsH 我没有修改我的答案,因为提出问题的人在 cmets 中表示他只对可以从根遍历的二叉树感兴趣,这也恰好是类型链接到的文章中讨论。你似乎想让我回答一个与被问到的问题不同的问题。是的,如果您断章取义地阅读我的答案,那是不准确的,但我的意图不是让寻找问题的人断章取义地阅读答案,而是回答所提出的问题。
    【解决方案3】:

    当您说“从叶子中构建”时,我猜您是在包括构造函数接受孩子但从不接受父母的事实。

    看来如果你把树建在 另一个方向,你会介绍 循环的可能性。

    不,因为那样你就会有相反的约束:构造函数必须接受父母,但永远不能接受孩子。因此,在创建其所有祖先之前,您永远无法创建后代。因此没有循环是可能的。

    再想一想, 似乎是从叶子上建立起来的 可能是创建一个 不可变的树。我说的对吗?

    不...请参阅我的 cmets 给 Brian 和 ergosys。

    对于许多应用程序,其子节点指向其父节点的树并不是很有用。我同意。如果您需要按照树的层次结构确定的顺序遍历树,那么向上的树就很难了。

    但是对于其他应用程序,这种树正是我们想要的那种。例如,我们有一个文章数据库。每篇文章可以有一个或多个翻译。每个翻译都可以有翻译。我们将此数据结构创建为关系数据库表,其中每条记录都有一个指向其父项的“外键”(指针)。这些记录都不需要更改其指向其父级的指针。添加新文章或翻译时,会使用指向相应父级的指针创建记录。

    一个常见的用例是查询翻译表,查找特定文章的翻译或特定语言的翻译。啊,你说,翻译表是一个可变数据结构。

    确实如此。但它与树是分开的。我们使用(不可变)树来记录层次关系,并使用可变表来对项目进行迭代。在非数据库情况下,您可以有一个哈希表指向树的节点。无论哪种方式,树本身(即节点)都不会被修改。

    Here's another example of this data structure,包括如何有效地访问节点。

    我的观点是,OP 问题的答案是“是”,我同意你们其他人的观点,循环的预防确实仅来自不变性。虽然您可以在另一个方向(自上而下)构建树,但如果这样做,并且它是不可变的,它仍然不能有循环。

    当您谈论强大的理论保证时,例如

    不可变数据结构的另一个不错的特性是 不可能意外(或 故意!)创建一棵树 包含一个循环 [强调原文]

    “这样一棵树不会很有用”相比之下就相形见绌了——即使这是真的。 人们总是不经意地创建无用的数据结构,更不用说故意创建所谓的无用数据结构了。假定的无用性并不能保护程序免受数据结构中循环的陷阱。理论上的保证可以(假设您确实符合它规定的标准)。

    附:向上指向树的一个很好的特性是,您可以保证向下指向树数据结构(如 Eric Lippert 的)所不具备的树定义的一个方面:每个节点至多有一个父节点。 (请参阅David's comment 和我的回复。)

    【讨论】:

    • 我明白你的意思,但这对于二叉树来说似乎不是一个非常有用的形式。
    • @Brian, en.wikipedia.org/wiki/… 说“有很多不同的方式来表示树;常见的表示将节点表示为在堆上分配的记录(不要与堆数据结构混淆),并带有指向他们的孩子,他们的父母,或两者兼而有之,或作为数组中的项目,它们之间的关系由它们在数组中的位置决定(例如,二叉堆)。”
    【解决方案4】:

    您不能从根目录构建它,它需要您对已添加的节点进行变异。

    【讨论】:

    • 取决于您如何设计数据结构...如果每个节点都有关于其父节点的信息而不是相反,您可以从根构建树。
    • @LarsH,没错,但它是一棵完全没用的树。
    • 不正确。请参阅对 Brian 的评论。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多