16

这是来自代码审查的任务。我想根据一种特殊的比较谓词从一组中选择一个最小值。像这样:

struct Complex { ... };

float calcReduction(Complex elem);

Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
  auto it = std::min_element(values.begin(), values.end(), 
                             [](const Complex& a, const Complex& b) { 
                               return calcReduction(a) < calcReduction(b); 
                             });

  if (it == values.end()) throw std::runtime_error("");

  return *it;
}

在这里,我找到了基于谓词的最小元素。这个谓词计算两个值的减少float,然后比较这些浮点数。工作正常,看起来很整洁。

你能看出问题吗?是的,一组N元素calcReduction()被称为2N时间,而只计算N一次就足够了——每个元素一次。

解决此问题的一种方法是编写显式计算:

Complex findMinValueExplicit(const std::vector<Complex>& values)
{
  float minReduction = std::numeric_limits<float>::max();
  Complex minValue;

  for (Complex value : values)
  {
    float reduction = calcReduction(value);
    if (reduction < minReduction)
    {
      minReduction = reduction;
      minValue = value;
    }
  }

  if (minReduction == std::numeric_limits<float>::max()) throw std::runtime_error("");

  return minValue;
}

它工作正常,我们只有N调用calcReduction(). 但是,与显式调用min_element. 因为当你打电话min_element时很容易猜到你会找到一个最小元素,你知道的。

我现在唯一的想法是创建自己的算法min_element_with_reduction,例如接受范围和归约函数。听起来很合理,但我想知道是否有现成的解决方案。

关于如何以明确的意图和一些现成的解决方案解决此任务的任何想法?欢迎提升。C++17 和范围很有趣。

4

5 回答 5

7

你可以使用boost::range library

auto reductionLambda = [](const Complex& a) { return calcReduction(a); };
auto it = boost::range::min_element(values | boost::adaptors::transformed( 
                             std::ref(reductionLambda));

范围本身也应该使用 C++17 进入标准 C++。

编辑

正如我们在评论中发现的那样,这也会进行两次转换。

所以这里有一些有趣的东西:

#include <boost/iterator/iterator_adaptor.hpp>
#include <boost/assign.hpp>
#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>


template <class Iterator, class UnaryFunction>
class memoizing_transform_iterator
  : public boost::iterator_adaptor<
        memoizing_transform_iterator<Iterator, UnaryFunction> // Derived
      , Iterator                                              // Base
      , std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))> // Value
      , boost::forward_traversal_tag    // CategoryOrTraversal
    >
{
 public:
    memoizing_transform_iterator() {}

    explicit memoizing_transform_iterator(Iterator iter, UnaryFunction f)
      : memoizing_transform_iterator::iterator_adaptor_(iter), fun(f) {}

    static int total;
 private:
    friend class boost::iterator_core_access;
    void increment() { ++this->base_reference(); memoized = false; }

    using MemoType = std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))>;      

    MemoType& dereference() const 
    {
        if (!memoized) {
            ++total;
            memoized = true;
            memo = fun(*this->base());
        }
        return memo;
    }

    UnaryFunction fun;
    mutable bool memoized = false;
    mutable MemoType memo;
};


template <class Iterator, class UnaryFunction>
auto make_memoizing_transform_iterator(Iterator i, UnaryFunction&& f)
{
    return memoizing_transform_iterator<Iterator, UnaryFunction>(i, f);
}



template<class I, class U>
int memoizing_transform_iterator<I, U>::total = 0;


// THIS IS COPIED FROM LIBSTDC++
template<typename _ForwardIterator>
   _ForwardIterator
     min_el(_ForwardIterator __first, _ForwardIterator __last)
     {
       if (__first == __last)
     return __first;
       _ForwardIterator __result = __first;
       while (++__first != __last)
     if (*__first < *__result)
       __result = __first;
       return __result;
     }


int main(int argc, const char* argv[])
{
    using namespace boost::assign;

    std::vector<int> input;
    input += 2,3,4,1,5,6,7,8,9,10;


    auto transformLambda = [](const int& a) { return a*2; };


    auto begin_it = make_memoizing_transform_iterator(input.begin(), std::ref(transformLambda));
    auto end_it = make_memoizing_transform_iterator(input.end(), std::ref(transformLambda));
    std::cout << *min_el(begin_it, end_it).base() << "\n";

    std::cout <<begin_it.total;

    return 0;
}

基本上我实现了一个迭代器,它可以记住调用转换函子的结果。奇怪的是,至少在在线编译器中,迭代器在比较它们的取消引用值之前被复制(从而破坏了记忆的目的)。但是,当我简单地从 libstdc++ 复制实现时,它按预期工作。也许你可以在真机上试一试?活生生的例子在这里

小编辑: 我在 VS2015 上进行了测试,它与std::min_element.

于 2015-10-16T16:22:12.140 回答
4

这是一个使用的解决方案(已经与range-v3 library一起使用,由即将到来的 Ranges TS 的作者实现)

#include <range/v3/all.hpp>
#include <iostream>
#include <limits>

using namespace ranges::v3;

