是否有任何技术原因导致std :: lower_bound不专门用于红黑树迭代器?

时间:2014-01-05 14:27:22

标签: c++ algorithm c++11 stl binary-search-tree

如果我将一对红黑树迭代器(std::lower_bound()set::iterator)传递给它,我一直认为map::iterator以对数时间运行。在这种情况下,至少使用libstdc ++实现时,我不得不自己两次注意std::lower_bound()在O(n)时间内运行。据我所知,该标准没有红黑树迭代器的概念; std::lower_bound()会将它们视为双向迭代器,并在线性时间内将它们视为advance我仍然没有看到任何理由为什么实现无法为红黑树迭代器创建特定于实现的迭代器标记,并且如果传入的迭代器恰好是,则调用专用的lower_bound()红黑树迭代器。

是否有任何技术原因导致std::lower_bound()不专门用于红黑树迭代器?


更新:是的,我知道查找成员函数,但事实并非如此。(在模板化代码中,我可能无权访问容器或仅在容器的一部分上工作。)


赏金过期后:我发现Mehrdad和Yakk的答案最有说服力。我也无法决定;我正在给予Mehrdad赏金并接受Yakk的回答。

5 个答案:

答案 0 :(得分:12)

有多种原因:

  1. 使用非成员版本时,可以使用不同的谓词。事实上,在使用时,使用不同的谓词 ,例如std::map<K, V>,因为地图谓词在K上运行,而范围在{{1}对上运行}和K
  2. 即使谓词是兼容的,该函数也有一个接口,它使用树中某处的一对节点,而不是有效搜索所需的根节点。尽管可能存在父指针,但对树的要求似乎不合适。
  3. 提供给算法的迭代器不需要是树的Vt.begin()。它们可能位于树中的某个位置,使得树结构的使用可能效率低下。
  4. 标准库没有树抽象或在树上运行的算法。虽然关联有序容器[可能]是使用树实现的,但相应的算法不会暴露给一般用途。
  5. 我认为值得怀疑的部分是使用通用名称算法,该算法具有双向迭代器的线性复杂度和随机访问迭代器的对数复杂度(我理解比较的数量在两种情况下都具有对数复杂度和运动被认为是快速的。)

答案 1 :(得分:6)

(阐述评论)

我认为可以提供一个与std::set提供的谓词不同的谓词,并且仍然满足部分排序(对于特殊集合)的要求。因此,如果谓词等同于lower_bound排序,则只能用特殊的红黑版本替换std::set算法。

示例:

#include <utility>
#include <algorithm>
#include <set>
#include <iostream>

struct ipair : std::pair<int,int>
{
    using pair::pair;
};

bool operator<(ipair const& l, ipair const& r)
{  return l.first < r.first;  }

struct comp2nd
{
    bool operator()(ipair const& l, ipair const& r) const
    {  return l.second > r.second; /* note the > */ }
};

std::ostream& operator<<(std::ostream& o, ipair const& e)
{  return o << "[" << e.first << "," << e.second << "]";  }

int main()
{
    std::set<ipair, comp2nd> my_set = {{0,4}, {1,3}, {2,2}, {3,1}, {4,0}};
    for(auto const& e : my_set) std::cout << e << ", ";

    std::cout << "\n\n";

    // my_set is sorted wrt ::operator<(ipair const&, ipair const&)
    //        and       wrt comp2nd
    std::cout << std::is_sorted(my_set.cbegin(), my_set.cend()) << "\n";
    std::cout << std::is_sorted(my_set.cbegin(), my_set.cend(),
                                comp2nd()) << "\n";

    std::cout << "\n\n";

    // implicitly using operator<
    auto res = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{3, -1});
    std::cout << *res;

    std::cout << "\n\n";

    auto res2 = std::lower_bound(my_set.cbegin(), my_set.cend(), ipair{-1, 3},
                                 comp2nd());
    std::cout << *res2;
}

输出:

[0,4], [1,3], [2,2], [3,1], [4,0], 

1
1

[3,1]

[1,3]

答案 2 :(得分:6)

好问题。老实说,我认为没有好的/令人信服的/客观的理由

我在这里看到的几乎所有原因(例如谓词要求)对我来说都不是问题。它们可能不方便解决,但它们是完全可解的(例如,只需要一个typedef来区分谓词)。

