Стирание элементов из вектора, если они есть и в другом векторе

Предположим, у меня есть vector a = {"the", "of"} и vector b = {"oranges", "the", "of", "apples"}.

Я хочу сравнить оба вектора и удалить элементы из a, которые также находятся в b. Вот что я придумал:

for (int i = 0; i < a.size(); i++) {
    for (int j =0; j < b.size(); j++) {
       if (a[i] == b[j]) {
          a.erase(a.begin() + i);
       }
    }
}

Но этот цикл не удаляет последний элемент в a. Странный!


person muqsitnawaz    schedule 30.11.2014    source источник
comment
Значение a[i] меняется в середине вашего внутреннего цикла.   -  person Kerrek SB    schedule 01.12.2014
comment
Вы можете отсортировать векторы? Тогда вы можете просто использовать std::set_difference.   -  person Kerrek SB    schedule 01.12.2014
comment
Да. Наверное поэтому не работает. Но как вы могли отсортировать строки? Мне приходится работать со строками.   -  person muqsitnawaz    schedule 01.12.2014
comment
Это разве не наборы? Просмотрите алгоритмы, предоставляемые стандартной библиотекой, и соедините их вместе, чтобы сформировать свое решение. Дайте нам знать, когда у вас возникнут конкретные проблемы с ними.   -  person Lightness Races in Orbit    schedule 01.12.2014


Ответы (5)


Проблема в том, что когда вы удаляете первый элемент a, индекс увеличивается с 0 до 1. На следующей итерации цикла размер вектора равен 1, что соответствует условию внешнего цикла, вызывающего его завершение. Вы можете избежать любых уловок, которые могут понадобиться, чтобы исправить это, просто используя std::remove_if, std::find и лямбду.

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

int main()
{
    std::vector<std::string> a{ "the", "of" };
    std::vector<std::string> b{ "oranges", "the", "of", "apples" };

    auto pred = [&b](const std::string& key) ->bool
    {
        return std::find(b.begin(), b.end(), key) != b.end();
    };

    a.erase(std::remove_if(a.begin(), a.end(), pred), a.end());

    std::cout << a.size() << "\n";
}

Лучшим тестом было бы поменять местами содержимое a и b. Это удалит «the» и «of», оставив вам «апельсины» и «яблоки».

person Captain Obvlious    schedule 30.11.2014
comment
Это несколько хуже; если внутренний цикл уже не был на своей последней итерации, тогда внешний цикл, безусловно, не завершится немедленно, и внезапно вы получаете доступ к элементам, которые, возможно, даже больше не существуют. - person Lightness Races in Orbit; 01.12.2014
comment
Ага, пропустил это. даже не думал о том, что внутренний цикл имеет UB после того, как стертый элемент является последним в контейнере. Я добавлю это после еще пары выстрелов :) - person Captain Obvlious; 01.12.2014

Попробуйте следующее

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cassert>

int main()
{
    std::vector<std::string> a = { "the", "of" };
    std::vector<std::string> b = { "oranges", "the", "of", "apples" };

    for ( auto it = a.begin(); it != a.end(); )
    {
        if ( std::find( b.begin(), b.end(), *it ) != b.end() )
        {
            it = a.erase( it ); 
        }
        else
        {
            ++it;
        }
    }

    assert( a.empty() );
}

Конечно, было бы лучше, если бы векторы были упорядочены.

person Vlad from Moscow    schedule 30.11.2014
comment
И тогда было бы лучше, если бы это были наборы, и тогда можно было бы просто найти разницу с помощью встроенных алгоритмов. Одна строка кода. - person Lightness Races in Orbit; 01.12.2014

В общем, вместо обхода содержимого вектора "вручную" и выборочного удаления его элементов, я бы предложил использовать уже встроенные в STL алгоритмы, комбинируя их должным образом.

Использование идиомы "стереть-удалить"

В частности, чтобы стереть элементы, удовлетворяющие некоторому свойству, из std::vector, вы можете рассмотреть возможность использования идиомы erase-remove.
В этом разделе вопросов и ответов на Stackoverflow обсуждаются некоторые варианты удаления элементов из контейнеров STL, включая случай std::vector.

Вы можете найти компилируемый код с комментариями ниже, здесь:

#include <algorithm>    // for std::remove_if()
#include <iostream>     // for std::cout, std::endl
#include <string>       // for std::string
#include <vector>       // for std::vector
using namespace std;

void print(const char* name, const vector<string>& v);

int main() 
{
    // Input vectors
    vector<string> a = {"the", "of"};
    vector<string> b = {"oranges", "the", "of", "apples"};

    print("a", a);
    print("b", b);

    // Use the erase-remove idiom
    a.erase(
        remove_if(
            a.begin(), 
            a.end(), 

            // This lambda returns true if current string 's'
            // (from vector 'a') is in vector 'b'. 
            [&b](const string& s) 
            {
                auto it = find(b.begin(), b.end(), s);
                return (it != b.end());
            }
        ), 

        a.end()
    );

    cout << "\nAfter removing:\n";
    print("a", a);
}


