从矢量中选择特定元素

Select specific elements from a vector

本文关键字:元素 选择      更新时间:2023-10-16

我有一个向量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;

有没有更好的方法?

  • 在C++03
  • 在C++11之内
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的重新实现(很难直接使用,因为它使用的函数对象被赋予了一个值,而不是容器中的索引或迭代器)。

在 C++11 中,您可以将 std::remove_ifstd::erase 与 lambda 一起使用,即"擦除-删除-习语":

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元素但是,文档中的语言并未明确保证这一点。它只是说:

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

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

基于 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 的内容。

我听说你喜欢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 将索引元素访问者转换为元素访问者。 有点像拉姆达柔道。

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];
  }
);

解决您的问题(现场示例)。

我实际上非常喜欢你这样做的方式,但我会在限制使用临时向量的范围方面进行一些更改,我会使用 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';
}

不同的版本可以就地擦除元素,但不需要像伊戈尔算法那样多的移动,并且在要擦除少量元素的情况下可能更有效:

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());

但是这个算法是不稳定的。

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