【问题标题】:Are there real-world reasons to employ a Binary Search Tree over a Binary Search of semi-contiguous list?是否有真实的理由在半连续列表的二分搜索上使用二分搜索树?
【发布时间】:2021-09-03 15:41:58
【问题描述】:

我正在观看有关算法的大学讲座,似乎其中有很多几乎完全依赖某种特定类型的二叉搜索树来完成查询/数据库/搜索任务。

我不理解这种对二分搜索的痴迷。似乎在广大的大多数场景中,如果是静态数据,BSP 可以替换为排序数组,或者如果插入动态发生,则可以替换为排序桶列表,然后是 可以对它们使用二分搜索

使用这种方法,您可以获得与 BST 相同的算法复杂度(至少用于查询),方式更好的缓存一致性,更少的内存碎片(以及更少的 gc 分配,具体取决于您使用的语言) re in),并且可能更容易编写。

根本问题是 BSP 完全是内存幼稚的——他们完全关注 O(n) 复杂性,他们忽略了内存碎片和缓存一致性的非常真实的性能考虑......我我错过了什么?

【问题讨论】:

    标签: performance memory-management binary-search-tree binary-search


    【解决方案1】:

    二叉搜索树 (BST) 并不完全等同于建议的数据结构。当涉及到动态插入和删除排序值(假设它们正确平衡)时,它们的渐近复杂性更好。例如,当您何时动态构建 top-k 值的索引时:

    while end_of_stream(stream):
        value <- stream.pop_value()
        tree.insert(value)
        tree.remove_max()
    

    由于线性时间插入,排序数组在这种情况下效率不高。分桶列表的复杂性并不比普通列表渐近好,而且还受到线性时间搜索的影响。可以注意到在这种情况下可以使用堆,实际上在这里使用堆可能会更好,尽管它们并不总是可以互换的。

    话虽如此,您是对的:BST 很慢,会导致大量缓存未命中和碎片等。因此,它们通常被更紧凑的变体取代,例如B-trees。 B-tree 使用排序数组索引来减少节点跳转的数量并使数据结构更加紧凑。它们可以与一些 4 字节指针优化混合使用,以使它们更加紧凑。 B-trees 之于 BST 就像分桶链表之于普通链表。 B 树非常适合为存储在慢速存储设备上的大型数据集构建动态数据库索引(因为它的大小):它们使应用程序能够使用非常很少的存储设备查找来获取与键关联的值(例如,在 HDD 上非常慢)。现实世界用例的另一个例子是区间树。

    请注意,使用压缩方法可以减少内存碎片。对于 BST/B 树,可以像堆中一样重新排序根节点。然而,压缩并不总是很容易应用,尤其是在像 C/C++ 这样的带有指针的本地语言上,尽管有些 very clever methods exists 这样做。

    请记住,B 树只适用于大型数据集(尤其是那些不适合缓存的数据集)。在相对较小的情况下,仅使用普通数组甚至排序数组通常是一个非常好的解决方案。

    【讨论】:

    • 您提到在插入方面,桶列表并不比普通列表好,为什么会这样?使用桶列表(据我了解:一堆链接在一起的固定大小的数组),您只需重新排序和/或重新分配您要插入的固定大小的数组,而不是整个数据结构。搜索也不是线性时间,因为 MoveNext/枚举会移动许多元素(取决于您的存储桶大小)。因此,使用存储桶列表,您可以通过改变存储桶的大小来调整查询/插入删除性能。
    • 桶列表中的插入可以是O(1),就像在普通列表中一样。但是在这种情况下,您需要在执行操作之前执行二分法以在数据结构中找到要插入/删除的项目。二分法是O(n) 在普通列表上,因为您需要遍历列表节点(随机访问节点是不可能的)。同样的事情也适用于分桶列表:虽然在桶中可以进行随机访问,但对于节点来说情况并非如此。因此,在这种情况下仍然需要一个节点遍历,因此O(n) 复杂度(桶只是减少了n 之前的隐藏常数因子)。
    • 请注意,我假设桶的大小在答案中是有界的(在实现中通常是这种情况)。因此,当您说“移动多个元素”时,请记住跳过的元素数量也是有限的。如果您不限制存储桶大小,则会导致其他问题,并且由此产生的复杂性接近已排序数组的复杂性。特别是线性的插入(因为大小不再有界)。最后有一个线索:小水桶不好,大水桶也是。
    • 最后请注意,您可以设计一个带有大小为sqrt(n) 的桶的桶列表。这很难做到,因为插入/删除总是发生在同一个位置。调整存储桶的大小需要线性时间,但这个成本可以摊销(就像 C++ 向量一样),所以我认为我们可以忽略这一点。无论如何,实现的最佳复杂度可能类似于O(sqrt(n)),这与使用树的复杂度O(log(n)) 相差甚远。请注意,此实现中由存储桶引起的内存碎片可能是一个问题(我认为甚至比树更严重)。
    • 我还假设桶大小是固定的/有界的。由于碎片,我会非常小心动态或异构的存储桶大小,它也可能会增加代码复杂性。我有兴趣研究 b 树!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-18
    • 2023-03-08
    相关资源
    最近更新 更多