Подтвердить что ты не робот

Выберите определенные элементы из вектора

У меня есть вектор 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;

Есть ли лучший способ сделать это?

  • в С++ 03
  • в С++ 11
4b9b3361

Ответ 1

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 (что было бы трудно использовать напрямую, потому что объект функции, который он использует, получает значение, а не индекс или итератор в контейнер).

Ответ 2

В С++ 11 вы можете использовать std::remove_if и std::erase с помощью лямбды, которая является "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 по порядку. Однако язык в документе явно не гарантирует этого. Это просто состояние:

Удаление выполняется путем смещения (посредством назначения перемещения) элементов в диапазоне таким образом, чтобы элементы, которые не должны быть удалены, появлялись в начале диапазона. Относительный порядок оставшихся элементов сохраняется и физические размеры контейнера не изменяются. Итераторы, указывающие на элемент между новым логическим концом и физическим концом диапазона, все еще разыскиваются, но сами элементы имеют неуказанные значения (в соответствии с пост-условием MoveAssignable). Вызов для удаления обычно сопровождается вызовом метода стирания контейнера, который стирает неуказанные значения и уменьшает физический размер контейнера в соответствии с его новым логическим размером.

Теперь было бы сложно, если бы только прямой итератор с std::vector гарантировал стабильность результатов и не применял предикат в порядке. Но это, безусловно, возможно.

Ответ 3

A 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.

Ответ 4

Я слышал, что ты любишь лямбды.

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() suporting, затем возвращает лямбда типа ((Index,E&)->X)->(E&->X) - возвращенная лямбда преобразует посетителя с индексированным элементом в посетителя элемента. Вид лямбда-дзюдо.

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

Ограничение на перемещение в remove/erase должно означать, что он вызывает лямбду на элементе в исходном положении.


Мы можем сделать это с помощью более элементарных шагов. Он усложняется посередине...

Во-первых, небольшая названная операторская библиотека:

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

решение вашей проблемы (живой пример).

Ответ 5

Мне действительно нравится, как вы это делали, но я бы сделал пару изменений в ограничении области применения временного вектора, и я бы использовал std::vector:: swap, чтобы избежать копирования в конце. Если у вас есть C++11, вы можете использовать std:: move вместо std::vector:: своп:

#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';
}

Ответ 6

Разная версия, которая стирает элементы на месте, но не требует столько ходов, сколько требует Igor algo, и в случае небольшого количества стираемых элементов может быть более эффективной:

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

но этот алгоритм нестабилен.

Ответ 7

Если вы используете list (или forward_list для С++ 11) вместо vector, вы можете сделать это на месте без затрат на перемещение/распределение/копирование, необходимых для операций vector. Совершенно возможно делать большинство связанных с хранилищем вещей с любым контейнером STL, но соответствующий выбор контейнеров часто дает значительные улучшения в производительности.