什么是删除重复项和排序向量的最有效方法?

372

我需要对一个可能有很多元素的C++向量进行去重并排序。

我目前有下面的代码,但它不起作用。

vec.erase(
      std::unique(vec.begin(), vec.end()),
      vec.end());
std::sort(vec.begin(), vec.end());

如何正确地执行此操作?

另外,先删除重复项(类似上面的代码)还是先进行排序更快?如果我先进行排序,执行std::unique后是否保证仍然排序?

或者还有其他(可能更有效的)方法来完成所有这些操作吗?


4
我假设您没有在插入之前进行检查以避免首先出现重复项的选项? - Joe
没错,那将是理想的。 - Kyle Ryan
44
建议对上面的代码进行纠正,或者明确指出它是错误的。std::unique假定范围已经排序。 - Matthieu M.
2
使用集合代替 - Ivan
你必须先使用sort,然后再使用erase+unique。 - user1438233
26个回答

0

大多数答案似乎使用O(nlogn),但是通过使用unordered_set,我们可以将其降低到O(n)。我看到一些解决方案使用sets,但我发现这个更优雅,可以使用setiterators

using Intvec = std::vector<int>;

void remove(Intvec &v) {
    // creating iterator starting with beginning of the vector 
    Intvec::iterator itr = v.begin();
    std::unordered_set<int> s;
    // loops from the beginning to the end of the list 
    for (auto curr = v.begin(); curr != v.end(); ++curr) {
        if (s.insert(*curr).second) { // if the 0 curr already exist in the set
            *itr++ = *curr; // adding a position to the iterator 
        }
    }
    // erasing repeating positions in the set 
    v.erase(itr, v.end());
}

0
std::set<int> s;
std::for_each(v.cbegin(), v.cend(), [&s](int val){s.insert(val);});
v.clear();
std::copy(s.cbegin(), s.cend(), v.cbegin());

1
也许在清空向量后调整其大小,以便在构建向量时仅进行1次内存分配。也许更喜欢使用std::move而不是std::copy将ints移入向量中,而不是复制它们,因为之后不需要set。 - YoungJohn

0
如果您的类可以轻松转换为int,并且您有一些内存,那么在排序之前可以使用unique,这样速度会更快:
#include <vector>
#include <stdlib.h>
#include <algorithm>
int main (int argc, char* argv []) {
  //vector init
  std::vector<int> v (1000000, 0);
  std::for_each (v.begin (), v.end (), [] (int& s) {s = rand () %1000;});
  std::vector<int> v1 (v);
  int beg (0), end (0), duration (0);
  beg = clock ();
  {
    std::sort (v.begin (), v.end ());
    auto i (v.begin ());
    i = std::unique (v.begin (), v.end ());
    if (i != v.end ()) v.erase (i, v.end ());
  }
  end = clock ();
  duration = (int) (end - beg);
  std::cout << "\tduration sort + unique == " << duration << std::endl;

  int n (0);
  duration = 0;
  beg = clock ();
  std::for_each (v1.begin (), v1.end (), [&n] (const int& s) {if (s >= n) n = s+1;});
  std::vector<int> tab (n, 0);
  {
    auto i (v1.begin ());
    std::for_each (v1.begin (), v1.end (), [&i, &tab] (const int& s) {
      if (!tab [s]) {
        *i++ = s;
        ++tab [s];
      }
    });
    std::sort (v1.begin (), i);
    v1.erase (i, v1.end ());
  }
  end = clock ();
  duration = (int) (end - beg);
  std::cout << "\tduration unique + sort == " << duration << std::endl;
  if (v == v1) {
    std::cout << "and results are same" << std::endl;
  }
  else {
    std::cout << "but result differs" << std::endl;
  }  
}

典型结果: 持续时间排序 + 唯一 == 38985 持续时间唯一 + 排序 == 2500 结果相同


0

这就是在std中命名的问题... std::unique 应该被称为 std::trim_consecutive_duplicates,这样就清楚了你需要先对向量进行排序,以使具有相同值的元素相邻。在这种情况下,我怀疑与集合相关的任何东西都不会比从向量到达更快,但如果您有机会从一开始就将所有内容放入集合中,那么您绝对应该这样做。

这是一个现代的C++20示例(演示):
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <cstdint>

namespace rng = std::ranges;

int main() {
    std::vector<uint32_t> myvec = { 255, 1,3, 16, 5,6, 1, 3, 3, 255, 300 };

    rng::sort(myvec);
    const auto [first, last] = rng::unique(myvec);
    myvec.erase(first, last);

    // Print resulting vector
    std::cout << "my unique vector = {";
    rng::for_each(myvec, [](uint32_t val){ std::cout << val << ", "; });
    std::cout << "}" << std::endl;
}

