二分搜索与二叉搜索树

时间:2011-05-11 18:34:54

标签: arrays algorithm data-structures binary-tree time-complexity

二进制搜索树对具有二分搜索的排序数组有什么好处?只是通过数学分析我没有看到差异,所以我假设低级实现开销必须存在差异。平均病例运行时间的分析如下所示。

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

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

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

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

4 个答案:

答案 0 :(得分:29)

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

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

答案 1 :(得分:7)

查询中的任何一个都没有太大的好处。

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

答案 2 :(得分:2)

另请注意,有一些标准算法可用于维护平衡二叉搜索树。他们摆脱了二叉树的缺陷并保持了所有其他优势。但它们很复杂,所以你应该先了解二叉树。

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

答案 3 :(得分:0)

除了@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());

我猜Left child right sibling tree结合了二叉树和排序数组的本质。

<身体> 进行插入排序 平均
数据结构 operation CPU成本 内存操作成本
排序数组 插入 O(logn)(流水线带来的好处) O(n)内存操作,请使用std::rotate()
搜索 O(登录) 内联实施的好处
删除 O(logn)(使用内存操作进行流水处理时) O(n)内存操作,请参考std::vector::erase()
平衡二叉树 插入 O(logn)(分支预测的缺点会影响流水线,也增加了树轮换的成本) 耗尽缓存的指针的其他成本。
搜索 O(登录)
删除 O(登录)(与插入相同)
左子右兄弟树(排序数组和二叉树的组合) 插入 O(登录) 如果保持不平衡,则在插入左孩子时不需要std::rotate()
搜索 O(logn)(最坏的情况是不平衡时为O(n)) 在右侧同级搜索中利用了缓存局部性,请参考std :: vector :: lower_bound()
删除 O(登录)(超线程/流水线操作时) O(n)内存操作参考std::vector::erase()