代码之家  ›  专栏  ›  技术社区  ›  Bill Michell

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

  •  22
  • Bill Michell  · 技术社区  · 6 年前

    假设您有一个元素集合,那么您如何挑选那些重复的元素,并将它们以最少的比较量放入每个组中呢?最好是C++,但是算法比语言更重要。 例如 给定e1、e2、e3、e4、e4、e2、e6、e4、e3,我想提取e2、e2、e3、e3、e4、e4、e4。 您将选择什么数据结构和算法?如果数据结构是预先排序的,比如std::multimap,请包括设置数据结构的成本。

    更新

    使事情更清楚。有一个限制: 元素必须自己比较 以确定它们是重复的。

    所以散列不适用,因为实际上它们将比较从重元素(例如数据块)转移到轻元素(整数),并减少一些比较,但不能消除它们,最后,当我们在一个碰撞桶内时,我们回到了最初的问题。

    假设你有一堆潜在的GBS重复文件,每个人都知道它们具有相同的哈希值。现在你要找出真正的副本。

    不,这不可能是一个现实问题(即使MD5也足以为现实文件生成唯一的哈希)。但是假装我们可以 专注于寻找数据结构+算法,其中包含最少的比较量 .


    我要做的是

    1. 表示为stl std::list数据结构(在该结构中1)它的元素删除比(例如)向量更便宜2)它的插入更便宜,不需要排序。)

    2. 弹出一个元素并将其与其余元素进行比较,如果发现重复的元素,则会将其从列表中拉出。一旦到达列表的末尾,就会发现一组重复(如果有的话)。

    3. 重复上述2个步骤,直到列表为空。

    在最好的情况下,它需要n-1,但是(n-1)!更糟的是。

    更好的选择是什么?


    我的代码使用上述方法:

    // algorithm to consume the std::list container,
    // supports: list<path_type>,list< pair<std::string, paths_type::const_iterater>>
    template<class T>
    struct consume_list
    {
        groups_type operator()(list<T>& l)
        {
            // remove spurious identicals and group the rest
            // algorithm:  
            // 1. compare the first element with the remaining elements, 
            //    pick out all duplicated files including the first element itself.
            // 2. start over again with the shrinked list
            //     until the list contains one or zero elements.
    
            groups_type sub_groups;           
            group_type one_group; 
            one_group.reserve(1024);
    
            while(l.size() > 1)
            {
                T front(l.front());
                l.pop_front();
    
                item_predicate<T> ep(front);
                list<T>::iterator it     = l.begin(); 
                list<T>::iterator it_end = l.end();
                while(it != it_end)
                {
                    if(ep.equals(*it))
                    {
                        one_group.push_back(ep.extract_path(*(it))); // single it out
                        it = l.erase(it);
                    }
                    else
                    {
                        it++;
                    }
                }
    
                // save results
                if(!one_group.empty())
                {
                    // save
                    one_group.push_back(ep.extract_path(front));                    
                    sub_groups.push_back(one_group);
    
                    // clear, memory allocation not freed
                    one_group.clear(); 
                }            
            }
            return sub_groups;
        }        
    }; 
    
    
    // type for item-item comparison within a stl container, e.g.  std::list 
    template <class T>
    struct item_predicate{};
    
    // specialization for type path_type      
    template <>
    struct item_predicate<path_type>
    {
    public:
        item_predicate(const path_type& base)/*init list*/            
        {}
    public:
        bool equals(const path_type& comparee)
        {
            bool  result;
            /* time-consuming operations here*/
            return result;
        }
    
        const path_type& extract_path(const path_type& p)
        {
            return p;
        }
    private:
        // class members
    }; 
    
    
    };
    

    感谢下面的答案,但是它们似乎被我的例子误导了,它是关于整数的。事实上 元素类型不可知(不一定是整数、字符串或任何其他pods) ,等谓词是自定义的,即 比较可能很重 .

    所以我的问题应该是:使用哪种数据结构+算法需要较少的比较。

    根据我的测试,使用像multiset这样的预先排序的容器,multimap并不好,因为

    1. 插入时的排序已经进行了比较,
    2. 下面的相邻发现再次进行比较,
    3. 这些数据结构更喜欢小于运算而不是相等运算,它们执行的小于2(a

    我不知道它如何保存比较。


    下面的一些答案忽略了另外一件事,我需要区分重复的组,而不仅仅是将它们保存在容器中。


    结论

    经过所有的讨论,似乎有三种方法

    1. 我最初的天真方法如上所述
    2. 从线性容器开始 std::vector ,对其排序,然后找到相等的范围
    3. 从关联的容器开始,例如 std::map<Type, vector<duplicates>> ,根据Charles Bailey的建议,在关联容器的设置过程中挑选重复项。

    我已经编写了一个样本来测试下面发布的所有方法。

    副本的数量和分发时间可能会影响最佳选择。

    • 方法1是最好的,当他们严重地落在前面,而最坏的是在最后。排序不会改变分布,但会改变尾数。
    • 方法3具有最平均的性能
    • 方法2永远不是最佳选择

    感谢所有参与讨论的人。

    一个输出,包含下面代码中的20个示例项。

    用[20 10 6 5 4 3 2 2 2 2 1测试 1 1 1 1 1 1 1 1 1 1]

    和[1 1 1 1 1 1 1 1 1 2 2 2 3 4 5 6 10 20]

    使用std::vector->sort()-> 相邻的查找():

    比较:'<'=139,=='=23 ]

    比较:'<'=38,=='=23]

    使用std::list->sort()->收缩 列表:

    比较:'<'=50,=='=43]

    比较:'<'=52,=='=43]

    使用标准::列表->收缩列表:

    比较:'<'=0,=='=121]

    比较:'<'=0,=='=43]

    使用std::vector->std::map>:

    比较:'<'=79,=='=0]

    比较:'<'=53,=='=0]

    使用std::vector-> 标准::多集-> 相邻的查找():

    比较:'<'=79,=='=7]

    比较:'<'=53,=='=7]

    代码

    // compile with VC++10: cl.exe /EHsc
    
    #include <vector>
    #include <deque>
    #include <list>
    #include <map>
    #include <set>
    #include <algorithm>
    #include <iostream>
    #include <sstream>
    
    #include <boost/foreach.hpp>
    #include <boost/tuple/tuple.hpp>
    #include <boost/format.hpp>
    
    using namespace std;
    
    struct Type
    {
        Type(int i) : m_i(i){}
    
        bool operator<(const Type& t) const
        {
            ++number_less_than_comparison;
            return m_i < t.m_i;
        }
    
        bool operator==(const Type& t) const
        {
            ++number_equal_comparison;    
            return m_i == t.m_i;
        }
    public:
        static void log(const string& operation)
        {
            cout 
            << "comparison during " <<operation << ": [ "
            << "'<'  = " << number_less_than_comparison
            << ", "
            << "'==' = " << number_equal_comparison
            << " ]\n";
    
            reset();
        }
    
        int to_int() const
        {
            return m_i;
        }
    private:
        static void reset()
        {
            number_less_than_comparison = 0;
            number_equal_comparison = 0;      
        }
    
    public:
        static size_t number_less_than_comparison;
        static size_t number_equal_comparison;    
    private:
        int m_i;
    };
    
    size_t Type::number_less_than_comparison = 0;
    size_t Type::number_equal_comparison = 0;  
    
    ostream& operator<<(ostream& os, const Type& t) 
    {
        os << t.to_int();
        return os;
    }
    
    template< class Container >
    struct Test
    {    
        void recursive_run(size_t n)
        { 
            bool reserve_order = false;
    
            for(size_t i = 48; i < n; ++i)
            {
                run(i);
            }    
        }
    
        void run(size_t i)
        {
            cout << 
            boost::format("\n\nTest %1% sample elements\nusing method%2%:\n") 
            % i 
            % Description();
    
            generate_sample(i);
            sort();
            locate();   
    
            generate_reverse_sample(i);
            sort();
            locate(); 
        }
    
    private:    
        void print_me(const string& when)
        {
            std::stringstream ss;
            ss << when <<" = [ ";
            BOOST_FOREACH(const Container::value_type& v, m_container)
            {
                ss << v << " ";
            }
            ss << "]\n";    
            cout << ss.str();
        }
    
        void generate_sample(size_t n)
        {
            m_container.clear();
            for(size_t i = 1; i <= n; ++i)
            {
                m_container.push_back(Type(n/i));    
            }
            print_me("init value");
            Type::log("setup");
        }
    
        void generate_reverse_sample(size_t n)
        {
            m_container.clear();
            for(size_t i = 0; i < n; ++i)
            {
                m_container.push_back(Type(n/(n-i)));     
            }
            print_me("init value(reverse order)");
            Type::log("setup");
        }    
    
        void sort()
        {    
            sort_it();
    
            Type::log("sort");
            print_me("after sort");
    
        }
    
        void locate()
        {
            locate_duplicates();
    
            Type::log("locate duplicate");
        }
    protected:
        virtual string Description() = 0;
        virtual void sort_it() = 0;
        virtual void locate_duplicates() = 0;
    protected:
        Container m_container;    
    };
    
    struct Vector : Test<vector<Type> >
    {    
        string Description()
        {
            return "std::vector<Type> -> sort() -> adjacent_find()";
        } 
    
    private:           
        void sort_it()
        {    
            std::sort(m_container.begin(), m_container.end()); 
        }
    
        void locate_duplicates()
        {
            using std::adjacent_find;
            typedef vector<Type>::iterator ITR;
            typedef vector<Type>::value_type  VALUE;
    
            typedef boost::tuple<VALUE, ITR, ITR> TUPLE;
            typedef vector<TUPLE> V_TUPLE;
    
            V_TUPLE results;
    
            ITR itr_begin(m_container.begin());
            ITR itr_end(m_container.end());       
            ITR itr(m_container.begin()); 
            ITR itr_range_begin(m_container.begin());  
    
            while(itr_begin != itr_end)
            {     
                // find  the start of one equal reange
                itr = adjacent_find(
                itr_begin, 
                itr_end, 
                    []  (VALUE& v1, VALUE& v2)
                    {
                        return v1 == v2;
                    }
                );
                if(itr_end == itr) break; // end of container
    
                // find  the end of one equal reange
                VALUE start = *itr; 
                while(itr != itr_end)
                {
                    if(!(*itr == start)) break;                
                    itr++;
                }
    
                results.push_back(TUPLE(start, itr_range_begin, itr));
    
                // prepare for next iteration
                itr_begin = itr;
            }  
        }
    };
    
    struct List : Test<list<Type> >
    {
        List(bool sorted) : m_sorted(sorted){}
    
        string Description()
        {
            return m_sorted ? "std::list -> sort() -> shrink list" : "std::list -> shrink list";
        }
    private:    
        void sort_it()
        {
            if(m_sorted) m_container.sort();////std::sort(m_container.begin(), m_container.end()); 
        }
    
        void locate_duplicates()
        {       
            typedef list<Type>::value_type VALUE;
            typedef list<Type>::iterator ITR;
    
            typedef vector<VALUE>  GROUP;
            typedef vector<GROUP>  GROUPS;
    
            GROUPS sub_groups;
            GROUP one_group; 
    
            while(m_container.size() > 1)
            {
                VALUE front(m_container.front());
                m_container.pop_front();
    
                ITR it     = m_container.begin(); 
                ITR it_end = m_container.end();
                while(it != it_end)
                {
                    if(front == (*it))
                    {
                        one_group.push_back(*it); // single it out
                        it = m_container.erase(it); // shrink list by one
                    }
                    else
                    {
                        it++;
                    }
                }
    
                // save results
                if(!one_group.empty())
                {
                    // save
                    one_group.push_back(front);                    
                    sub_groups.push_back(one_group);
    
                    // clear, memory allocation not freed
                    one_group.clear(); 
                }            
            }
        }        
    
    private:
        bool m_sorted;
    };
    
    struct Map : Test<vector<Type>>
    {    
        string Description()
        {
            return "std::vector -> std::map<Type, vector<Type>>" ;
        }
    private:    
        void sort_it() {}
    
        void locate_duplicates()
        {
            typedef map<Type, vector<Type> > MAP;
            typedef MAP::iterator ITR;
    
            MAP local_map;
    
            BOOST_FOREACH(const vector<Type>::value_type& v, m_container)
            {
                pair<ITR, bool> mit; 
                mit = local_map.insert(make_pair(v, vector<Type>(1, v)));   
                if(!mit.second) (mit.first->second).push_back(v); 
             }
    
            ITR itr(local_map.begin());
            while(itr != local_map.end())
            {
                if(itr->second.empty()) local_map.erase(itr);
    
                itr++;
            }
        }        
    };
    
    struct Multiset :  Test<vector<Type>>
    {
        string Description()
        {
            return "std::vector -> std::multiset<Type> -> adjacent_find()" ;
        }
    private:
        void sort_it() {}
    
        void locate_duplicates()
        {   
            using std::adjacent_find;
    
            typedef set<Type> SET;
            typedef SET::iterator ITR;
            typedef SET::value_type  VALUE;
    
            typedef boost::tuple<VALUE, ITR, ITR> TUPLE;
            typedef vector<TUPLE> V_TUPLE;
    
            V_TUPLE results;
    
            SET local_set;
            BOOST_FOREACH(const vector<Type>::value_type& v, m_container)
            {
                local_set.insert(v);
            }
    
            ITR itr_begin(local_set.begin());
            ITR itr_end(local_set.end());       
            ITR itr(local_set.begin()); 
            ITR itr_range_begin(local_set.begin());  
    
            while(itr_begin != itr_end)
            {     
                // find  the start of one equal reange
                itr = adjacent_find(
                itr_begin, 
                itr_end, 
                []  (VALUE& v1, VALUE& v2)
                    {
                        return v1 == v2;
                    }
                );
                if(itr_end == itr) break; // end of container
    
                // find  the end of one equal reange
                VALUE start = *itr; 
                while(itr != itr_end)
                {
                    if(!(*itr == start)) break;                
                    itr++;
                }
    
                results.push_back(TUPLE(start, itr_range_begin, itr));
    
                // prepare for next iteration
                itr_begin = itr;
            }  
        } 
    };
    
    int main()
    {
        size_t N = 20;
    
        Vector().run(20);
        List(true).run(20);
        List(false).run(20);
        Map().run(20);
        Multiset().run(20);
    }
    
    11 回复  |  直到 8 年前
        1
  •  3
  •   CB Bailey    15 年前

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

    这个例子总是将第一个有代表性的元素插入到映射的deque存储中,因为它使通过组进行的后续迭代在逻辑上变得简单,但是如果这个重复证明了一个问题,那么只执行 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
            }
        }
    }
    

    我做了一些比较和对象计数的实验。在以100000个随机顺序组成50000组(即每组2个对象的平均值)的对象进行的测试中,上述方法的比较和复制次数如下:

    1630674 comparisons, 443290 copies
    

    (我试着减少副本的数量,但实际上是以牺牲比较为代价的,这在您的场景中似乎是成本较高的操作。)

    使用多映射,并在最终迭代中保留上一个元素以检测组转换,这会导致以下代价:

    1756208 comparisons, 100000 copies
    

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

    1885879088 comparisons, 100000 copies
    

    是的,这是我的最佳方法的~1.9b与~1.6m的比较。要使list方法在接近最佳比较数的任何地方执行,必须对其进行排序,这将花费与构建固有排序的容器相似的比较数。

    编辑

    我使用了您发布的代码,并对我以前使用的相同测试数据集运行了隐含算法(我必须对代码做一些假设,就像一些假设的定义那样),我计算了:

    1885879088 comparisons, 420939 copies
    

    也就是说,与我的哑列表算法的比较数量完全相同,但是有更多的副本。我认为这意味着我们在这个案例中使用了基本相似的算法。我看不到任何其他排序顺序的证据,但看起来您需要一个包含多个等效元素的组列表。这可以在我的 Iterate 通过添加功能 if (i->size > 1) 条款。

    我仍然看不到任何证据表明,像这张Deques地图这样的分类容器并不是一个好的(即使不是最佳的)策略。

        2
  •  13
  •   derobert    15 年前

    是的,你可以做得更好。

    1. 对它们进行排序(对于简单整数为O(n),通常为O(n*log n),然后保证重复项是相邻的,从而快速地找到它们。

    2. 使用哈希表,也可以使用O(N)。对于每个项目,(a)检查它是否已经在哈希表中;如果已经在哈希表中,则它是一个副本;如果没有,则将它放在哈希表中。

    编辑

    您使用的方法似乎可以进行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)较小,对于任何合理较高的n值。

    你的箱子里N有多大?

    http://img188.imageshack.us/img188/7315/foou.png

    就减少哈希冲突而言,最好的方法是获得一个更好的哈希函数-d。假设这不可能,如果您可以生成一个变量(例如,不同的模),您可能可以执行嵌套哈希。

        3
  •  7
  •   Nathan    15 年前

    1。最坏情况下对数组o(n log n)排序-mergesort/heapsort/binary tree sort等

    2。比较邻居并把火柴拔出O(N)

        4
  •  5
  •   Alex Martelli    15 年前

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

        5
  •  1
  •   hhafez    15 年前

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

        6
  •  1
  •   Martin v. Löwis    15 年前

    如果已知为整数列表,并且知道它们都在a和b之间(例如a=0,b=9),则生成b-a元素数组,并创建b-a容器。

    在非常特殊的情况下(普通整数列表),我建议您只计算它们,因为您无论如何都不能区分不同的整数:

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

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

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

        7
  •  0
  •   lavinio    15 年前

    最简单的方法可能是对列表进行排序,然后迭代查找dup。

    如果你对数据有所了解,更有效的算法是可能的。

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

    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;
    

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

        8
  •  0
  •   ttvd    15 年前

    大多数提到散列/无序映射解决方案的人都假设O(1)插入和查询时间,但这可能是O(n)最坏的情况。此外,还可以减少对象散列的成本。

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

        9
  •  0
  •   Stack Overflow is garbage    15 年前

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

    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) 然后第二次传球 O(lg n) 进行比较 每组 (这样就完成了 至多 n 时代,同样屈服 O(n LG n) .

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

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

    当然,一些较小的常量优化是可能的。 output.reserve(input.size()) 在输出向量上会是一个好主意,以避免重新分配。 input.end() 被占用的次数也比需要的要多,并且可以很容易地被缓存。

    根据假设的群体规模, equal_range 可能不是最有效的选择。我假设它做了一个二进制搜索来获得对数复杂性,但是如果每个组只有两个元素,一个简单的线性扫描就会更快。不过,在任何情况下,初始排序都是成本的主要部分。

        10
  •  0
  •   Alexandre Rademaker    13 年前

    只是为了说明我在三重存储的正常化过程中遇到了同样的问题。我使用Allegro Common Lisp的散列表功能实现了由Charles Bailey总结的方法3,该方法在Common Lisp中。

    函数“agent equal?”用于测试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)
               ...
               )))))
    
        11
  •  0
  •   ismax    8 年前

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