您会推荐哪种自平衡二叉树?


18

我正在学习Haskell,作为练习,我正在制作二叉树。制作了常规的二叉树之后,我想使其适应自我平衡。所以:

  • 哪个最有效?
  • 哪个最容易实现?
  • 最常使用哪个?

但是至关重要的是,您推荐哪个?

我认为这属于此处,因为它尚有争议。


在效率和易于实施方面,总体效率得到了很好的定义,但是对于您的实施,我认为最好的办法是尽可能多地实施,然后让我们知道哪一种效果最好……
glenatron

Answers:


15

我建议您从红黑树AVL树开始

红黑树的插入速度更快,但AVL树的查找边缘略微。AVL树可能更容易实现,但根据我自己的经验,可能还不够。

AVL树确保每次插入或删除后树均平衡(没有子树的平衡因子大于1 / -1,而红黑树则确保树在任何时候均保持合理平衡)。


1
就个人而言,我发现红黑插入比AVL插入更容易。原因是通过与B树的(不完美)类比。插入很奇怪,但是删除很邪恶(需要考虑很多情况)。实际上,我不再拥有自己的C ++红黑删除实现-当我意识到(1)我从未使用过它时就删除了它-每次我想要删除时我都删除了多个项目,因此我从树转换为列表,从列表中删除,然后转换回树,并且(2)无论如何它都被破坏了。
Steve314,2011年

2
@ Steve314,红黑树比较容易,但是您还无法实现可行的实现吗?那么AVL树是什么样的?
dan_waterworth 2011年

@dan_waterworth-我什至还没有实现可以使用的insert方法的实现-有了注释,了解了基本原理,但是却没有正确地结合动力,时间和信心。如果我只想要能正常工作的版本,那只是从教科书中复制和伪代码,然后翻译(不要忘了C ++具有标准的库容器),但是这样做的乐趣何在?
Steve314 2011年

顺便说一句-我相信(但不能提供参考),一本相当受欢迎的教科书包含一种平衡二叉树算法之一的错误实现-不确定,但可能是红黑删除。因此,不仅是我;-)
Steve314 2011年

1
@ Steve314,我知道,在命令式语言中,树可能非常复杂,但是令人惊讶的是,在Haskell中实现它们很容易。我在周末写了规则的AVL树和一维空间变体,它们都只有大约60行。
dan_waterworth 2011年

10

如果您可以使用随机数据结构,我会考虑使用其他方法:跳过列表

从高级的角度来看,它是树结构,除了它不是实现为树,而是实现为具有多层链接的列表之外。

您将获得O(log N)插入/搜索/删除,并且无需处理所有棘手的重新平衡情况。

我从来没有考虑过用功能语言来实现它们,并且Wikipedia页面没有显示任何内容,因此它可能并不容易(变得不可变性)。


我真的很喜欢跳过列表,尽管不是使用功能性语言,但我之前已经实现了它们。我想我会在这之后尝试它们,但是现在我在自平衡树上。
dan_waterworth 2011年

同样,人们经常使用跳过列表来处理并发数据结构。使用Haskell的并发原语(例如MVar或TVar)来代替强制不变性可能更好。虽然,这不会告诉我很多有关编写功能代码的知识。
dan_waterworth 2011年

2
@ Fanatic23,跳过列表不是ADT。ADT是一个集合或一个关联数组。
dan_waterworth 2011年

@dan_waterworth我不好,你是对的。
Fanatic23 2011年

5

如果您想从一个相对简单的结构开始(AVL树和红黑树都比较笨拙),则一个选项是treap-命名为“ tree”和“ heap”的组合。

每个节点都获得一个“优先级”值,该值通常在创建节点时随机分配。节点位于树中,以便遵守键顺序,并遵守类似堆的优先级顺序。类堆排序意味着父级的两个子级都比父级低。

EDIT 删除了上面“键值内”的内容-优先级和键顺序同时使用,因此即使对于唯一键,优先级也很重要。

这是一个有趣的组合。如果密钥是唯一的,并且优先级是唯一的,则任何节点集都有唯一的树结构。即使这样,插入和删除还是有效的。严格来说,树可以不平衡到有效地成为链表的程度,但这极不可能(与标准二叉树一样),包括在正常情况下,例如按顺序插入键(与标准二叉树不同)。