int main()
{
    auto const expensive = [](auto x) { static int n; std::cout << n++ << " "; return x; };
    auto const v = view::closed_iota(1,10) | view::transform(expensive); 

    auto const m1 = *min_element(v);
    std::cout << "\n" << m1 << "\n";

    auto const inf = std::numeric_limits<int>::max();    
    auto const min = [](auto x, auto y) { return std::min(x, y); };

    auto const m2 = accumulate(v, inf, min);
    std::cout << "\n" << m2 << "\n";    
}

Live On Coliru输出:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 
1
19 20 21 22 23 24 25 26 27 28 
1

如您所见, usingmin_element进行2N比较,但accumulate仅使用N.

于 2016-01-02T20:18:45.753 回答
3

唯一缺少的是元迭代器。

元迭代器接受一个迭代器,并创建一个包含它的副本的迭代器。它将所有操作传递给包含的迭代器,除非取消引用时返回包含的迭代器的副本。

请注意,用于此的代码也可以在 size_t 或 int 或类似的 torsor 上创建迭代

template<class It, class R>
struct reduced_t {
  It it;
  R r;
  friend bool operator<( reduced_t const& lhs, reduced_t const& rhs ) {
    return lhs.r < rhs.r;
  }
};
template<class It, class F>
reduced_t<It, std::result_of_t<F(typename std::iterator_traits<It>::reference)>>
reducer( It it, F&& f ) {
  return {it, std::forward<F>(f)(*it)};
}

template<class It, class F>
It reduce( It begin, It end, F&& f ) {
  if (begin==end)
    return begin;

  return std::accumulate(
    meta_iterator(std::next(begin)), meta_iterator(end),
    reducer(begin, f),
    [&](
      auto&& reduced, // reduced_t<blah...> in C++11
      It i
    ) {
      auto r2 = reducer( i, f );
      return (std::min)(reduced, r2);
    }
  ).it;
};

f(*it)每个迭代器只调用一次。

我不会称之为...明显。诀窍是我们使用accumulate和 元迭代器来实现min_element,然后我们可以accumulate对转换后的元素进行操作(它被调用一次,然后返回)。

您可以使用原语以基于堆栈的编程风格重写它,但是有很多原语要编写。也许发布范围-v3。


在这一点上,我想象有一些高性能的组合编程库。如果我这样做了,我们可以执行以下操作:

reducer( X, f )可以graph( deref |then| f )(X)使用order_by( get_n_t<1> )for ordering 重写。

呼叫accumulate可以读取。accumulate( skip_first(range), g(begin(range)), get_least( order_by( get_n_t<1> ) ) )

不确定这是否更清楚。

于 2015-10-16T18:37:25.177 回答
2

如果您将 minElem 作为 lambda 参数,则可以使用 min_element

Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
  float minElem = std::numeric_limits<float>::max();
  auto it = std::min_element(values.begin(), values.end(),
                             [&minElem](const Complex& a, const Complex& b) {
                               float tmp = calcReduction(a);
                               if (tmp < minElem) {
                                  minElem = tmp;
                                  return true;
                               }
                               return false;
                             });

  if (it == values.end()) throw std::runtime_error("");

  return *it;
}

编辑:为什么不b使用时会起作用?25.4.7.21 min_element

21 返回: [first,last) 范围内的第一个迭代器 i,使得对于 [first,last) 范围内的每个迭代器 j,以下相应条件成立:!(*j < *i) 或 comp(*j, * i) == 错误。如果第一个 == 最后一个,则返回最后一个。

因为b应该被命名smallestYet(来自cplusplus.com的代码)

template <class ForwardIterator>
  ForwardIterator min_element ( ForwardIterator first, ForwardIterator last )
{
  if (first==last) return last;
  ForwardIterator smallest = first;

  while (++first!=last)
    if (*first<*smallest)    // or: if (comp(*first,*smallest)) for version (2)
      smallest=first;
  return smallest;
}

这让我想到了一个新的最喜欢的报价:

计算机科学中只有 10 个难题:缓存失效、命名事物和非一个错误。”

  • 一位评论说,由于我们不使用b.
  • 我担心minElem缓存可能不正确。
  • 我意识到这个名字b应该更有意义,因为它是“对最小元素的解引用迭代器”或smallestYet.
  • 最后,并不是所有人都理解二进制,因为它的末尾没有写“b”。
于 2015-10-16T16:53:59.180 回答
2

这是另一种选择,但它仍然是您的第二个解决方案。老实说,它看起来并不清晰,但有人可能会喜欢它。(我std::pair<float, Complex>用来存储减少结果和减少的值。)

std::pair<float, Complex> result{std::numeric_limits<float>::max(), {}};
auto output_function = [&result](std::pair<float, Complex> candidate) {
    if (candidate.first < result.first)
        result = candidate;
};
std::transform(values.begin(), values.end(), 
               boost::make_function_output_iterator(output_function),
               [](Complex x) { return std::make_pair(calcReduction(x), x); });

PS如果您的calcReduction成本很高,您是否考虑过在Complex对象中缓存结果?这将导致实现稍微复杂一些,但您将能够使用 plainstd::min_element来明确您的意图。

于 2015-10-18T01:31:44.103 回答