从矢量中选择特定元素

时间:2016-07-14 13:02:00

标签: c++ c++11 stdvector

我有一个向量v1和一个相同大小的布尔向量v2。我想从v1中删除所有值,以便v2的并行元素为false

vector<int> v3; // assume v1 is vector<int>
for (size_t i=0; i<v1.size(); i++)
    if (v2[i])
        v3.push_back(v1[i]);
v1=v3;

有更好的方法吗?

  • in C ++ 03
  • in C ++ 11

7 个答案:

答案 0 :(得分:20)

size_t last = 0;
for (size_t i = 0; i < v1.size(); i++) {
  if (v2[i]) {
    v1[last++] = v1[i];
  }
}
v1.erase(v1.begin() + last, v1.end());

与您的相同,除了它就地工作,不需要额外的存储空间。这基本上是std::remove_if的重新实现(难以直接使用,因为它使用的函数对象被赋予一个值,而不是容器中的索引或迭代器)。

答案 1 :(得分:17)

在C ++ 11中,您可以将std::remove_ifstd::erase与lambda一起使用,即"erase-remove-idiom"

size_t idx = 0;
v1.erase(std::remove_if(v1.begin(),
                          v1.end(),
                          [&idx, &v2](int val){return !v2[idx++];}),
           v1.end())

以下是指向它的链接:cpp.sh/57jpc

正如评论所指出的那样,对于这样做的安全性有一些讨论;这里的基本假设是std::remove_if将谓词按顺序应用于v1 的元素。但是,doc中的语言并未明确保证这一点。它只是states

  

通过移动(通过移动分配)范围内的元素来完成移除,使得不被移除的元素出现在范围的开头。保留的元素的相对顺序被保留,容器的物理大小不变。指向新逻辑结束和范围的物理结束之间的元素的迭代器仍然是可解除引用的,但元素本身具有未指定的值(根据MoveAssignable后置条件)。调用remove之后通常会调用容器的erase方法,该方法会擦除未指定的值并减小容器的物理大小以匹配其新的逻辑大小。

现在,只有std::vector的前向迭代器才能保证结果的稳定性并且不按顺序应用谓词。但这肯定是可能的

答案 2 :(得分:7)

基于remove_if的替代方案是:

v1.erase(std::remove_if(v1.begin(), v1.end(),
                        [&v1, &v2](const int &x){ return !v2[&x - &v1[0]]; }),
         v1.end());

另请注意,如果您只需要在v1上跳过某些元素的视图,则可以避免修改v1并使用类似boost::filter_iterator的内容。

答案 3 :(得分:7)

我听说你喜欢lambdas。

auto with_index_into = [](auto&v){
  return [&](auto&& f){
    return [&,f=decltype(f)(f)](auto& e){
      return f( std::addressof(e)-v.data(), e );
    };
  };
};

这可能很有用。它需要一个.data()支持容器,然后返回一个类型为((Index,E&)->X)->(E&->X)的lambda - 返回的lambda将索引元素访问者转换为元素访问者。 lambda柔道。

template<class C, class Test>
auto erase_if( C& c, Test&& test) {
  using std::begin; using std::end;
  auto it=std::remove_if(begin(c),end(c),test);
  if (it==end(c)) return false;
  c.erase(it, end(c));
  return true;
}

因为我讨厌在客户端代码中删除擦除习惯用法。

现在代码非常漂亮:

erase_if( v1, with_index_into(v1)(
  [](std::size_t i, auto&e){
    return !v2[i];
  }
));

对删除/擦除中移动的限制意味着它会调用元素在其原始位置的lambda。

我们可以通过更多元素步骤来实现这一目标。它在中间变得复杂......

首先,微小的命名运算符库:

namespace named_operator {
  template<class D>struct make_operator{};

  enum class lhs_token {
    star = '*',
    non_char_tokens_start = (unsigned char)-1,
    arrow_star,
  };

  template<class T, lhs_token, class O> struct half_apply { T&& lhs; };

  template<class Lhs, class Op>
  half_apply<Lhs, lhs_token::star, Op>
  operator*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }
  template<class Lhs, class Op>
  half_apply<Lhs, lhs_token::arrow_star, Op>
  operator->*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, lhs_token::star, Op>&& lhs, Rhs&& rhs )
  {
    return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, lhs_token::arrow_star, Op>&& lhs, Rhs&& rhs )
  {
    return named_next( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }
}

现在我们定义then

namespace lambda_then {
  struct then_t:named_operator::make_operator<then_t> {} then;

  template<class Lhs, class Rhs>
  auto named_next( Lhs&& lhs, then_t, Rhs&& rhs ) {
    return
      [lhs=std::forward<Lhs>(lhs), rhs=std::forward<Rhs>(rhs)]
      (auto&&...args)->decltype(auto)
    {
      return rhs( lhs( decltype(args)(args)... ) );
    };
  }
}
using lambda_then::then;

定义了一个标记then,使得lambda1 ->*then* lambda2返回一个接受其参数的函数对象,将其传递给lambda1,然后将返回值传递给lambda2。

接下来我们定义to_index(container)

template<class C>
auto index_in( C& c ) {
  return [&](auto& e){
    return std::addressof(e)-c.data();
  };
}

我们还保留上述erase_if

这导致:

erase_if( v1,
  index_in(v1)
  ->*then*
  [&](auto i){
    return !v2[i];
  }
);

solving your problem (live example).

答案 4 :(得分:3)

我实际上非常喜欢你这样做但我会在限制使用临时矢量的范围方面做一些改变,我会使用std::vector::swap来避免副本。如果您有C++11,则可以使用std::move代替std::vector::swap

#include <vector>
#include <iostream>

int main()
{
    std::vector<int> iv = {0, 1, 2, 3, 4, 5, 6};
    std::vector<bool> bv = {true, true, false, true, false, false, true};

    // start a new scope to limit
    // the lifespan of the temporary vector
    {
        std::vector<int> v;

        // reserve space for performance gains
        // if you don't mind an over-allocated return
        // v.reserve(iv); 

        for(std::size_t i = 0; i < iv.size(); ++i)
            if(bv[i])
                v.push_back(iv[i]);

        iv.swap(v); // faster than a copy
    }

    for(auto i: iv)
        std::cout << i << ' ';
    std::cout << '\n';
}

答案 5 :(得分:2)

不同的版本删除了元素,但不需要像Igor的算法那样多的移动,并且在少量元素被擦除的情况下可能更有效:

using std::swap;
size_t last = v1.size();
for (size_t i = 0; i < last;) {
   if( !v2[i] ) {
       --last;
       swap( v2[i], v2[last] );
       swap( v1[i], v1[last] );
   } else 
       ++i;
}
v1.erase(v1.begin() + last, v1.end());

但这个算法不稳定。

答案 6 :(得分:1)

如果使用list(或forward_list代替C ++ 11)而不是vector,则可以就地执行此操作,而无需移动/分配/复制开销用于vector次操作。使用任何STL容器完成大多数与存储相关的事情是完全可能的,但适当选择容器通常会显着提高性能。