Как удалить дубликаты из несортированного std::vector при сохранении исходного порядка с использованием алгоритмов?
У меня есть массив целых чисел, из которого мне нужно удалить дубликаты, сохраняя при этом порядок первого вхождения каждого целого числа. Я могу видеть, как это происходит, но представьте себе, что есть лучший способ, который делает использование алгоритмов STL лучше? Вставка находится вне моего контроля, поэтому я не могу проверить наличие дубликатов перед вставкой.
int unsortedRemoveDuplicates(std::vector<int> &numbers) {
std::set<int> uniqueNumbers;
std::vector<int>::iterator allItr = numbers.begin();
std::vector<int>::iterator unique = allItr;
std::vector<int>::iterator endItr = numbers.end();
for (; allItr != endItr; ++allItr) {
const bool isUnique = uniqueNumbers.insert(*allItr).second;
if (isUnique) {
*unique = *allItr;
++unique;
}
}
const int duplicates = endItr - unique;
numbers.erase(unique, endItr);
return duplicates;
}
Как это можно сделать с помощью алгоритмов STL?
9 ответов
Наивный способ заключается в использовании std::set
как вам все говорят. Это излишне и имеет плохую локальность кэша (медленно).
Умный * способ заключается в использовании std::vector
соответственно (не забудьте увидеть сноску внизу):
#include <algorithm>
#include <vector>
struct target_less
{
template<class It>
bool operator()(It const &a, It const &b) const { return *a < *b; }
};
struct target_equal
{
template<class It>
bool operator()(It const &a, It const &b) const { return *a == *b; }
};
template<class It> It uniquify(It begin, It const end)
{
std::vector<It> v;
v.reserve(static_cast<size_t>(std::distance(begin, end)));
for (It i = begin; i != end; ++i)
{ v.push_back(i); }
std::sort(v.begin(), v.end(), target_less());
v.erase(std::unique(v.begin(), v.end(), target_equal()), v.end());
std::sort(v.begin(), v.end());
size_t j = 0;
for (It i = begin; i != end && j != v.size(); ++i)
{
if (i == v[j])
{
using std::iter_swap; iter_swap(i, begin);
++j;
++begin;
}
}
return begin;
}
Тогда вы можете использовать его как:
int main()
{
std::vector<int> v;
v.push_back(6);
v.push_back(5);
v.push_back(5);
v.push_back(8);
v.push_back(5);
v.push_back(8);
v.erase(uniquify(v.begin(), v.end()), v.end());
}
* Примечание: это умный способ в типичных случаях, когда количество дубликатов не слишком велико. Более подробный анализ производительности см. В этом связанном ответе на связанный вопрос.
Звучит как работа для std:: copy_if. Определите предикат, который отслеживает элементы, которые уже были обработаны, и возвращает false, если они есть.
Если у вас нет поддержки C++11, вы можете использовать неуклюже названный std::remove_copy_if и инвертировать логику.
Это непроверенный пример:
template <typename T>
struct NotDuplicate {
bool operator()(const T& element) {
return s_.insert(element).second; // true if s_.insert(element);
}
private:
std::set<T> s_;
};
затем
std::vector<int> uniqueNumbers;
NotDuplicate<int> pred;
std::copy_if(numbers.begin(), numbers.end(),
std::back_inserter(uniqueNumbers),
std::ref(pred));
где std::ref
использовался, чтобы избежать потенциальных проблем с алгоритмом, внутренне копирующим то, что является функтором с состоянием, хотя std::copy_if
не предъявляет никаких требований к побочным эффектам применяемого функтора.
Быстро и просто, C++11:
template<typename T>
size_t RemoveDuplicatesKeepOrder(std::vector<T>& vec)
{
std::set<T> seen;
auto newEnd = std::remove_if(vec.begin(), vec.end(), [&seen](const T& value)
{
if (seen.find(value) != std::end(seen))
return true;
seen.insert(value);
return false;
});
vec.erase(newEnd, vec.end());
return vec.size();
}
int unsortedRemoveDuplicates(std::vector<int>& numbers)
{
std::set<int> seenNums; //log(n) existence check
auto itr = begin(numbers);
while(itr != end(numbers))
{
if(seenNums.find(*itr) != end(seenNums)) //seen? erase it
itr = numbers.erase(itr); //itr now points to next element
else
{
seenNums.insert(*itr);
itr++;
}
}
return seenNums.size();
}
//3 6 3 8 9 5 6 8
//3 6 8 9 5
Чтобы проверить работоспособность предложенных решений, я протестировал три из них, перечисленных ниже. В тестах используются случайные векторы с 1 млн элементов и разным соотношением дубликатов (0%, 1%, 2%, ..., 10%, ..., 90%, 100%).
Mehrdad, в настоящее время принятый ответ:
void uniquifyWithOrder_sort(const vector<int>&, vector<int>& output) { using It = vector<int>::iterator; struct target_less { bool operator()(It const &a, It const &b) const { return *a < *b; } }; struct target_equal { bool operator()(It const &a, It const &b) const { return *a == *b; } }; auto begin = output.begin(); auto const end = output.end(); { vector<It> v; v.reserve(static_cast<size_t>(distance(begin, end))); for (auto i = begin; i != end; ++i) { v.push_back(i); } sort(v.begin(), v.end(), target_less()); v.erase(unique(v.begin(), v.end(), target_equal()), v.end()); sort(v.begin(), v.end()); size_t j = 0; for (auto i = begin; i != end && j != v.size(); ++i) { if (i == v[j]) { using std::iter_swap; iter_swap(i, begin); ++j; ++begin; } } } output.erase(begin, output.end()); }
void uniquifyWithOrder_set_copy_if(const vector<int>& input, vector<int>& output) { struct NotADuplicate { bool operator()(const int& element) { return _s.insert(element).second; } private: set<int> _s; }; vector<int> uniqueNumbers; NotADuplicate pred; output.clear(); output.reserve(input.size()); copy_if( input.begin(), input.end(), back_inserter(output), ref(pred)); }
void uniquifyWithOrder_set_remove_if(const vector<int>& input, vector<int>& output) { set<int> seen; auto newEnd = remove_if(output.begin(), output.end(), [&seen](const int& value) { if (seen.find(value) != end(seen)) return true; seen.insert(value); return false; }); output.erase(newEnd, output.end()); }
Они немного изменены для простоты и позволяют сравнивать решения на месте с решениями на месте. Полный код, использованный для тестирования, доступен здесь.
Результаты показывают, что если вы знаете, у вас будет по крайней мере 1% дубликатов remove_if
решение с std::set
самый лучший. В противном случае вы должны пойти с sort
решение:
// Intel(R) Core(TM) i7-2600 CPU @ 3.40 GHz 3.40 GHz
// 16 GB RAM, Windows 7, 64 bit
//
// cl 19
// /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /WX- /Zc:forScope /Gd /Oi /MD /EHsc /nologo /Ot
//
// 1000 random vectors with 1 000 000 elements each.
// 11 tests: with 0%, 10%, 20%, ..., 90%, 100% duplicates in vectors.
// Ratio: 0
// set_copy_if : Time : 618.162 ms +- 18.7261 ms
// set_remove_if : Time : 650.453 ms +- 10.0107 ms
// sort : Time : 212.366 ms +- 5.27977 ms
// Ratio : 0.1
// set_copy_if : Time : 34.1907 ms +- 1.51335 ms
// set_remove_if : Time : 24.2709 ms +- 0.517165 ms
// sort : Time : 43.735 ms +- 1.44966 ms
// Ratio : 0.2
// set_copy_if : Time : 29.5399 ms +- 1.32403 ms
// set_remove_if : Time : 20.4138 ms +- 0.759438 ms
// sort : Time : 36.4204 ms +- 1.60568 ms
// Ratio : 0.3
// set_copy_if : Time : 32.0227 ms +- 1.25661 ms
// set_remove_if : Time : 22.3386 ms +- 0.950855 ms
// sort : Time : 38.1551 ms +- 1.12852 ms
// Ratio : 0.4
// set_copy_if : Time : 30.2714 ms +- 1.28494 ms
// set_remove_if : Time : 20.8338 ms +- 1.06292 ms
// sort : Time : 35.282 ms +- 2.12884 ms
// Ratio : 0.5
// set_copy_if : Time : 24.3247 ms +- 1.21664 ms
// set_remove_if : Time : 16.1621 ms +- 1.27802 ms
// sort : Time : 27.3166 ms +- 2.12964 ms
// Ratio : 0.6
// set_copy_if : Time : 27.3268 ms +- 1.06058 ms
// set_remove_if : Time : 18.4379 ms +- 1.1438 ms
// sort : Time : 30.6846 ms +- 2.52412 ms
// Ratio : 0.7
// set_copy_if : Time : 30.3871 ms +- 0.887492 ms
// set_remove_if : Time : 20.6315 ms +- 0.899802 ms
// sort : Time : 33.7643 ms +- 2.2336 ms
// Ratio : 0.8
// set_copy_if : Time : 33.3077 ms +- 0.746272 ms
// set_remove_if : Time : 22.9459 ms +- 0.921515 ms
// sort : Time : 37.119 ms +- 2.20924 ms
// Ratio : 0.9
// set_copy_if : Time : 36.0888 ms +- 0.763978 ms
// set_remove_if : Time : 24.7002 ms +- 0.465711 ms
// sort : Time : 40.8233 ms +- 2.59826 ms
// Ratio : 1
// set_copy_if : Time : 21.5609 ms +- 1.48986 ms
// set_remove_if : Time : 14.2934 ms +- 0.535431 ms
// sort : Time : 24.2485 ms +- 0.710269 ms
// Ratio: 0
// set_copy_if : Time: 666.962 ms +- 23.7445 ms
// set_remove_if : Time: 736.088 ms +- 39.8122 ms
// sort : Time: 223.796 ms +- 5.27345 ms
// Ratio: 0.01
// set_copy_if : Time: 60.4075 ms +- 3.4673 ms
// set_remove_if : Time: 43.3095 ms +- 1.31252 ms
// sort : Time: 70.7511 ms +- 2.27826 ms
// Ratio: 0.02
// set_copy_if : Time: 50.2605 ms +- 2.70371 ms
// set_remove_if : Time: 36.2877 ms +- 1.14266 ms
// sort : Time: 62.9786 ms +- 2.69163 ms
// Ratio: 0.03
// set_copy_if : Time: 46.9797 ms +- 2.43009 ms
// set_remove_if : Time: 34.0161 ms +- 0.839472 ms
// sort : Time: 59.5666 ms +- 1.34078 ms
// Ratio: 0.04
// set_copy_if : Time: 44.3423 ms +- 2.271 ms
// set_remove_if : Time: 32.2404 ms +- 1.02162 ms
// sort : Time: 57.0583 ms +- 2.9226 ms
// Ratio: 0.05
// set_copy_if : Time: 41.758 ms +- 2.57589 ms
// set_remove_if : Time: 29.9927 ms +- 0.935529 ms
// sort : Time: 54.1474 ms +- 1.63311 ms
// Ratio: 0.06
// set_copy_if : Time: 40.289 ms +- 1.85715 ms
// set_remove_if : Time: 29.2604 ms +- 0.593869 ms
// sort : Time: 57.5436 ms +- 5.52807 ms
// Ratio: 0.07
// set_copy_if : Time: 40.5035 ms +- 1.80952 ms
// set_remove_if : Time: 29.1187 ms +- 0.63127 ms
// sort : Time: 53.622 ms +- 1.91357 ms
// Ratio: 0.08
// set_copy_if : Time: 38.8139 ms +- 1.9811 ms
// set_remove_if : Time: 27.9989 ms +- 0.600543 ms
// sort : Time: 50.5743 ms +- 1.35296 ms
// Ratio: 0.09
// set_copy_if : Time: 39.0751 ms +- 1.71393 ms
// set_remove_if : Time: 28.2332 ms +- 0.607895 ms
// sort : Time: 51.2829 ms +- 1.21077 ms
// Ratio: 0.1
// set_copy_if : Time: 35.6847 ms +- 1.81495 ms
// set_remove_if : Time: 25.204 ms +- 0.538245 ms
// sort : Time: 46.4127 ms +- 2.66714 ms
Вот что ищет WilliamKF. Он использует оператор стирания. Этот код хорош для списков, но не для векторов. Для векторов не следует использовать оператор стирания.
//makes uniques in one shot without sorting !!
template<class listtype> inline
void uniques(listtype* In)
{
listtype::iterator it = In->begin();
listtype::iterator it2= In->begin();
int tmpsize = In->size();
while(it!=In->end())
{
it2 = it;
it2++;
while((it2)!=In->end())
{
if ((*it)==(*it2))
In->erase(it2++);
else
++it2;
}
it++;
}
}
То, что я пробовал для векторов без использования сортировки:
//makes vectors as fast as possible unique
template<typename T> inline
void vectoruniques(std::vector<T>* In)
{
int tmpsize = In->size();
for (std::vector<T>::iterator it = In->begin();it<In->end()-1;it++)
{
T tmp = *it;
for (std::vector<T>::iterator it2 = it+1;it2<In->end();it2++)
{
if (*it2!=*it)
tmp = *it2;
else
*it2 = tmp;
}
}
std::vector<T>::iterator it = std::unique(In->begin(),In->end());
int newsize = std::distance(In->begin(),it);
In->resize(newsize);
}
Как-то похоже, что это будет работать. Я проверил это немного, может кто-нибудь подскажет, действительно ли это работает! Это решение не требует большего оператора. Я имею в виду, зачем использовать больший оператор для поиска уникальных элементов? Использование для векторов:
int myints[] = {21,10,20,20,20,30,21,31,20,20,2};
std::vector<int> abc(myints , myints+11);
vectoruniques(&abc);
Поскольку ваш вектор содержит целые числа, гораздо более быстрое решение, чем «сортировка и уникальность», состоит в том, чтобы объявить перед этим большой глобальный или статический большой массив, инициализированный значением -1 (t0 в моем примере), и использовать его. Результаты доказывают, что это примерно в 50 раз быстрее, чем «сортировка и уникальность»:
results :
duration sort then unique == 36871
duration unique without sorting == 656
unique without sort is ok
Вот код:
int main(int argc, char*argv []) {
//I am always declaring at least 2 global big vector<int> in my software, to speed up operations like unique
//so I am not counting the creation of these "huge" vector in algo duration.
const int N (5000); //max possible integer of vector tested
const auto zero ((const int) 0), sm1 ((const int) -1);
std::vector<int> t0 (N, sm1);
//init vector to render unique
std::vector<int> vec (1000000, sm1);
std::for_each (vec.begin (), vec.end (), [] (auto& s) {s = rand ()%1000;}); //1000 < N
std::vector<int> vec1 (vec);
clock_t beg (clock ()), end (beg);
{
beg = clock ();
std::sort (vec.begin (), vec.end ());
auto j (std::unique (vec.begin (), vec.end ()));
if (j != vec.end ()) vec.erase (j, vec.end ());
end = clock ();
std::cout << "\tduration sort then unique == " << (end - beg) << std::endl;
}
{
beg = clock ();
auto j (vec1.begin ());
std::for_each (vec1.begin (), vec1.end (), [&j, &t0, &zero] (const auto& s) {
if (t0 [s]) {
*j++ = s;
t0 [s] = zero;
}
});
if (j != vec1.end ()) vec1.erase (j, vec1.end ());
//clean t0
std::for_each (vec1.begin (), vec1.end (), [&t0, &sm1] (const auto&s) {t0 [s] = sm1;});
end = clock ();
//sorting is just here to compare with sort+unique, but not counting in duration
std::sort (vec1.begin (), vec1.end ());
std::cout << "\tduration unique without sorting == " << (end - beg) << std::endl;
}
if (vec == vec1) std::cout << "\tunique without sort is ok" << std::endl;
else std::cout << "\tunique without sort is nok" << std::endl;
}
Вот кое-что, что обрабатывает POD и не POD типы с поддержкой перемещения. Используется оператор по умолчанию == или пользовательский предикат равенства. Не требует сортировки / оператора<, генерации ключей или отдельного набора. Не знаю, эффективнее ли это, чем другие методы, описанные выше.
template <typename Cnt, typename _Pr = std::equal_to<typename Cnt::value_type>>
void remove_duplicates( Cnt& cnt, _Pr cmp = _Pr() )
{
Cnt result;
result.reserve( std::size( cnt ) ); // or cnt.size() if compiler doesn't support std::size()
std::copy_if(
std::make_move_iterator( std::begin( cnt ) )
, std::make_move_iterator( std::end( cnt ) )
, std::back_inserter( result )
, [&]( const typename Cnt::value_type& what )
{
return std::find_if(
std::begin( result )
, std::end( result )
, [&]( const typename Cnt::value_type& existing ) { return cmp( what, existing ); }
) == std::end( result );
}
); // copy_if
cnt = std::move( result ); // place result in cnt param
} // remove_duplicates
Использование / тесты:
{
std::vector<int> ints{ 0,1,1,2,3,4 };
remove_duplicates( ints );
assert( ints.size() == 5 );
}
{
struct data
{
std::string foo;
bool operator==( const data& rhs ) const { return this->foo == rhs.foo; }
};
std::vector<data>
mydata{ { "hello" }, {"hello"}, {"world"} }
, mydata2 = mydata
;
// use operator==
remove_duplicates( mydata );
assert( mydata.size() == 2 );
// use custom predicate
remove_duplicates( mydata2, []( const data& left, const data& right ) { return left.foo == right.foo; } );
assert( mydata2.size() == 2 );
}
Вот общая версия C++11, которая работает с итераторами и не выделяет дополнительное хранилище. Недостатком может быть O(n^2), но, скорее всего, он быстрее для меньших входных размеров.
template<typename Iter>
Iter removeDuplicates(Iter begin,Iter end)
{
auto it = begin;
while(it != end)
{
auto next = std::next(it);
if(next == end)
{
break;
}
end = std::remove(next,end,*it);
it = next;
}
return end;
}
....
std::erase(removeDuplicates(vec.begin(),vec.end()),vec.end());
Пример кода: http://cpp.sh/5kg5n