将minmax_element与min_element和max_element一起使用是否有效率优势?

时间:2016-10-27 11:38:00

标签: c++ max min

std::minmax_element:返回一对由最小元素的迭代器组成的第一个元素和最大元素的迭代器作为第二个元素。

std::min_element:将迭代器返回到[first,last]范围内的最小元素。

std::max_element:将迭代器返回到[first,last]范围内的最大元素。

std::minmax_element是否使用完整列表的排序来实现此目的?

处理来自std::minmax_element返回对的开销是否足够?

4 个答案:

答案 0 :(得分:13)

您不必担心/home/danil/projects/python/pdfminer-source/env/local/lib/python2.7/site-packages/pdfminer/pdffont.py进行任何排序。它以精确的方式离开范围。它更有效的原因是它可以在单个通道中找到最大值和最小值,而当分别查找最大值和最小值时,您必须执行两次完整遍历。

std::minmax_element的复杂度为std::minmax_element,其中max(floor(3/2(N−1)), 0)std::max_element均为std::min_element,因此使用{{1}的操作减少约25% }

max(N-1,0)找到最后一个最大元素而std::minmax_element找到第一个最大元素时,也存在差异。

因此,如果您需要找到范围的最小值和最大值,那么您应该使用std::minmax_element。如果您只需要最小值或最大值,则应使用专用版本。使用即将推出的C ++ 17标准和结构化绑定,处理std::max_element的返回将变得更加容易。你可以写

std::minmax_element

现在该对的第一个元素存储在std::minmax_element中,第二个元素存储在auto [min, max] = std::minmax_element(...); 中。

答案 1 :(得分:6)

其他答案都很好。我想补充一点关于minmax_element必然如何运作的一些内容,但这也有助于解释为什么它(通常)比分别运行min_elementmax_element更有效,并谈谈一些具体的问题没有表现更好的情况。

如果我们考虑一个简单的实现,你将保持最大值和最小值(及其相应的迭代器)并简单地遍历该范围,将每个值与最小值和最大值进行比较并调整或者必要时。但是,这将为您提供总共2N的比较;虽然它可能比在列表中迭代两次(由于更好地使用局部性)更好地执行,但规范要求(大致)3/2 N比较。那怎么可能呢?

它通过处理对而不是单个项来工作。取范围(#0和#1)中的前两项,我们可以比较它们并将最大值分配给最大值,将最小值分配给最小值。然后,我们比较接下来的两个项目(#3和#4)来决定哪个项目更大;我们将较大的值与最大值进行比较,将较小的值与最小值进行比较,并根据需要更新max-value / min-value。然后我们用另外一对(#5和#6,然后是#7和#8等)重复这个过程。

因此,每对需要进行三次比较 - 相互之间,然后是当前最大值的最高值,最低值是当前最小值。这减少了3/2 N所需的比较次数

然而,根据下面的评论,应该注意到这个&#34;改进&#34;当使用比较便宜的类型(或比较器)时,算法倾向于在现代处理器上产生比天真版本更差的性能 - 特别是,范围超过vector<int>或类似:每个元素的两个元素之间的比较对具有不可预测的结果,导致处理器中的分支预测失败(尽管只有在数据或多或少随机排序时才会出现这种情况);当前的编译器并不总是将分支转换为条件转移,因为它们可能。此外,编译器更难以矢量化更复杂的算法。

理论上,我认为,C ++库实现可以为minmax_element函数提供重载,该函数使用原始(int等)元素类型的朴素算法和默认比较器。虽然标准强制要求比较次数,但如果无法观察到这些比较的影响,那么实际计算的数量并不重要,只要时间复杂度相同(两者都是O(N)。例)。但是,虽然这可能会提供随机排序数据的更好性能,但在订购数据时可能会产生更差的性能。

考虑到以上所有因素,一个简单的测试用例(如下所示)显示了一个有趣的结果:对于随机排序的数据,分别使用min_elementmax_element实际上可以稍快一点< / strong>比使用minmax_element但是,对于排序数据,minmax_element比单独使用min_elementmax_element要快得多。在我的系统(Haswell处理器)下面(用gcc -O3 -std=c++11 -march=native编译,GCC版本5.4),样本运行分别显示692毫秒的最小值/最大值和848点的最小值组合。当然,运行之间存在一些差异,但这些值似乎很典型。

请注意:

  • 性能差异很小,不太可能成为现实世界计划的主导因素;
  • 差异取决于编译器优化;在未来,结果可能会逆转;
  • 对于更复杂的数据类型(或者更复杂的比较器),结果可能会逆转,因为在这种情况下,较少的比较可能会有显着的改善。
  • 当订购样本数据而不是随机时(在下面的程序中将v.push_back(r(gen))替换为v.push_back(i)),性能非常不同:对于单独的最小/最大,它& #39;大约728毫秒,而对于组合的最小值,它下降到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;
}

答案 2 :(得分:5)

是。你只需要在范围内迭代一次而不是两次。

答案 3 :(得分:3)

std::minmax_element复杂性:

  

谓词的最多max(floor(3/2(N-1)),0)应用程序,其中N = std :: distance(first,last)。

std::min_element复杂度(与max_element相同):

  

恰好是最大(N-1,0)比较,其中N = std :: distance(first,last)。

忽略maxfloor,我们得到:

(N-1) * 2 vs 3/2 (N-1)

因此,使用minmax_element,您可以使用3/4 + max_element或更好的方式获得所需比较min_element

minmax_element使用<运算符的传递性,它知道如果它正在更新最小值,则不需要通过比较两个元素来比较最大值一次,即如果a < b,那么我们只需要检查min(a, current_min)max(b, current_max),反之亦然。

另外值得注意的是:

  

此算法与std :: make_pair(std :: min_element(),std :: max_element())不同,不仅在效率方面,而且在std :: max_element找到时,此算法找到最后一个最大元素第一大元素。