在集合中查找重复元素并将它们分组的快速算法是什么?

时间:2009-08-26 05:33:52

标签: c++ algorithm elements duplicate-data

11 个答案:

答案 0 :(得分:13)

是的,你可以做得更好。

  1. 对它们进行排序(对于简单整数为O(n),一般为O(n * log n)),然后重复保证相邻,使得快速找到它们O(n)

  2. 使用哈希表,也是O(n)。对于每个项目,(a)检查它是否已经在哈希表中;如果是的话,它是重复的;如果没有,请将其放在哈希表中。

  3. 修改

    您正在使用的方法似乎进行了O(N ^ 2)比较:

    for i = 0; i < length; ++i           // will do length times
        for j = i+1; j < length; ++j     // will do length-i times
            compare
    

    因此,对于长度5,你做4 + 3 + 2 + 1 = 10比较;对于6你做15,等等(N ^ 2)/ 2 - N / 2是准确的。对于任何合理的高N值,N * log(N)都较小。

    你的案件中N有多大?

    就减少散列冲突而言,最好的方法是获得更好的散列函数:-D。假设这是不可能的,如果你可以做一个变体(例如,不同的modulous),你可以做一个嵌套的哈希。

答案 1 :(得分:7)

<强> 1。在最坏的情况下对数组O(n log n)进行排序 - mergesort / heapsort / binary tree sort

<强> 2。比较邻居并将匹配拉出O(n)

答案 2 :(得分:5)

保持基于散列表的结构从值到计数;如果你的C ++实现没有提供std::hash_map(到目前为止还不是C ++标准的一部分! - ),请使用Boost或从网上获取一个版本。对集合进行一次传递(即O(N))可以进行值 - >计数映射;再一次通过哈希表(&lt; = O(N),清楚地)以识别具有计数&gt;的值。 1并适当地发射它们。总体O(N),这不是你的建议。

答案 3 :(得分:3)

您可以使用从代表元素到其他元素的列表/向量/双端队列的映射。这需要在插入容器时进行相对较少的比较,这意味着您可以在不进行任何比较的情况下迭代生成的组。

此示例始终将第一个代表性元素插入到映射的双端队列存储中,因为它使整个组的后续迭代在逻辑上变得简单,但如果此重复证明存在问题,那么仅执行push_back将很容易if (!ins_pair.second)

typedef std::map<Type, std::deque<Type> > Storage;

void Insert(Storage& s, const Type& t)
{
    std::pair<Storage::iterator, bool> ins_pair( s.insert(std::make_pair(t, std::deque<Type>())) );
    ins_pair.first->second.push_back(t);
}

然后(相对)简单且便宜地迭代这些组:

void Iterate(const Storage& s)
{
    for (Storage::const_iterator i = s.begin(); i != s.end(); ++i)
    {
        for (std::deque<Type>::const_iterator j = i->second.begin(); j != i->second.end(); ++j)
        {
            // do something with *j
        }
    }
}

我进行了一些比较和对象计数的实验。在以10000个对象随机顺序形成50000组(即每组平均2个对象)的测试中,上述方法花费了以下数量的比较和副本:

1630674 comparisons, 443290 copies

(我尝试降低副本数量,但只是以牺牲比较为代价,这似乎是您方案中的高成本操作。)

使用多图,并在最后一次迭代中保留前一个元素以检测组转换成本:

1756208 comparisons, 100000 copies

使用单个列表并弹出前面元素并对其他组成员执行线性搜索:

1885879088 comparisons, 100000 copies

是的,那是~1.9b的比较,相比之下,我最好的方法是~1.6m。为了使列表方法能够在最佳数量的比较附近执行,它必须进行排序,这将花费相似数量的比较,因为首先构建一个固有排序的容器。

修改

我使用你发布的代码并运行隐含的算法(我必须对代码做出一些假设的假设定义)和我之前使用的相同测试数据集,并且我计算了:

1885879088 comparisons, 420939 copies

即。与我的哑列表算法完全相同的比较数,但有更多的副本。我认为这意味着我们在这种情况下使用基本相似的算法。我看不到任何替代排序顺序的证据,但看起来你想要一个包含多个等效元素的组列表。这可以通过添加Iterate子句在我的if (i->size > 1)函数中简单地实现。

我仍然看不到任何证据表明构建一个排序容器(如此deques地图)不是一个好的(即使不是最佳的)策略。

答案 4 :(得分:1)

