填充红黑树的最有效方法是什么?

时间:2018-05-20 21:14:35

标签: c++ algorithm data-structures red-black-tree

假设我知道某些数据集及其控制顺序的所有内容 - 将其组织成红黑树的最有效方法是什么?

或者,在流行的std::set/map实现(基于“红黑树”)的背景下 - 使用上述数据集填充std::set的最有效方法是什么?

在你回答之前,请考虑一下:

  • afaik,红黑树有便宜的O(1)(正确暗示)插入...除非树深度超出某些限制,在这种情况下它将被重新平衡(以O(log N)成本) - 就像std::vector::push_back()的情况一样,我们最终得到了摊销的常数复杂性

  • e.g。如果数据集是值列表[0,999],则应该有一系列从不触发重新平衡的提示插入(即保留每个插入O(1))。

非常简单的例子(需要弄清楚如何选择这些YYY / ZZZ值):

std::set<int> s;
std::vector< std::set<int>::iterator > helper(1000);

helper[0] = s.insert(0);
helper[1] = s.insert(helper[0], 1);
//...
helper[500] = s.insert(helper[YYY], 500);
//...
helper[999] = s.insert(helper[ZZZ], 999);

我在寻找:

  1. 一种算法,允许我填充(“红黑树” - 基于)std::set与(特别)准备(任意长)序列,其中每个插入保证O(1)

  2. 应该有办法减少额外的内存需求(即helper的大小)或理想地消除对它的需要

  3. 在最糟糕的情况下填充树的算法(了解传入数据集应该的样子) - 当我们最终得到最大可能的{时,就是这种情况{1}}事件

  4. 奖金目标是获得“AVL树”问题1-3的答案 - 基于rebalance

  5. 谢谢

2 个答案:

答案 0 :(得分:0)

首先,对输入进行排序。

理想情况是将排序的输入放入平衡的二叉树中,但只是假装它在树中就可以了;它需要更多的簿记。它实际上不一定是真正的树数据结构;你可以使用一个数组,其中根是元素0,元素i的子元素是2i + 1和2i + 2。在任何情况下,都可以递归地构建树。​​

如果您拥有原始数据的平衡二叉树,则需要将其复制到集合中,而不会产生任何重新平衡。要做到这一点,请对树进行广度优先扫描(如果使用上面提到的数组,这只是对数组的顺序扫描,这使得此步骤非常简单)。您可以保存BFS中每个级别的插入点,以便获得下一级别的提示(因此您需要能够将迭代器保存到大约一半的树),但它会更容易,也可能更快在构建它时从组开始,从每个级别的开始处开始,然后在每次插入后以两个元素前进。

这些都不比仅按顺序构建集更快。但它是这个问题的答案。

对于最差的hinted-insert填充,以反向排序顺序插入元素,使用前一个插入点的插入点提示每个插入。

我认为相同的算法适用于AVL树。

答案 1 :(得分:0)

找到了一种不需要额外内存并且可以用于任何二进制搜索树(红黑/ AVL / etc)的算法:

  1. 传入数据数组,表示一个“扁平的”二叉树(根在[0],根子在[1]和[2],左节点子在[3]和[4],右节点子)在[5]和[6]等)。技巧是选择每个子树的根,以使生成的二叉树充满每个lvl(但最后一个),在最后一级,所有节点形成“不间断”行。像这样:

         N11
       /     \
     N21      N22
     / \      /
    N31 N32 N33
    

有关如何将已排序数组转换为此类序列的信息,请参见下面的代码。我相信,对于任何序列,只有这样一种可能的方式将其排列在二进制搜索树中-即,您在此处最终获得某种“稳定性”保证(对于给定的序列长度,我们确切知道每个元素将在何处终止)在树上)。

  1. 然后对数据执行一次传递,并逐级填充树。在每个级别上,我们确切地知道在切换到下一个lvl(或数据用完)之前要拉(2 ^(lvl-1))个元素。在每次迭代的开始,我们将位置重置为最左侧的元素(std::set<T>::begin()),并在插入左右子元素之后,移至当前级别的下一个叶子(从上一个{{1 }}电话。

注释:

  • 具有++it的性能优势(与提示插入的排序序列相比为5-10%)

  • 不幸的是,MS红黑树实现最终在这里执行了许多不必要的工作-检查相邻元素(以确保插入不会破坏二进制树不变性),重新绘制节点(出于某种原因新插入的节点)总是红色),可能还有其他。检查邻居包括其他比较和内存访问,以及遍历多个级别的树

  • 如果该方法在内部实现(不使用insert()公共接口作为一种功能,它期望数据符合要求并声明“未定义行为”,则该方法的好处会更高)。 t ...

  • ...在这种情况下,甚至更好的算法将以树深度优先的方式进行填充,并要求对输入数据进行不同的重新排列(在上面的示例中,std::set<int>)。我们最终也将只进行一次树遍历... las,尽管如此,它不能使用std::set公共接口来实现此方法-它将在构造的每一步上强制执行红黑树不变,从而导致不必要的重新平衡< / p>

代码:(MSVC 2015,对马铃薯质量的赦免-大约一个小时就写在膝盖上了)

[N11, N21, N31, N32, N22, N33]

典型结果:

std::set

我很确定测量结果并不完全准确,但是关系始终存在-优化版本总是更快。另外,请注意,用于重新排列元素的算法可能会得到改进-目的是优化树填充(而不是输入数据准备)。