为什么将std :: map实现为红黑树?


193

为什么std::map实现为红黑树

有几个平衡的二进制搜索树(BST)。选择红黑树时在设计上要进行哪些取舍?


26
尽管我见过的所有实现都使用RB树,但请注意,这仍然取决于实现。
托马斯(Thomas)

3
@托马斯 它是与实现相关的,那么为什么所有实现都使用RB树呢?
Denis Gorodetskiy

1
我真的很想知道是否有任何STL实现者考虑过使用跳过列表。
Matthieu M.

2
C ++的映射和集合实际上是有序映射和有序集合。它们不是使用哈希函数实现的。每个查询都将采用,O(logn)而不是O(1),但是值将始终进行排序。从C ++ 11(我认为)开始,有unordered_mapunordered_set是使用哈希函数实现的,尽管它们没有进行排序,但是大多数查询和操作都可以O(1)(平均)在
SomethingSomething

@Thomas是对的,但实际上却没那么有趣。该标准考虑了特定算法或一组算法,从而确保了复杂性。
贾斯汀·迈纳斯

Answers:


125

两种最常见的自平衡树算法可能是Red-Black树AVL树。为了在插入/更新之后平衡树,两种算法都使用旋转的概念,在该概念中,树的节点被旋转以执行重新平衡。

虽然在两种算法中,插入/删除操作均为O(log n),但对于红黑树,重新平衡旋转是O(1)操作,而对于AVL,这是O(log n)操作,因此红黑树在重新平衡阶段的这方面更有效,这是更常用的可能原因之一。

在大多数集合库中都使用红黑树,包括Java和Microsoft .NET Framework提供的树。


54
您听起来好像红黑树可以在O(1)时间内进行树修改,但事实并非如此。红黑树和AVL树的树修改均为O(log n)。因为主要操作已经是O(log n),所以使得树修改的平衡部分是O(1)还是O(log n)变得毫无意义。即使在AVL树所做的所有额外工作之后,树也会变得更加紧密,从而导致查找速度更快。因此,这是一个非常有效的折衷方案,不会使AVL树次于红黑树。
死灵法师

35
您必须超越实际运行时的复杂性才能看到差异-当查找次数多于插入/删除次数时,AVL树的总运行时通常较低。当有更多的插入/删除操作时,RB树的总运行时间会降低。发生中断的确切比例当然取决于实现,硬件和确切用法的许多细节,但是由于库作者必须支持各种用法模式,因此他们必须进行有根据的猜测。AVL也很难实施,因此您可能希望使用它具有已证明的优势。
史蒂夫·杰索普

6
RB树不是“默认实现”。每个实施者选择一个实施。据我们所知,他们都选择了RB树,因此大概是为了提高性能或易于实现/维护。就像我说的那样,性能的断点可能并不意味着他们认为插入/删除比查找多,只是两者之间的比率高于他们认为RB可能胜过AVL的水平。
史蒂夫·杰索普

9
@Denis:不幸的是,获取数字的唯一方法是列出std::map实现列表,跟踪开发人员,并询问他们制定决策所依据的标准,因此这仍然是猜测。
史蒂夫·杰索普

4
所有这一切都缺少了存储每个节点来制定平衡决策所需的辅助信息的成本。红黑树需要1位才能代表颜色。AVL树至少需要2位(代表-1、0或1)。
SJHowe

46

这实际上取决于用法。AVL树通常具有更多的重新平衡轮换。因此,如果您的应用程序没有太多的插入和删除操作,但是在搜索上占了很大比重,那么AVL树可能是一个不错的选择。

std::map 之所以使用Red-Black树,是因为它在节点插入/删除和搜索的速度之间获得了合理的权衡。


1
你确定吗???我个人认为,红黑树要么复杂,要么更简单。唯一的事情是在Rd-Black树中,重新平衡的发生频率比AVL少。
埃里克·厄勒

