其他答案都很好。我想补充一点关于minmax_element 必须如何工作的内容,但这也有助于解释为什么它(通常)比单独运行min_element 和max_element 工作得更好,并讨论它的一些具体情况没有表现更好。
如果我们考虑一个简单的实现,您将维护一个最大值和最小值(以及它们相应的迭代器)并简单地遍历范围,将每个值与最小值和最大值进行比较并调整根据需要。但是,这会给您总共 2N 次比较;虽然它可能比遍历列表两次(由于更好地使用局部性)表现得更好,但规范要求(大约)3/2 N 比较。那怎么可能呢?
它通过处理对而不是单个项目来工作。取范围内的前两项(#0 和#1),我们可以比较它们并将最大值分配给最大值,将最小值分配给最小值。然后,我们比较接下来的两项(#3 和#4)来决定哪一项更大;我们将较大的一个与最大值进行比较,将较小的一个与最小值进行比较,并根据需要更新最大值/最小值。然后,我们对每一对额外的对(#5 和 #6,然后是 #7 和 #8,依此类推)重复此过程。
因此,每对都需要进行 3 次比较 - 相互比较,然后是当前最大值的最高值,以及当前最小值的最低值。这将所需的比较次数减少到 3/2 N!
然而,根据下面的 cmets,应该注意的是,当使用比较便宜的类型(或比较器)时,这种“改进的”算法在现代处理器上往往会产生比原始版本更差的性能 - 特别是,范围超过vector<int> 或类似:每对的两个元素之间的比较具有不可预测的结果,导致处理器中的分支预测失败(尽管这仅在数据或多或少随机排序时才成立);当前的编译器并不总是将分支转换为条件传输,因为它们可能会这样做。此外,编译器更难向量化更复杂的算法。
理论上,我认为,C++ 库实现可以为 minmax_element 函数提供重载,该函数使用默认比较器的原始(int 等)元素类型的朴素算法。虽然标准要求对比较次数进行限制,但如果无法观察到这些比较的效果,那么实际计算的数字并不重要,只要时间复杂度相同(在这两种情况下都是 O(N)) .但是,虽然这可能会为随机排序的数据提供更好的性能,但在数据排序时可能会产生更差的性能。
考虑到以上所有因素,一个简单的测试用例(如下)显示了一个有趣的结果:对于随机排序的数据,分别使用min_element 和max_element 实际上可以稍微快一点使用minmax_element。 但是,对于已排序的数据,minmax_element 比单独使用 min_element 和 max_element 快得多。在我的系统(Haswell 处理器)上(使用gcc -O3 -std=c++11 -march=native,GCC 版本 5.4 编译),示例运行分别显示 min/max 为 692 毫秒,minmax 组合为 848 毫秒。当然,运行之间存在一些差异,但这些值似乎很典型。
注意:
- 性能差异很小,不太可能成为实际程序中的主导因素;
- 差异取决于编译器优化;未来,结果很可能会逆转;
- 对于更复杂的数据类型(或者更确切地说是更复杂的比较器),结果可能会相反,因为在这种情况下,较少的比较可能会带来显着的改进。
- 当样本数据是有序的而不是随机的(在下面的程序中将
v.push_back(r(gen)) 替换为 v.push_back(i))时,性能非常不同:对于单独的 min/max,大约为 728 毫秒,而对于组合的 minmax,它会下降到 246 毫秒。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <chrono>
constexpr int numEls = 100000000;
void recresult(std::vector<int> *v, int min, int max)
{
// Make sure the compiler doesn't optimize out the values:
__asm__ volatile (
""
:
: "rm"(v), "rm"(min), "rm"(max)
);
}
int main(int argc, char **argv)
{
using namespace std;
std::mt19937 gen(0);
uniform_int_distribution<> r(0, 100000);
vector<int> v;
for (int i = 0; i < numEls; i++) {
v.push_back(r(gen));
}
// run once for warmup
int min = *min_element(v.begin(), v.end());
int max = *max_element(v.begin(), v.end());
recresult(&v, min, max);
// min/max separately:
{
auto starttime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 5; i++) {
int min = *min_element(v.begin(), v.end());
int max = *max_element(v.begin(), v.end());
recresult(&v, min, max);
}
auto endtime = std::chrono::high_resolution_clock::now();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(endtime - starttime).count();
cout << "min/max element: " << millis << " milliseconds." << endl;
}
// run once for warmup
auto minmaxi = minmax_element(v.begin(), v.end());
recresult(&v, *(minmaxi.first), *(minmaxi.second));
// minmax together:
{
auto starttime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 5; i++) {
minmaxi = minmax_element(v.begin(), v.end());
recresult(&v, *(minmaxi.first), *(minmaxi.second));
}
auto endtime = std::chrono::high_resolution_clock::now();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(endtime - starttime).count();
cout << "minmax element: " << millis << " milliseconds." << endl;
}
return 0;
}