【问题标题】:Why is std::map implemented as a red-black tree?为什么 std::map 被实现为红黑树?
【发布时间】:2023-03-14 02:48:01
【问题描述】:

为什么std::map 被实现为red-black tree

那里有几个平衡的binary search trees (BST)。选择红黑树时的设计权衡是什么?

【问题讨论】:

  • 虽然我见过的所有实现都使用 RB-tree,但请注意这仍然依赖于实现。
  • @Thomas。它是依赖于实现的,那么为什么所有的实现都使用 RB-trees?
  • 我真的很想知道是否有任何 STL 实施者考虑过使用跳过列表。
  • C++的map和set其实是有序map和有序集。它们不是使用散列函数实现的。每个查询都将采用O(logn) 而不是O(1),但这些值将始终进行排序。从 C++11(我认为)开始,有 unordered_mapunordered_set,它们是使用哈希函数实现的,虽然它们没有排序,但大多数查询和操作都可以在 O(1)(平均)中实现
  • 我很惊讶没有人谈论迭代器失效。 STL 的 API 保证,当您从 std::map 插入或删除元素时,指向其他元素的迭代器不会失效。这使得为​​每个动态分配的节点存储多个元素,同时满足通常的时间复杂度保证,即使不是完全不可能,也非常困难。 (对std::map 的查询和更新必须花费最坏的对数时间。)因此,在实践中,std::map 实现必须是某种自平衡二叉树。

标签: c++ dictionary data-structures stl binary-search-tree


【解决方案1】:

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

虽然在这两种算法中插入/删除操作都是 O(log n),但在红黑树重新平衡旋转的情况下是 O(1) 操作,而对于 AVL,这是 O(log n) 操作,使得红黑树在这方面的再平衡阶段效率更高,也是它更常用的可能原因之一。

大多数集合库都使用红黑树,包括来自 Java 和 Microsoft .NET Framework 的产品。

【讨论】:

  • 您听起来好像红黑树可以在 O(1) 时间内进行树修改,但事实并非如此。红黑树和 AVL 树的树修改都是 O(log n)。这使得树修改的平衡部分是 O(1) 还是 O(log n) 变得没有意义,因为主要操作已经是 O(log n)。即使在 AVL 树所做的所有稍微额外的工作之后,也会产生更紧密平衡的树,从而导致查找速度稍快。所以这是一个完全有效的权衡,不会使 AVL 树不如红黑树。
  • 您必须超越复杂性而看到实际运行时才能看到差异 - 当查找比插入/删除多得多时,AVL 树的总运行时间通常较低。当有更多的插入/删除时,RB 树的总运行时间会更低。中断发生的确切比例当然取决于实现、硬件和确切使用的许多细节,但由于库作者必须支持广泛的使用模式,他们必须进行有根据的猜测。 AVL 实施起来也稍微困难一些,因此您可能希望使用它来获得证明的好处。
  • RB 树不是“默认实现”。每个实施者选择一个实施。据我们所知,他们都选择了 RB 树,所以大概这要么是为了性能,要么是为了便于实施/维护。正如我所说,性能断点可能并不意味着他们认为插入/删除比查找更多,只是两者之间的比率高于他们认为 RB 可能超过 AVL 的水平。
  • @Denis:不幸的是,获得数字的唯一方法是列出std::map 实现,追踪开发人员,并询问他们使用什么标准做出决定,所以这仍然是猜测.
  • 所有这些都缺少存储做出平衡决策所需的辅助信息的每个节点的成本。红黑树需要 1 位来表示颜色。 AVL 树至少需要 2 位(表示 -1、0 或 1)。
【解决方案2】:

这真的取决于使用情况。 AVL 树通常有更多的再平衡轮换。因此,如果您的应用程序没有太多的插入和删除操作,但对搜索的权重很大,那么 AVL 树可能是一个不错的选择。

std::map 使用红黑树,因为它在节点插入/删除和搜索速度之间取得了合理的折衷。

【讨论】:

  • 你确定吗???我个人认为红黑树要么更复杂,要么更复杂,永远不会更简单。唯一的问题是,在 Rd-Black 树中,重新平衡的发生频率低于 AVL。
  • @Eric 从理论上讲,R/B 树和 AVL 树的插入和删除都有复杂度 O(log n) )。但是运营成本的很大一部分是轮换,这在这两棵树之间是不同的。请参考discuss.fogcreek.com/joelonsoftware/… 引用:“平衡 AVL 树可能需要 O(log n) 次旋转,而红黑树最多需要两次旋转才能使其达到平衡(尽管它可能需要检查 O(log n)节点来决定在哪里旋转是必要的)。”相应地编辑了我的 cmets。
  • 非常感谢让我注意到最大旋转 2 以插入 RB 树。你说的对。我没有意识到这一点。就像你说的那样,重新着色发生在 Log(n) 中,但旋转的成本要低得多。我认为您的回答很棒,我不记得前一个是什么;-)。谢谢!!!