void print(const char* name, const vector<string>& v) 
{
    cout << name << " = {";
    bool first = true;
    for (const auto& s : v) 
    {
        if (first) 
        {
            first = false;
            cout << s;
        } 
        else 
        {
            cout << ", " << s;
        }
    }
    cout << "}" << endl;
}

Выход:

a = {the, of}
b = {oranges, the, of, apples}

After removing:
a = {}

PS
Обратите также внимание на очень похожий вопрос о Stackoverflow.


Используя std::set_difference()

Альтернативным подходом может быть использование std::set_difference(), например что-то вроде следующего кода: здесь.
(Обратите внимание, что в этом случае, согласно set_difference() предпосылке, входные векторы должны быть уже отсортированы.)

#include <algorithm>    // for std::set_difference(), std::sort()
#include <iostream>     // for std::cout, std::endl
#include <iterator>     // for std::inserter
#include <string>       // for std::string
#include <vector>       // for std::vector
using namespace std;

void print(const char* name, const vector<string>& v);

int main() 
{
    // Input vectors
    vector<string> a = {"the", "of"};
    vector<string> b = {"oranges", "the", "of", "apples"};

    print("a", a);
    print("b", b);

    // Sort the vectors before calling std::set_difference().
    sort(a.begin(), a.end());
    sort(b.begin(), b.end());

    // Resulting difference vector
    vector<string> c;
    set_difference(a.begin(), a.end(),
                   b.begin(), b.end(),
                   inserter(c, c.begin()));

    print("difference(a,b)", c);
}


void print(const char* name, const vector<string>& v) 
{
    cout << name << " = {";
    bool first = true;
    for (const auto& s : v) 
    {
        if (first) 
        {
            first = false;
            cout << s;
        } 
        else 
        {
            cout << ", " << s;
        }
    }
    cout << "}" << endl;
}
person Mr.C64    schedule 30.11.2014

Проблема, с которой вы столкнулись, связана с тем, что вы удаляете элементы из a по мере того, как перебираете его, но не компенсируете это. Это распространенная проблема при попытке написать цикл со стиранием в нем.

Если не имеет значения, в каком порядке находится содержимое ваших векторов, и вы можете сохранить результат в другом векторе, один из лучших подходов — отсортировать оба вектора и вызвать std::set_difference.

#include <algorithm>
#include <iterator>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> a = { "the", "of" };
    std::vector<std::string> b = { "oranges", "the", "of", "apples" };
    std::vector<std::string> res;

    std::sort(a.begin(), a.end());
    std::sort(b.begin(), b.end());

    std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
        std::back_inserter(res));
}

res будет содержать все элементы a, которых не было в b, которые в этом случае будут пустыми.

Если порядок имеет значение или если это должно быть сделано на месте, вы можете использовать идиому стереть-удалить. Ничего не стоит, что это, вероятно, будет медленнее для больших векторов, поскольку это неизбежно алгоритм O (n ^ 2).

#include <algorithm>
#include <iterator>
#include <string>
#include <vector>

struct Pred
{
    const std::vector<std::string>& filter;
    Pred(const std::vector<std::string>& x)
        :filter(x){}

    bool operator()(const std::string& str) const
    {
        return std::find(filter.begin(), filter.end(), str) != filter.end();
    }
};

int main()
{
    std::vector<std::string> a = { "the", "of" };
    std::vector<std::string> b = { "oranges", "the", "of", "apples" };

    Pred pred(b);

    a.erase(std::remove_if(a.begin(), a.end(), pred), a.end());
}

Если у вас нет доступа к компилятору, совместимому с C++11, структура Pred должна стать хорошей заменой лямбда-выражения. В противном случае эта лямбда выполнит эту работу:

auto pred = [&b](const std::string& str)
    {
        return std::find(b.begin(), b.end(), str) != b.end();
    };
person Jared Mulconry    schedule 30.11.2014

это правильный синтаксис стирания вектора формы:

myvector.erase (myvector.begin()+5);

Во-вторых, после того, как вы его стерли, ваш индекс этого вектора будет недействителен.

Поэтому я рекомендую вам провести двухэтапное сканирование. В первом раунде вы отмечаете элементы, которые хотите удалить. ВО втором раунде вы можете стереть их.

Кстати, ваш алгоритм имеет временную сложность O (n ^ 2). Если вы можете, я рекомендую сначала отсортировать вектор. Затем вы можете использовать гораздо более быстрый алгоритм, чтобы справиться с этим.

person BufBills    schedule 30.11.2014
comment
Ну, да. Именно это я и собирался написать здесь. Виноват. Но это просто не работает. - person muqsitnawaz; 01.12.2014
comment
Это правда, что erase делает ваш итератор недействительным, но erase также возвращает новый действительный итератор элементу, который заменил стертый вами. Ответ от Влада из Москвы показывает, как это работает. - person David K; 01.12.2014