1
从理论上讲,R / B树和AVL树都具有插入和删除的复杂度O(log n)。但是运营成本的很大一部分是轮换,这在这两种树之间是不同的。请参阅describe.fogcreek.com/joelonsoftware/…Quote:“平衡AVL树可能需要O(log n)旋转,而一棵红黑树最多需要进行两次旋转才能使其达到平衡(尽管可能必须检查O(log n)节点以决定需要旋转的位置。” 相应地编辑了我的评论。
webbertiger

26

AVL树的最大高度为1.44logn,而RB树的最大高度为2logn。在AVL中插入元素可能意味着在树中的某一点进行了重新平衡。重新平衡完成插入。插入新叶子后,必须更新该叶子的祖先直到根,或者直到两个子树深度相等的点。必须更新k个节点的概率为1/3 ^ k。重新平衡为O(1)。删除一个元素可能意味着一个以上的重新平衡(最多树的一半深度)。

RB树是表示为二分搜索树的4阶B树。B树中的4个节点在等效BST中产生两个级别。在最坏的情况下,树的所有节点都是2节点,只有一连串的3节点向下直到一片叶子。该叶距根的距离为2logn。

从根到插入点,必须将4节点更改为2节点,以确保任何插入都不会使叶子饱和。从插入回来,必须分析所有这些节点以确保它们正确表示4个节点。这也可以在树上向下完成。全球成本将相同。天下没有免费的午餐!从树中删除元素的顺序相同。

所有这些树都要求节点携带有关高度,重量,颜色等的信息。只有Splay树没有此类附加信息。但是大多数人都害怕Splay树,因为它们的结构很随机!

最后,树还可以在节点中携带权重信息,从而实现权重平衡。可以应用各种方案。当一个子树包含的元素数超过另一个子树的元素数的三倍时,应该重新平衡。再次通过单旋转或双旋转进行平衡。这意味着最坏的情况是2.4logn。一个人可以摆脱2次而不是3次,这是一个更好的比率,但是这可能意味着在这里和那里不平衡的子树只剩下不到1%。整rick

哪种树最好?肯定是AVL。它们是最简单的代码,最差的高度最接近logn。对于具有1000000个元素的树,AVL的最大高度将为29,RB的高度为40,并且权重平衡的比例为36或50。

还有许多其他变量:随机性,添加,删除,搜索的比例等。


2
好答案。但是,如果AVL最好,为什么标准库将std :: map实现为RB树?
Denis Gorodetskiy

13
我不同意AVL树无疑是最好的。尽管它们的高度很低,但与红色/黑色的树相比,它们总共需要做更多的工作(O(log n)重新平衡工作与O(1)摊销的重新平衡工作)。八卦树可能好得多,而且您关于人们担心它们的说法是没有根据的。那里没有一种通用的“最佳”树平衡方案。
templatetypedef

几乎完美的答案。你为什么说AVL是最好的。那是完全错误的,这就是为什么大多数常规实现都使用Red-Black树的原因。您需要具有较高的读取操作比率才能选择AVL。同样,AVL的内存占用空间比RB少。
埃里克·厄勒

我同意在大多数情况下AVL往往会更好,因为通常搜索树的频率比插入树的频率高。当RB树在多数写情况下稍有优势,而在多数读情况下稍有劣势时,为什么这么广泛地认为RB树更好呢?真的相信您会插入比您发现的更多的文字吗?
doug65536

25

先前的答案仅针对树的替代方案,而红黑色可能仅出于历史原因而保留。

为什么不使用哈希表?

类型仅要求将<运算符(比较)用作树中的键。但是,哈希表要求每种键类型都必须hash定义一个函数。对于通用编程,将类型要求最小化是非常重要的,因此您可以将其与多种类型和算法一起使用。

设计一个好的哈希表需要对将使用它的上下文有充分的了解。应该使用开放式寻址还是链接链接?调整大小之前应接受什么级别的负载?应该使用昂贵的散列来避免冲突,还是使用粗糙而又快速的散列?

由于STL无法预测哪个是您的应用程序的最佳选择,因此默认值需要更灵活。树木“可以正常工作”,并且可以很好地缩放。

(C ++ 11确实使用添加了哈希表unordered_map。您可以从文档中看到它需要设置策略来配置许多这些选项。)

那其他树呢?

与BST不同,红黑树提供快速查找并具有自我平衡能力。另一位用户指出了它比自平衡AVL树的优势。

Alexander Stepanov(STL的创建者)说,如果他再写std::map一次,他将使用B *树而不是红黑树,因为它对于现代内存缓存更加友好。

此后最大的变化之一就是缓存的增长。高速缓存未命中的代价非常高,因此现在引用的位置更加重要。具有低引用局部性的基于节点的数据结构意义不大。如果我今天要设计STL,那我会有不同的容器集。例如,对于实现关联容器,内存中的B *树比红黑树好得多。- 亚历山大·斯捷潘诺夫Alexander Stepanov)

地图应始终使用树吗?

另一个可能的地图实现是排序向量(插入排序)和二进制搜索。这对于不经常修改但经常被查询的容器来说是很好的选择。我经常在C语言中这样做,qsort并且bsearch是内置的。

我什至需要使用地图吗?

缓存注意事项意味着,即使在学校里我们曾经教过的情况(例如从列表中间删除一个元素),使用std::liststd::deque结束缓存也几乎没有意义std:vector。应用相同的推理,使用for循环对列表进行线性搜索通常比为几个查询构建地图更有效和更清洁。

当然,选择可读的容器通常比性能更重要。


3

2017-06-14更新:我发表评论后,webbertiger编辑了答案。我应该指出,对我来说,它的答案现在好得多。但是我保留了我的答案,就像其他信息一样。

由于以下事实,我认为第一个答案是错误的(更正:不再同时存在),第三个答案的确认是错误的。我觉得我必须澄清一些事情...

最受欢迎的2种树是AVL和Red Black(RB)。主要区别在于利用率:

  • AVL:如果咨询(阅读)比率大于操纵(修改)比率,则更好。内存占用面积略小于RB(由于需要着色)。
  • RB:在一般情况下,咨询(阅读)与操作(修改)之间有平衡,或者在咨询上进行更多修改的情况下更好。由于存储了红黑色标记,因此内存占用量略大。

主要区别来自着色。RB树中的重新平衡操作确实比AVL少,因为着色使您有时可以跳过或缩短具有较高成本的重新平衡操作。由于颜色的原因,RB树还具有更高级别的节点,因为它可以接受黑色节点之间的红色节点(可能会有2倍以上的级别),从而使搜索(读取)的效率有所降低……但是,因为它是一种常数(2x),则保持在O(log n)中。

如果您考虑一棵树的修改对性能的影响(显着)与对一棵树的咨询的性能影响(几乎无关紧要),那么在一般情况下,相对于AVL而言,更倾向于RB。


2

这只是您实现的选择-它们可以作为任何平衡树来实现。各种选择都是可比较的,只是有微小的差异。因此,任何事物都一样。

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.