几年前,在C#课程中,我学会了写一个看起来或多或少的二叉树:
data Tree a = Branch a (Tree a) (Tree a) | Leaf
我看到它的好处,它在分支上有它的值,这允许快速和容易地查找和插入值,因为它会在每个分支的根上遇到一个值,直到它达到叶子,没有价值。
然而,自从我开始学习Haskell之后;我已经看到了很多像这样定义的树的例子:
data Tree a = Branch (Tree a) (Tree a) | Leaf a
这个定义让我很困惑。我看不出在不分支的元素上使用数据的有用性,因为它最终会导致树看起来像这样:
对我来说,这似乎是一个设计不佳的List的替代品。它也让我质疑它的查找时间,因为它无法评估哪个分支向下找到它正在寻找的值;但是需要通过每个节点来找到它正在寻找的东西。
那么,是否有人可以解释为什么第二个版本(叶子上的值)在Haskell中比第一个版本更普遍?
答案 0 :(得分:3)
我认为这取决于您要模拟的内容以及您尝试对其进行建模的方式。
内部节点存储值且叶子只是叶子的树本质上是一个标准的二叉树(树每个叶子为NULL
,你基本上有一个命令式二叉树)。如果值按排序顺序存储,则现在具有二叉搜索树。以这种方式存储数据有许多特定的优点,其中大部分直接从命令设置传输。
树叶存储数据的树和内部节点仅用于结构的树确实有其优点。例如,红色/黑色树支持两个名为split
和join
的强大操作,这些操作在某些情况下具有优势。 split
将键作为输入,然后破坏性地修改树以生成两棵树,其中一棵树包含少于指定输入键的所有键,另一棵包含剩余键。在某种意义上,join
是相反的:它接收两棵树,其中一棵树的值都小于另一棵树的值,然后将它们融合在一起成为一棵树。这些操作在大多数红/黑树上特别难以实现,但如果所有数据仅存储在叶子中而不是存储在内部节点中则更简单。 This paper detailing an imperative implementation of red/black trees提到由于这个原因,一些较旧的红/黑树实现使用了这种方法。
作为在叶子中存储密钥的另一个潜在优势,假设您要实现连接两个列表的连接操作。如果您没有叶子中的数据,这就像
一样简单concat first second = Branch first second
这是有效的,因为这些节点中没有存储数据。如果数据存储在叶子中,则需要以某种方式将密钥从其中一个叶子移动到新的连接节点,这需要更多时间并且更难以使用。
最后,在某些情况下,您可能希望将数据存储在叶子中,因为叶子与内部节点根本不同。例如,考虑一个解析树,其中叶子存储来自解析的特定终端,而内部节点存储生产中的所有非终结符。在这种情况下,确实存在两种不同类型的节点,因此在内部节点中存储任意数据没有意义。
希望这有帮助!
答案 1 :(得分:3)
你描述了一棵树上有数据的树和#34;一个设计不佳的替代List。"
我同意这可以用作列表的替代品,但它不一定设计得很差!考虑数据类型
data Tree t = Leaf t | Branch (Tree t) (Tree t)
您可以定义cons
和snoc
(附加到列表末尾)操作 -
cons :: t -> Tree t -> Tree t
cons t (Leaf s) = Branch (Leaf t) (Leaf s)
cons t (Branch l r) = Branch (cons t l) r
snoc :: Tree t -> t -> Tree t
snoc (Leaf s) t = Branch (Leaf s) (Leaf t)
snoc (Branch l r) t = Branch l (snoc r t)
这些在O(log n)时间内运行(对于大致平衡的列表),其中n是列表的长度。这与标准链表形成对比,后者具有O(1)cons
和O(n)snoc
操作。您还可以定义一个常量时间append
(如在templatetypedef'答案中)
append :: Tree t -> Tree t -> Tree t
append l r = Branch l r
对于任意大小的两个列表是O(1),而标准列表是O(n),其中n是左参数的长度。
在实践中,您需要定义这些功能的稍微更智能的版本,以尝试保持树的平衡。要做到这一点,在分支处获得一些额外的信息通常是有用的,这可以通过具有多种分支来完成(如在红黑树中具有"红色"和"黑& #34;节点)或在分支处明确包含其他数据,如
data Tree b a = Leaf a | Branch b (Tree b a) (Tree b a)
例如,您可以通过在节点中存储两个子树中的元素总数来支持O(1)size
操作。由于您需要正确地保存有关子树大小的信息,因此树上的所有操作都会变得稍微复杂一些 - 实际上,计算树大小的工作将在构建树的所有操作上分摊(并且巧妙地保留,这样,无论何时需要重建大小,都可以完成最少的工作。
答案 2 :(得分:0)
更多是更好 更糟糕更多。我将解释几个基本注意事项,以说明您的直觉失败的原因。但总的说法是,不同的数据结构需要不同的东西。
在某些上下文中,空叶节点实际上可能是空间(因此也是时间)问题。如果一个节点由一些信息和两个指向其子节点的指针表示,那么每个节点的子节点都是叶子,最终会得到两个空指针。这是每个叶节点的两个机器字,可以累加相当多的空间。一些结构通过确保每个叶子至少保存一条信息来证明其存在来避免这种情况。在某些情况下(例如ropes),每个叶子可能具有相当大且密集的有效载荷。
使内部节点更大(通过在其中存储信息)使得修改树更加昂贵。更改平衡树中的叶子通常会强制您为O(log n)
内部节点分配替换。如果每个都更大,您只需分配更多空间并花费额外时间来复制更多单词。内部节点的额外大小也意味着您可以将较少的树结构放入CPU缓存中。