C++:多集迭代器中 next() 和 prev() 的运行时间?

Posted

技术标签:

【中文标题】C++:多集迭代器中 next() 和 prev() 的运行时间?【英文标题】:C++ : Running time of next() and prev() in a multiset iterator? 【发布时间】:2018-02-17 13:09:41 【问题描述】:

在对应的多重集包含N 元素的multiset<int>::iterator 类型对象上应用next()prev() 函数的时间复杂度是多少?

我知道在 STL 中,多重集被实现为平衡的二叉搜索树,因此我希望每次操作的时间复杂度为 O(log N)(在最坏的情况下),以防我们只是遍历树直到我们找到合适的值,但我有预感这应该是平均 O(1)。

但是如果树是这样实现的——在平衡二叉搜索树中插入元素x时,我们还可以检索到树中小于x的最大数和大于@的最小数987654327@ 在 O(log N) 中。因此理论上,我们可以让树中的每个节点都维护指向其nextprev 元素的指针,以便next()prev() 然后在每个查询中以恒定时间运行。

有人可以分享一下发生了什么吗?

【问题讨论】:

嗯,好问题;我认为 O(1)(甚至不是平均,只是 O(1)),但没有标准的钻研无法找到证据。平均而言,保持 O(1) 而不是 O(1) 可能需要工作。 是的!在我的 O(1) 方法中,它最终可能会使集合中的插入和删除操作慢两倍,因为在插入元素之前必须进行两次查找来记录指针。但这并不意味着没有更有效的方法:) 我认为该标准仅直接保证迭代器操作的恒定摊销时间复杂度 (iterator.requirements.general#10)。我没有找到特定容器的更精确信息(容器上的操作有特定的复杂性,但容器的迭代器上没有)。 @BanachTarski:您不需要进行额外的查找来维护线程树(其中每个节点都有下一个/上一个指针)。您始终可以在给定节点之前或之后将新节点插入到双向链表中,并且在 BST 搜索算法结束时,您始终知道其中一个相邻节点。然而,线程显着增加了节点的大小,使其成为空间/时间的权衡,我认为实现通常会选择空间。 @Yakk 虽然 可能 实现 O(1) 最坏情况迭代器,但由于额外的缓存占用空间,没有人这样做。 【参考方案1】:

标准要求迭代器上的所有操作都在摊销的常数时间内运行:http://www.eel.is/c++draft/iterator.requirements#general-10。基本思想是每个迭代器类别只定义可以在摊销时间内实现的操作。

迭代是很常见的事情,如果迭代器上的operator++(我猜这就是你的意思是next?)是logN,那么在循环中遍历容器就是NlogN。该标准使这成为不可能;因为operator++ 是摊销常数,所以迭代标准中的任何数据结构总是 O(N)。

但是,我在 gcc5.4 上研究了multiset 的实现,至少有一个例子。 setmultiset 都是根据相同的底层结构_Rb_tree 实现的。深入研究一下这个结构,它的节点不仅有左右节点指针,还有父节点指针,而迭代器只是指向节点的指针。

给定二叉搜索树中包含指向其父节点的指针的节点,很容易找出树中的下一个节点是什么:

    如果它有一个右孩子,下降到右孩子。然后尽可能下降左孩子;这是下一个节点。 如果它没有右子节点,则上升到父节点,并确定原始节点是父节点的左子节点还是右子节点。如果节点是父节点的左子节点,则父节点是下一个节点。如果节点是父节点的右边,则父节点已经被处理,因此您需要在父节点和祖父节点之间递归地应用相同的逻辑。

这个 SO 问题显示了具有核心逻辑的源代码:What is the definition of _Rb_tree_increment in bits/stl_tree.h?(由于某种原因很难找到)。

这没有恒定的时间,特别是在 1. 和 2. 我们有下降或上升的循环,最多可能需要 log(N) 时间。但是,您可以轻松地说服自己摊销时间是恒定的,因为当您使用此算法遍历树时,每个节点最多被触摸 4 次:

    曾经在下到它的左孩子的路上。 一旦它从左孩子身上恢复过来,需要考虑自己。 当它下降到它的右孩子时。 从右孩子升序时。

回想起来,我会说这是一个相当明显的选择。对整个数据结构的迭代是一种常见的操作,因此性能非常重要。添加第三个指向节点的指针不是微不足道的空间量,但也不是世界末日;最多它会使数据结构从 3 个字膨胀到 4 个字(2 个指针 + 数据,对齐最少为 3 个,而 3 个指针 + 数据)。如果您使用范围,而不是两个迭代器,另一种方法是维护一个堆栈,然后您不需要父指针,但这仅在您从头到尾迭代时才有效;它不允许从中间的迭代器迭代到结尾(这也是 BST 的重要操作)。

【讨论】:

eel.is/c++draft/iterator.requirements#general-10 指定所有迭代器操作的复杂度要求。 @Holt 该评论将通过提取重要部分来改进;所有迭代器方法都必须是常数时间(摊销)。【参考方案2】:

我认为 next() 和 prev() 将取 1 到 h 之间的任何值,其中 h 是树的高度,大约为 O(log N)。如果您使用 next() 从头到尾遍历 N 个节点,则迭代器应该访问整个树,大约为 2N(2 因为迭代器必须先向下遍历,然后再向上遍历链接节点的指针)。总遍历不是 O(N * log N),因为某些步骤比其他步骤更好。在最坏的情况下,next() 可能是从叶节点到头节点,h 大约为 O(log N)。但这只会发生两次(一次到达 begin(),第二次到达左树最右边的节点到头节点)。所以平均 next() 和 prev() 为 2,即 O(1)。

【讨论】:

以上是关于C++:多集迭代器中 next() 和 prev() 的运行时间?的主要内容,如果未能解决你的问题,请参考以下文章

STL 2—迭代器相关运算——advance(),distance(),next(),prev()

jquery选择器中的加号(+)是什么

C++双向链表

模板中的 C++ 双链表

C++ STL应用与实现17: 如何使用迭代器辅助函数

迭代器