1
+1。Treaps是我个人的选择,我什至写了一篇有关如何实现它们的博客文章
P Shved

5

哪个最有效?

含糊且难以回答。所有的计算复杂度都是明确定义的。如果这就是效率的意思,那么就没有真正的争论。确实,所有好的算法都带有证明和复杂性因素。

如果您指的是“运行时”或“内存使用”,则需要比较实际的实现。然后,语言,运行时间,操作系统和其他因素开始起作用,使问题难以回答。

哪个最容易实现?

含糊且难以回答。有些算法对您来说似乎很复杂,但对我来说却微不足道。

最常使用哪个?

含糊且难以回答。首先是“由谁来”?这部分吗?只有Haskell?C或C ++呢?其次,存在专有软件问题,我们无权访问源进行调查。

但是至关重要的是,您推荐哪个?

我认为这属于此处,因为它尚有争议。

正确。由于您的其他条件不是很有帮助,因此您将获得所有这些。

您可以获取大量树算法的源代码。如果您想学习一些东西,则可以简单地实现所能找到的每一个。而不是要求“推荐”,只需收集您可以找到的每种算法。

这是清单:

http://en.wikipedia.org/wiki/Self-balancing_binary_search_tree

有六个流行的定义。从这些开始。


3

如果您对Splay树感兴趣,则可以找到一个更简单的版本,我认为这些版本是Allen和Munroe在论文中首次描述的。它没有相同的性能保证,但是避免了在处理“ zig-zig”与“ zig-zag”重新平衡时的复杂性。

基本上,在搜索(包括搜索要删除的插入点或节点)时,找到的节点将直接朝根的方向旋转,自下而上(例如,递归搜索功能退出)。在每个步骤中,您都选择一个向左或向右旋转,具体取决于您要向根部向上拉出另一步的孩子是右孩子还是左孩子(如果我正确记得我的旋转方向,那就分别是)。

像Splay树一样,其想法是最近访问的项始终位于树的根附近,因此可以快速再次访问。更简单的是,这些Allen-Munroe旋转到根树(我称它们为树-不知道正式名称)可以更快,但它们没有相同的摊销性能保证。

一件事-由于此数据结构从定义上讲即使是针对查找操作也会发生突变,因此可能需要一元实现。IOW,它可能不适合函数式编程。


即使在找到时,Splay也会令人烦恼,因为它们会修改树。这在多线程环境中将是非常痛苦的,这首先就是使用像Haskell这样的功能语言的主要动机之一。再说一次,我以前从未使用过函数式语言,所以也许这不是一个因素。
Quick Joe Smith

@Quick-取决于您打算如何使用树。如果您在真正的功能样式代码中使用它,则要么在每个发现中删除变异(使Splay树变得有点傻),要么最终会在每次查找中复制大部分的二叉树,并随着工作的进展跟踪您正在使用的树状态(可能使用单子风格的原因)。如果您在创建新树状结构后不再引用旧树状结构,则编译器可能会优化复制(在函数式编程中常见类似的假设),但事实并非如此。
Steve314 2011年

两种方法听起来都不值得付出努力。再说一次,绝大部分功能纯语言都不是。
快速乔史密斯

1
@Quick-复制树是您要使用纯函数语言对任何树数据结构进行操作,以使诸如插入之类的算法发生变异的方法。从源代码上讲,该代码与进行就地更新的命令性代码没有什么不同。大概已经针对不平衡的二叉树解决了差异。只要您不尝试将父链接添加到节点,重复项将至少共享公共子树,并且如果不是完美的话,Haskell中的深度优化是相当难的。原则上来说,我本人是反对Haskell的,但这并不一定是问题。
Steve314,2011年

2

一个非常简单的平衡树是AA树。它的不变性更简单,因此实现起来也更容易。由于其简单性,其性能仍然很好。

作为高级练习,您可以尝试使用GADT来实现平衡树的变体之一,这些变体的不变性由类型系统类型强制执行。

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.