你尝试过排序吗?例如使用快速排序等算法?如果性能足够好,那么这将是一个简单的方法。

答案 5 :(得分:1)

如果已知它是整数列表,并且如果已知它们都在A和B之间(例如A = 0,B = 9),则创建一个BA元素数组,并创建BA容器。

在非常具体的情况下(普通整数列表),我建议你只计算它们,因为你不能区分不同的整数:

for(int i = 0; i < countOfNumbers; i++)
    counter[number[i]]++;

如果 可区分,请创建一个列表数组,并将它们添加到相应的列表中。

如果它们不是数字,请使用std :: map或std :: hash_map,将键映射到值列表。

答案 6 :(得分:0)

最简单的可能就是对列表进行排序,然后迭代它以查找重复。

如果您对数据有所了解,可以使用更有效的算法。

例如,如果您知道列表很大,并且只包含1..n中的整数,其中n相当小,则可以使用一对布尔数组(或位图),并执行以下操作:< / p>

bool[] once = new bool[MAXIMUM_VALUE];
bool[] many = new bool[MAXIMUM_VALUE];
for (int i = 0; i < list.Length; i++)
    if (once[ value[i] ])
        many[ value[i] ] = true;
    else
        once[ value[i] ] = true;

现在,许多[]包含多个值被多次看到的数组。

答案 7 :(得分:0)

大多数提到哈希/无序地图解决方案的人都假设O(1)插入和查询时间,但它可能是O(N)最坏情况。此外,您还可以消除对象散列的成本。

我个人会将对象插入二叉树(每个插入一次O(logn)),并在每个节点上保留计数器。这将产生O(nlogn)构造时间和O(n)遍历以识别所有重复。

答案 8 :(得分:0)

如果我正确理解了这个问题,那么这是我能想到的最简单的解决方案:

std::vector<T> input;
typedef std::vector<T>::iterator iter;

std::vector<std::pair<iter, iter> > output;

sort(input.begin(), input.end()); // O(n lg n) comparisons

for (iter cur = input.begin(); cur != input.end();){
  std::pair<iter, iter> range = equal_range(cur, input.end(), *cur); // find all elements equal to the current one O(lg n) comparisons
  cur = range.second;
  output.push_back(range);
}

总运行时间:O(n log n)。 您有一个排序过程O(n lg n),然后是第二个过程,其中{em>每个组执行O(lg n)比较(所以最多 {{ 1}}次,也产生n

请注意,这取决于输入是向量。只有随机访问迭代器在第二遍中具有对数复杂度。双向迭代器将是线性的。

这不依赖于哈希(按要求),它保留所有原始元素(而不是仅返回每个组的一个元素,以及它发生频率的计数)。

当然,可以进行一些较小的常量优化。输出向量上的O(n lg n)是个好主意,以避免重新分配。 output.reserve(input.size())的使用频率高得多,而且可以轻松缓存。

根据假设的群体大小,input.end()可能不是最有效的选择。我假设它进行二进制搜索以获得对数复杂度,但如果每个组只有几个元素,则简单的线性扫描会更快。无论如何,初始排序虽然支配了成本。

答案 9 :(得分:0)

注意我在我正在使用的三重商店的规范化过程中遇到了同样的问题。我使用Allegro Common Lisp中的哈希表功能实现了Charles Bailey在Common Lisp中总结的方法3。

功能“代理人相等?”用于测试TS中的两个代理何时相同。函数“merge-nodes”合并每个集群上的节点。在下面的代码中,“...”用于删除不那么重要的部分。

(defun agent-equal? (a b)
 (let ((aprops (car (get-triples-list :s a :p !foaf:name)))
       (bprops (car (get-triples-list :s b :p !foaf:name))))
   (upi= (object aprops) (object bprops))))

(defun process-rdf (out-path filename)
 (let* (...
        (table (make-hash-table :test 'agent-equal?)))
  (progn 
   ...
   (let ((agents (mapcar #'subject 
          (get-triples-list :o !foaf:Agent :limit nil))))
     (progn 
       (dolist (a agents)
          (if (gethash a table)
            (push a (gethash a table))
            (setf (gethash a table) (list a))))
       (maphash #'merge-nodes table)
           ...
           )))))

答案 10 :(得分:0)

自C ++ 11以来,STL使用std::unordered_map提供了哈希表。 所以O(N)解决方案是将您的值放入unordered_map< T, <vector<T> >