STL 中的某些容器没有查找函数

some containers in stl don't have find function

本文关键字:查找 函数 STL      更新时间:2023-10-16

一些STL容器(如std::liststd::vector)没有find()方法作为成员函数。为什么?我知道从<algorithm>中可以选择使用std::find,但这种使用并不是100%自然的。

总体设计原则是尽可能使用std::find,并在更高效时实现find成员功能。

确实具有find成员的容器是具有比在std::find中执行的线性搜索更有效的元素查找机制的容器。例如,诸如std::setstd::map之类的二进制搜索树,或者诸如它们的对应unordered之类的哈希表。

findlower_boundupper_bound成员函数仅在比使用非成员等价物效率更高时提供,或者在非成员无法操作

特别要注意的是,std::string具有find功能,该功能为字符搜索提供类似std::find()的线性搜索功能,为子字符串搜索提供类似于std::search()的功能:虽然非成员版本可能具有相同的big-O效率,但如果专用机器代码指令经常可用于"字符串"搜索,它们的效率可能会更低。还有历史、方便和易于移植的因素。

除了效率问题之外,值得注意的是,一些容器:

  • 本质上要么已排序(multi-setmap)要么未排序(unordered_mapunordered_set),通常未排序(例如std::string),或者很容易(std::vector

  • 公开支持前向迭代和/或随机接入

  • 可能私下支持二进制搜索

  • 有这样一个专门的用于元素访问的公共API,因此算法的潜在重用是相对有限的(例如unordered_map::bucket/::begin(n)等)

同样令人感兴趣的是,可以使用许多算法在vector中进行搜索:

  • std::find进行强力线性O(n)搜索,它将首先"找到"较低索引元素,

  • CCD_ 31需要一个排序的向量,但为了实现O(log2n)的复杂度而四处跳跃。

  • 其他选项,如外推搜索和散列可能适用

您将如何选择实现和添加哪些成员?看起来有点武断。尽管如此,从性能角度来看,使用哪种元素的选择可能很重要:对于一百万个元素,find在匹配前平均进行五十万个元素的比较,当元素不在时平均进行一百万个元素的对比,而binary_search通常会进行约20次比较。

具有find的容器通常不提供这样的灵活性,并且它们提供的find和/或lower_bound/upper_bound可以被视为非成员等价物的替代品,并且可能是搜索容器的唯一合理方式。

因为algorithm中的std::find函数适用于它们。

通常,像std::vectorstd::list这样的容器具有线性搜索时间复杂性。因此,将成员find函数附加到它们是冗余的,因为已经存在std::find。对于其他容器(例如,std::setstd::map等),有更好的方法(即,比线性复杂度更快)来实现搜索。因此,实现者将这些更快的搜索算法实现为成员函数。

具有按关键字搜索功能的容器将集成查找方法(例如,使用可以有效查找的二进制树在内部实现的映射)。

其他的,如您所引用的,将允许使用std::find进行范围搜索,但没有特色的find函数,因为它与std::find相比没有算法优势(排序/特殊情况除外)

std::vectorstd::liststd::forward_list等容器是顺序容器。没有什么比顺序搜索更好的方法可以应用于这些容器了。因此,如果每个顺序容器的顺序搜索对于所有这些容器都是相同的,则无需重写顺序搜索。

std::basic_string类是一个例外,它最初模拟的C字符串已经具有strchr、strstr和其他特殊搜索函数。

对不同的容器使用相同的函数可以使API更加清晰,您不必学习每个容器的特性,只需学习如何将一个函数应用于所有容器。

这也是为了代码的可重用性-您使用的算法从提供迭代器的任何容器中获取迭代器,因此该算法不必依赖于容器是std::vectorstd::list等。

如其他评论中所述,设计原理是vector::find()可以像非成员函数std::find()一样高效地实现。使用后者的好处是,它将数据结构和作用于数据结构的运算符解耦,从而提高了可维护性(这对库的开发人员来说是有利的)。

然而,前者的好处是,它将使所有容器之间的API保持一致,并使客户端代码不那么冗长。这将提高可学习性和可读性(这对图书馆的用户来说是有利的)。此外,一致的API将允许编写通用代码。考虑一下:

template <typename Container, typename T>
void foo(const Container& c, const T& x) {
    if (std::find(c.begin(), c.end(), x) != c.end()) {
        // ...
    }
}

当CCD_ 54是CCD_ 55或CCD_。为了提高效率,我们需要做:

template <typename Container, typename T>
void foo(const Container& c, const T& x) {
    if (c.find(x) != c.end()) {
        // ...
    }
}

但是它不编译std::vectorstd::list。这给库的用户带来了负担,他们需要为他们想要支持的每种类型手动编写专用/重载的通用函数:

template <typename T>
bool contains(const std::vector<T>& c, const T& x) {
    return std::find(c.begin(), c.end(), x) != c.end();
}
template <typename T>
bool contains(const std::set<T>& c, const T& x) {
    return c.find(x) != c.end();
}
template <typename Container, typename T>
void foo(const Container& c, const T& x) {
    if (contains(c, x)) {
        // ...
    }
}

我承认做出这些类型的设计决策很难,但我的观点是STL的设计师在这里犯了一个错误。非常小的可维护性负担似乎在很大程度上值得为用户提供更好的API和一致性。简而言之,由于find必须是某些容器的成员函数(为了性能),因此find应该是所有容器的成员功能(为了一致性)。请注意,我完全同意其他算法是非成员函数。

(我的意思是,拜托,容器从定义上来说是包含东西的东西。对于用户来说,编写一个通用且高效的"包含"函数应该是微不足道的。事实上,我认为应该将其添加到容器概念中,但我离题了。)