【解决方案3】:

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

为什么不用哈希表?

一个类型只需要 < 运算符(比较)用作树中的键。但是,哈希表要求每个键类型都定义了一个hash 函数。将类型要求保持在最低水平对于泛型编程非常重要,因此您可以将其与各种类型和算法一起使用。

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

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

(C++11 确实添加了带有unordered_map 的哈希表。您可以从documentation 看到它需要设置策略来配置其中的许多选项。)

其他树呢?

与 BST 不同,红黑树提供快速查找和自我平衡。另一位用户指出了它相对于自平衡 AVL 树的优势。

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

从那时起,最大的变化之一就是缓存的增长。 缓存未命中成本非常高,因此引用的局部性要高得多 现在很重要。基于节点的数据结构,具有较低的局部性 参考,意义不大。如果我今天设计 STL,我 会有一组不同的容器。例如,内存中 B*-tree 是比红黑树更好的实现选择 一个关联容器。 - Alexander Stepanov

地图应该总是使用树吗?

另一种可能的映射实现是排序向量(插入排序)和二分查找。这会很好用 用于不经常修改但经常查询的容器。 我经常在 C 中这样做,因为 qsortbsearch 是内置的。

我还需要使用地图吗?

缓存考虑意味着使用std::liststd::deque 而不是std:vector 很少有意义,即使对于我们在学校教过的那些情况(例如从列表中间删除一个元素)。 应用相同的推理,使用 for 循环对列表进行线性搜索通常比为几次查找构建地图更高效、更简洁。

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

【讨论】:

    【解决方案4】:

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

    RB-trees 是 4 阶 B-trees,表示为二叉搜索树。 B 树中的 4 个节点导致等效 BST 中的两个级别。在最坏的情况下,树的所有节点都是 2 节点,只有一个 3 节点链向下到叶子。那片叶子与根的距离为 2logn。

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

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

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

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

    还有很多其他变量:随机性、添加、删除、搜索的比率等。

    【讨论】:

    • 好答案。但如果 AVL 是最好的,为什么标准库将 std::map 实现为 RB 树?
    • 我不同意 AVL 树无疑是最好的。尽管它们的高度较低,但它们(总共)需要比红/黑树更多的工作来进行重新平衡(O(log n) 重新平衡工作与 O(1) 摊销重新平衡工作)。张开的树可能会好得多,而您关于人们害怕它们的断言是没有根据的。没有一种通用的“最佳”树平衡方案。
    • 几乎完美的答案。为什么说AVL最好。这是完全错误的,这就是为什么大多数通用实现都使用红黑树。您需要有相当高的读取操作比率才能选择 AVL。此外,AVL 的内存占用比 RB 少。
    • 我同意 AVL 在大多数情况下往往更好,因为通常搜索树的频率高于插入树的频率。为什么 RB 树被广泛认为是更好的,因为它在以写入为主的情况下具有轻微的优势,更重要的是,在以读取为主的情况下具有轻微的劣势?真的相信你会插入比你发现的更多吗?
    • 因为 AVL 树“肯定”是最好的而被否决。必须考虑 # 次读取与 # 次写入以确定是否首选 AVL。
    【解决方案5】:

    这只是您实现的选择 - 它们可以作为任何平衡树来实现。各种选择都具有可比性,差异很小。因此,any 和 any 一样好。

    【讨论】:

      【解决方案6】:

      2017-06-14 更新:webbertiger 在我发表评论后编辑其答案。我应该指出,它的答案现在对我来说好多了。但我保留了我的答案作为附加信息......

      由于我认为第一个答案是错误的(更正:不再是两者),而第三个肯定是错误的。我觉得我必须澄清一些事情......

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

      • AVL:如果咨询(阅读)的比例大于操作(修改),则更好。内存占用略小于 RB(由于着色需要位)。
      • RB:在咨询(读取)和操作(修改)之间或更多修改超过咨询之间存在平衡的一般情况下更好。由于存储了红黑标志,内存占用略大。

      主要区别在于颜色。 RB 树中的重新平衡操作确实比 AVL 少,因为着色使您有时可以跳过或缩短具有相对高成本的重新平衡操作。由于着色,RB 树也有更高级别的节点,因为它可以接受黑色节点之间的红色节点(有大约 2 倍以上的级别的可能性),这使得搜索(读取)的效率有点低......但因为它是一个常数 (2x),它保持在 O(log n)。

      如果您考虑修改树的性能影响(显着)VS 咨询树的性能影响(几乎微不足道),在一般情况下,自然会更喜欢 RB 而不是 AVL。

      【讨论】:

        猜你喜欢
        • 2014-05-05
        • 2018-01-06
        • 1970-01-01
        • 2014-09-23
        • 2010-12-14
        • 2012-01-06
        • 2016-01-16
        • 1970-01-01
        相关资源
        最近更新 更多