我在最顶层的答案中看到的最有说服力的理由是:

  

尽管可能存在父指针,但对树的要求似乎不合适。

但是,我认为假设父指针已经实现是完全合理的。

为什么呢?因为如果迭代器指向正确的位置,set::insert(iterator, value)的时间复杂度保证摊销的常量时间

考虑一下:

  1. 树必须保持自我平衡。
  2. 保持树平衡需要在每次修改时查看父节点
  3. 你怎么能避免在这里存储父指针?

    如果没有父指针,为了确保插入后树平衡,必须每次从根开始遍历树,这肯定是摊销的常量时间。

    我显然不能在数学上证明没有可以提供这种保证的数据结构,所以显然我可能错了,这是可能的。
    但是,在没有这样的数据结构的情况下,我所说的是这是一个合理的假设,由setmap的所有实现提供我见过的其实是红黑树。


    旁注,但请注意,我们只是无法在C ++ 03中部分专门化函数(如lower_bound)。
    但这不是一个真正的问题,因为我们可能只需要专门的类型,并将调用转发给该类型的成员函数。

答案 3 :(得分:6)

没有技术上的原因导致无法实现这一点。

为了演示,我将草拟出一种实现这一目的的方法。

我们添加了一个新的Iterator类别SkipableIterator。它是BiDirectionalIterator的子类型和RandomAccessIterator的超类型。

SkipableIterator保证在between可见的上下文中完成的函数std::between有效。

template<typeanme SkipableIterator>
SkipableIterator between( SkipableIterator begin, SkipableIterator end )

between返回beginend之间的迭代器。当且仅当end++begin == end正好在end之后)时,它才会返回begin

从概念上讲,between应该有效地找到“约beginend之间的一个元素,但我们应该小心允许随机跳过列表或平衡的红黑树两个都工作。

随机访问迭代器具有between - return (begin + ((end-begin)+1)/2;

的非常简单的实现

添加新标签也很容易。派生使得现有代码能够正常运行,只要它们正确使用标签调度(并且没有明确专门化),但这里有一个小小的破损问题。我们可以有“标签版本”,其中iterator_category_2iterator_category的细化(或者更少hacky),或者我们可以使用完全不同的机制来讨论可跳过的迭代器(一个独立的迭代器特征?)。

一旦我们拥有此功能,我们就可以编写一个快速有序的搜索算法,该算法适用于map / setmulti。它也适用于像QList这样的跳过列表容器。它甚至可能与随机访问版本的实现相同!

答案 4 :(得分:4)

这是一个非常简单的非技术原因:标准不要求它,任何未来的更改都会无缘无故地破坏与现有编译代码的兼容性。

将时钟回到2000年代早期,在GCC和GCC 3之间的过渡期间,以及之后,在GCC 3的小修订期间。我所研究的许多项目都是二进制兼容的;我们不能要求用户重新编译我们的程序或插件,也不能确定他们编译的GCC版本或编译它们的STL版本。

解决方案:不要使用STL。我们有内部字符串,向量和尝试,而不是使用STL。 表面上标准部分引入的依赖地狱的解决方案非常棒,我们放弃了它。不仅仅是一两个项目。

幸运的是,这个问题已经基本消失了,而像boost这样的库已经介入提供仅包含STL容器的版本。在GCC 4中,我认为使用标准STL容器没有问题,实际上,二进制兼容性更容易,主要是由于标准化工作。

但是,您的更改会引入新的,未说出的依赖

假设明天会出现一个新的数据结构,它基本上击败了红黑树,但并不保证某些专用迭代器可用。一个非常受欢迎的实现只是一些几年前是跳过列表,它在可能显着更小的内存占用量下提供了相同的保证。跳过列表似乎没有成功,但另一个数据结构可能很好。我个人的偏好是使用try,它提供了更好的缓存性能和更强大的算法性能;如果libstdc ++中有人认为这些结构在大多数用途中提供更好的性能,那么它们的迭代器将与红黑树大不相同。

严格遵循标准,即使面对数据结构变化,也可以保持二进制向后兼容性。这是一个用于动态使用的库的Good Thing(TM)。对于一个静态使用的,例如Boost Container库,如果这样的优化得到很好的实现和充分利用,我就不会睁大眼睛。

但对于像libstdc ++这样的动态库,二进制向后兼容性更为重要。