繁体   English   中英

二叉搜索与二叉搜索树

[英]Binary search vs binary search tree

二分搜索树与使用二分搜索的排序数组相比有什么好处? 仅通过数学分析,我看不出有什么区别,因此我认为低级实现开销肯定存在差异。 平均案例运行时间的分析如下所示。

带二分搜索的排序数组
搜索:O(log(n))
插入:O(log(n))(我们运行二进制搜索来找到插入元素的位置)
删除:O(log(n))(我们运行二分查找来查找要删除的元素)

二叉搜索树
搜索:O(log(n))
插入:O(log(n))
删除:O(log(n))

对于上面列出的操作(如果树不平衡),二叉搜索树的最坏情况是 O(n),所以这看起来实际上比使用二分搜索的排序数组更糟糕。

另外,我不是假设我们必须事先对数组进行排序(这将花费 O(nlog(n)),我们会将元素一个一个地插入到数组中,就像我们对二叉树所做的一样。唯一的好处我可以看到的 BST 是它支持其他类型的遍历,如中序、前序、后序。

您的分析是错误的,对于排序数组,插入和删除都是 O(n),因为您必须物理移动数据为插入腾出空间或压缩它以覆盖已删除的项目。

哦,对于完全不平衡的二叉搜索树,最坏的情况是 O(n),而不是 O(logn)。

查询任何一个都没有什么好处。

但是,当您一次添加一个元素时,构建排序树比构建排序数组要快得多。 因此,完成后将其转换为数组是没有意义的。

另请注意,存在用于维护平衡二叉搜索树的标准算法。 他们摆脱了二叉树的不足并保持了所有其他的优势。 但是,它们很复杂,因此您应该先了解二叉树。

除此之外,大 O 可能是相同的,但常数并不总是如此。 使用二叉树,如果您正确存储数据,您可以很好地利用多个级别的缓存。 结果是,如果您进行大量查询,您的大部分工作都保留在 CPU 缓存内,这大大加快了速度。 如果您谨慎地构建树,则尤其如此。 请参阅http://blogs.msdn.com/b/devdev/archive/2007/06/12/cache-oblivious-data-structures.aspx以了解树的巧妙布局如何大大提高性能的示例。 您对其进行二分搜索的数组不允许使用任何此类技巧。

添加到 @Blindy ,我会说在排序数组中的插入比 CPU 指令 O(logn) 需要更多的内存操作 O(n) std::rotate() ) ,请参阅插入排序。

    std::vector<MYINTTYPE> sorted_array;

    // ... ...

    // insert x at the end
    sorted_array.push_back(x);

    auto& begin = sorted_array.begin();

    // O(log n) CPU operation
    auto& insertion_point = std::lower_bound(begin()
             , begin()+sorted_array().size()-1, x); 
    
    // O(n) memory operation
    std::rotate(begin, insertion_point, sorted_array.end());

我猜左孩子右兄弟树结合了二​​叉树和排序数组的本质。

数据结构 手术 CPU成本 内存操作成本
排序数组 插入 O(logn)(流水线的好处) O(n) 内存操作,参考使用std::rotate()插入排序
搜索 O(登录) 内联实施的好处
删除 O(logn)(使用内存操作进行流水线操作时) O(n) 内存操作,参考std::vector::erase()
平衡二叉树 插入 O(logn)(分支预测影响流水线的缺点,还增加了树旋转的成本) 耗尽缓存的指针的额外成本。
搜索 O(登录)
删除 O(logn)(与插入相同)
左孩子右兄弟树(组合排序数组和二叉树) 插入 O(logn) 平均 如果保持不平衡,则在插入左孩子时不需要std::rotate()
搜索 O(logn)(在不平衡的最坏情况下为 O(n)) 在右兄弟搜索中利用缓存局部性,参考 std::vector::lower_bound()
删除 O(logn)(当超线程/流水线时) O(n) 内存操作参考std::vector::erase()

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM