在半连续列表的二分搜索上使用二分搜索树有现实世界的理由吗?

问题描述

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

我不明白这种对二分搜索的痴迷。似乎在绝大多数场景中,在静态数据的情况下,BSP 可以用排序数组替换,如果插入动态发生,则可以用排序的桶列表替换,然后可以对它们进行二分搜索

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

基本问题是 BSP 完全没有内存——他们的重点是完全在 O(n) 复杂度上,他们忽略了内存碎片和缓存一致性的非常真实的性能考虑......我我错过了什么?

解决方法

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

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

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

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

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

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