输出:

my unique vector = {1, 3, 5, 6, 16, 255, 300, }

-1
这是使用std::unique()时出现的重复删除问题的示例。在LINUX机器上,程序会崩溃。请阅读注释以了解详细信息。
// Main10.cpp
//
// Illustration of duplicate delete and memory leak in a vector<int*> after calling std::unique.
// On a LINUX machine, it crashes the progam because of the duplicate delete.
//
// INPUT : {1, 2, 2, 3}
// OUTPUT: {1, 2, 3, 3}
//
// The two 3's are actually pointers to the same 3 integer in the HEAP, which is BAD
// because if you delete both int* pointers, you are deleting the same memory
// location twice.
//
//
// Never mind the fact that we ignore the "dupPosition" returned by std::unique(),
// but in any sensible program that "cleans up after istelf" you want to call deletex
// on all int* poitners to avoid memory leaks.
//
//
// NOW IF you replace std::unique() with ptgi::unique(), all of the the problems disappear.
// Why? Because ptgi:unique merely reshuffles the data:
// OUTPUT: {1, 2, 3, 2}
// The ptgi:unique has swapped the last two elements, so all of the original elements in
// the INPUT are STILL in the OUTPUT.
//
// 130215   dbednar@ptgi.com
//============================================================================

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

#include "ptgi_unique.hpp"

// functor used by std::unique to remove adjacent elts from vector<int*>
struct EqualToVectorOfIntegerStar: public std::equal_to<int *>
{
    bool operator() (const int* arg1, const int* arg2) const
    {
        return (*arg1 == *arg2);
    }
};

void printVector( const std::string& msg, const std::vector<int*>& vnums);

int main()
{
    int inums [] = { 1, 2, 2, 3 };
    std::vector<int*> vnums;

    // convert C array into vector of pointers to integers
    for (size_t inx = 0; inx < 4; ++ inx)
        vnums.push_back( new int(inums[inx]) );

    printVector("BEFORE UNIQ", vnums);

    // INPUT : 1, 2A, 2B, 3
    std::unique( vnums.begin(), vnums.end(), EqualToVectorOfIntegerStar() );
    // OUTPUT: 1, 2A, 3, 3 }
    printVector("AFTER  UNIQ", vnums);

    // now we delete 3 twice, and we have a memory leak because 2B is not deleted.
    for (size_t inx = 0; inx < vnums.size(); ++inx)
    {
        delete(vnums[inx]);
    }
}

// print a line of the form "msg: 1,2,3,..,5,6,7\n", where 1..7 are the numbers in vnums vector
// PS: you may pass "hello world" (const char *) because of implicit (automatic) conversion
// from "const char *" to std::string conversion.

void printVector( const std::string& msg, const std::vector<int*>& vnums)
{
    std::cout << msg << ": ";

    for (size_t inx = 0; inx < vnums.size(); ++inx)
    {
        // insert comma separator before current elt, but ONLY after first elt
        if (inx > 0)
            std::cout << ",";
        std::cout << *vnums[inx];

    }
    std::cout << "\n";
}

PS:我也运行了“valgrind ./Main10”,而且valgrind没有发现任何问题。我强烈建议所有使用LINUX的C++程序员使用这个非常高效的工具,特别是如果你正在编写必须24x7运行且永远不会泄漏或崩溃的实时应用程序! - joe
std::unique的核心问题可以概括为:“std::unique返回未指定状态的重复项”!为什么标准委员会要这样做,我永远不会知道。委员会成员有什么评论吗? - joe
2
是的,“std::unique返回未指定状态的重复项”。因此,不要依赖已经“去重”的数组手动管理内存!最简单的方法是使用std::unique_ptr而不是裸指针。 - alexk7
这似乎是对不同答案的回应;它并没有回答问题(其中“vector”包含整数而不是指针,并且没有指定比较器)。 - Toby Speight

-3
void EraseVectorRepeats(vector <int> & v){ 
TOP:for(int y=0; y<v.size();++y){
        for(int z=0; z<v.size();++z){
            if(y==z){ //This if statement makes sure the number that it is on is not erased-just skipped-in order to keep only one copy of a repeated number
                continue;}
            if(v[y]==v[z]){
                v.erase(v.begin()+z); //whenever a number is erased the function goes back to start of the first loop because the size of the vector changes
            goto TOP;}}}}

这是我创建的一个函数,您可以使用它来删除重复项。所需的头文件只有<iostream><vector>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接