根据转换后的值查找最小元素

Posted

技术标签:

【中文标题】根据转换后的值查找最小元素【英文标题】:Finding minimum element based on a transformed value 【发布时间】:2016-01-15 11:24:00 【问题描述】:

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

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;

在这里我找到基于谓词的最小元素。这个谓词计算两个值的 reductionfloat,然后比较这些浮点数。效果很好,看起来很整洁。

你能看出问题吗?是的,对于一组 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 和范围很有趣。

【问题讨论】:

为了简化代码,我首先检查values.empty()。然后,用第一个元素初始化并进入剩余元素的循环。不,这可能不像您当前的代码那么明显,但是如果您优化此代码以获得最大速度(您已经对此进行了分析,对吗?)恕我直言,一些妥协是可以接受的,特别是如果结果代码在 cmets 中得到正确解释。 @UlrichEckhardt 我认为你是对的,唯一合理的做法是定义我自己的算法。其他解决方案看起来完全……复杂。 【参考方案1】:

你可以使用boost::rangelibrary。

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

范围本身也应该随着 C++17 进入标准 C++。

编辑

正如我们在 cmets 中计算的那样,这也会进行两次转换。

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

#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++ 复制实现时,它按预期工作。也许你可以在真机上试一试?实例是here。

小编辑: 我在 VS2015 上进行了测试,它在 std::min_element 上按预期工作。

【讨论】:

*it 将返回减少的值,并且无法获得基础Complex 值。此外,我认为这里也会有 2N 计算,就像我的第一个代码 sn-p 一样。 @Mikhail 您可以通过在返回的迭代器上使用base() 来访问原始值。但是,您可能对重新计算是正确的。嗯。 好的,我看到您的基本想法与@Yakk 相同。但是你有完整的实现。这次真是万分感谢。虽然我认为我不会在实际代码中使用它:)我相信你在make_memoizing_transform_iterator() 函数中忘记了std::forward "另外,它实际上是不正确的语法 - 提升范围不能采用 lambdas..." 什么?他们当然可以。您可能需要更新 Boost。【参考方案2】:

这是一个使用的解决方案(已经与 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

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

【讨论】:

你只需要N-1个比较就可以解决这个问题。 很遗憾,我无法在 VS2015 中用 clang 编译它。 @Mikhail VS2015 将很快收到 Update2,也许它会工作。我没有在 VS2015 下让 clang 工作的经验。【参考方案3】:

唯一缺少的是元迭代器。

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

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

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&lt;1&gt; ) 进行排序。

accumulate 调用可以读取为accumulate( skip_first(range), g(begin(range)), get_least( order_by( get_n_t&lt;1&gt; ) ) )

不确定是否更清楚。

【讨论】:

不确定我会使用它,但使用迭代器存储归约结果的方法看起来很自然。谢谢。 为什么要等待强大的作曲库,ranges-v3 already works,正如您在我的回答中看到的那样:)【参考方案4】:

如果您将 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

因为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”。

【讨论】:

这实际上是我截取的第二个代码,包含在min_element 中。我不认为它比基于范围的 for 更具可读性。 考虑values = std::numeric_limits&lt;float&gt;::max() 有没有功能上的区别? 同意,您的实现将处理这种情况,而我不会。为此 +1。 忽略b会让人困惑。 是的,但我找不到其他 std::algorithm 在运行所有元素时将迭代器返回到特定元素作为单个参数操作的结果。【参考方案5】:

这是另一种选择,但实际上它仍然是您的第二种解决方案。老实说,它看起来并不清晰,但有人可能会喜欢它。 (我使用std::pair&lt;float, Complex&gt;来存储归约结果和被归约的值。)

std::pair<float, Complex> resultstd::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); );

P.S.如果您的calcReduction 成本很高,您是否考虑过在Complex 对象中缓存结果?这将导致实现稍微复杂一些,但您将能够使用普通的std::min_element,这样可以清楚地表达您的意图。

【讨论】:

看起来不错,我喜欢这种方法。实际上,最初实现这段代码的人,也使用了pair,但将它们全部存储在一个向量中,然后应用min_element

以上是关于根据转换后的值查找最小元素的主要内容,如果未能解决你的问题,请参考以下文章

Python-根据范围转换列表中的值

在 c++ 中将值转换为范围内的值,使用 boost 或 std 进行优化

使用转换后的值对 NSTableColumn 进行排序

尝试过滤转换后的日期时间值时超出范围的值

前端用js写一个函数实现KBMBGBTB单位转换

怎样把电压编码后转换为实际电压数值