【问题标题】:How to keep only duplicates efficiently?如何有效地只保留重复项?
【发布时间】:2011-02-02 18:16:16
【问题描述】:

给定一个 STL 向量,仅按排序顺序输出重复项,例如,

INPUT : { 4, 4, 1, 2, 3, 2, 3 }
OUTPUT: { 2, 3, 4 }

该算法是微不足道的,但目标是使其与 std::unique() 一样高效。我的幼稚实现就地修改了容器:

我的幼稚实现:

void not_unique(vector<int>* pv)
{
    if (!pv)
        return;

 // Sort (in-place) so we can find duplicates in linear time
 sort(pv->begin(), pv->end());

 vector<int>::iterator it_start = pv->begin();
 while (it_start != pv->end())
 {
  size_t nKeep = 0;

  // Find the next different element
  vector<int>::iterator it_stop = it_start + 1;
  while (it_stop != pv->end() && *it_start == *it_stop)
  {
   nKeep = 1; // This gets set redundantly
   ++it_stop;
  }

  // If the element is a duplicate, keep only the first one (nKeep=1).
  // Otherwise, the element is not duplicated so erase it (nKeep=0).
  it_start = pv->erase(it_start + nKeep, it_stop);
 }
}

如果您可以使其更高效、更优雅或更通用,请告诉我。例如,自定义排序算法,或在第二个循环中复制元素以消除erase() 调用。

【问题讨论】:

  • std::unique() 假定向量已排序。您能否详细说明您认为您的代码效率较低的原因?
  • 只是为了挑选您所拥有的:通过引用获取容器。没有理由在这里使用指针(例如,使用keep_duplicates(0) 并不安全。)函数内部的代码和函数的调用都会稍微简化一些。 :)
  • 澄清一下:如果输入是{1, 1, 1},输出应该是{1}还是{1, 1}
  • 这不是 O(n)。由于 erase 具有线性复杂度,所以它是 O(n^2),在 while 循环内部,它也具有线性复杂度。
  • @GMan:我总是通过指针传递输出参数以指示它们可以被修改。这样,当用户看到像“foo(a, b, &c, &d);”这样的函数调用时,他们无需阅读文档即可知道哪些参数是输入和输出。不利的一面是,正如您所指出的那样,实现有点复杂(是的,它应该检查 NULL)。

标签: c++ algorithm stl performance unique


【解决方案1】:

我的建议是修改插入排序,这样您就可以同时排序和过滤欺骗。

【讨论】:

    【解决方案2】:

    我认为从大 O 的角度来看,您已经将其实现得尽可能好。最重要的成本是排序,即 O(N log N)。但是,一种可能性是使用重复条目构建一个新向量,而不是使用现有向量和删除操作删除非重复项。但是,只有当重复的不同数量相对于条目总数而言较小时,这才会更好。

    考虑一个极端的例子。如果原始数组由 1,000 个条目组成,只有一个重复项,那么输出将是一个只有一个值的向量。使用一个条目创建新向量可能比从原始向量中删除其他 999 个条目更有效。但是,我怀疑在现实世界的测试中,这种更改所节省的成本可能难以衡量。

    编辑我只是从“面试”问题的角度考虑这个问题。换句话说,这不是一个非常有用的答案。但是有可能在 O(N)(线性时间)而不是 O(N Log N) 中解决这个问题。使用存储空间而不是 CPU。创建两个“位”数组,最初清除它们。循环遍历整数值向量。在第一位数组中查找每个值。如果未设置,则设置该位(将其设置为 1)。如果已设置,则设置第二个数组中的相应位(表示重复)。处理完所有向量条目后,扫描第二个数组并输出重复的整数(由第二个位数组中设置的位表示)。使用位数组的原因只是为了空间效率。如果处理 4 字节整数,则所需的原始空间为 (2 * 2^32 / 8 )。但这实际上可以通过使其成为一个稀疏数组来变成一个可用的算法。非常伪的伪代码是这样的:

    bitarray1[infinite_size];
    bitarray2[infinite_size];
    
    clear/zero bitarrays
    
    // NOTE - do not need to sort the input
    foreach value in original vector {
        if ( bitarray1[value] ) 
            // duplicate
            bitarray2[value] = 1
        bitarray1[value] = 1
    }
    
    // At this point, bitarray2 contains a 1 for all duplicate values.
    // Scan it and create the new vector with the answer
    for i = 0 to maxvalue
        if ( bitarray2[i] )
            print/save/keep i
    

    【讨论】:

      【解决方案3】:

      调用“erase(it_start + keep, it_stop);”在 while 循环中将导致一遍又一遍地复制剩余的元素。

      我建议将所有唯一元素交换到矢量的前面,然后一次性擦除剩余的元素。

      int num_repeats(vector<int>::const_iterator curr, vector<int>::const_iterator end) {
        int same = *curr;
        int count = 0;
        while (curr != end && same == *curr) {
          ++curr;
          ++count;
        }
        return count;
      }
      
      void dups(vector<int> *v) {
        sort(v->begin(), v->end());
        vector<int>::iterator current = v->begin();
        vector<int>::iterator end_of_dups = v->begin();
        while (current != v->end()) {
          int n = num_repeats(current, v->end());
          if (n > 1) {
            swap(*end_of_dups, *current);
            end_of_dups++;
          }
          current += n;
        }
        v->erase(end_of_dups, v->end());
      }
      

      【讨论】:

        【解决方案4】:

        我的第一次尝试失败了,假设std::unique 将所有重复项移动到范围的末尾(它没有)。哎呀。这是另一个尝试:

        这是not_unique 的实现。它删除在排序范围内只出现一次的任何元素重复出现多次的任何元素。因此,结果范围是出现多次的唯一元素列表。

        该函数具有线性复杂性,并在该范围内进行单次传递(std::unique 具有线性复杂性)。 It 必须满足前向迭代器的要求。返回结果范围的结尾。

        template <typename It>
        It not_unique(It first, It last)
        {
            if (first == last) { return last; }
        
            It new_last = first;
            for (It current = first, next = ++first; next != last; ++current, ++next)
            {
                if (*current == *next)
                {
                    if (current == new_last)
                    {
                        ++new_last;
                    }
                    else
                    {
                        *new_last++ = *current;
                        while (next != last && *current == *next)
                        {
                            ++current;
                            ++next;
                        }
                        if (next == last)
                            return new_last;
                    }
                }
            }
            return new_last;
        }
        

        【讨论】:

        • +1 我希望你不介意,但我在回答中给出了完整的答案。 (也就是说,在我写的内容中,我有上一个/当前,而不是当前/下一个,所以我保留了它。但否则你写了内部部分。)
        • 当范围应该被排序时,我通常更喜欢在开头添加一个is_sorted(以防万一......)。使用adjacent_find 和反向二元谓词可以很容易地编写它。
        • @Matthieu: 范围排序是调用函数的前提条件(std::unique也有同样的前提条件)。不过,我同意调试断言对于捕获逻辑错误很有用。 @GMan:我一点也不介意。看起来不错。
        • 这不适用于 { 4, 4, 1, 2, 3, 4, 2, 4, 3 }。输出应为 { 2, 3, 4 } 但改为 { 2, 3, 4, 4, 4 }。
        • @Marc:很好。我不应该在半夜写代码:-)。比较 *current != *new_last 无效,因为 *new_last 永远不会是结果范围的有效部分。这可以通过比较*current != *(new_last - 1) 来轻松纠正,但是我们需要随机访问迭代器。我已经更新了算法来修复它,以便它确实适用于前向迭代器,但现在它有点令人费解:-O。今晚我可能有时间看看它并清理它。
        【解决方案5】:

        这是标准库的风格。感谢算法goes to James! (如果你 +1 我,你最好 +1 他,否则)。我所做的只是使其成为标准库风格:

        #include <algorithm>
        #include <functional>
        #include <iostream>
        #include <iterator>
        #include <vector>
        
        // other stuff (not for you)
        template <typename T>
        void print(const char* pMsg, const T& pContainer)
        {
            std::cout << pMsg << "\n    ";
            std::copy(pContainer.begin(), pContainer.end(),
                std::ostream_iterator<typename T::value_type>(std::cout, " "));
            std::cout << std::endl;
        }
        
        template <typename T, size_t N>
        T* endof(T (&pArray)[N])
        {
            return &pArray[0] + N;
        }
        
        // not_unique functions (for you)
        template <typename ForwardIterator, typename BinaryPredicate>
        ForwardIterator not_unique(ForwardIterator pFirst, ForwardIterator pLast,
                                   BinaryPredicate pPred)
        {
            // correctly handle case where an empty range was given:
            if (pFirst == pLast) 
            { 
                return pLast; 
            }
        
            ForwardIterator result = pFirst;
            ForwardIterator previous = pFirst;
        
            for (++pFirst; pFirst != pLast; ++pFirst, ++previous)
            {
                // if equal to previous
                if (pPred(*pFirst, *previous))
                {
                    if (previous == result)
                    {
                        // if we just bumped bump again
                        ++result;
                    }
                    else if (!pPred(*previous, *result))
                    {
                        // if it needs to be copied, copy it
                        *result = *previous;
        
                        // bump
                        ++result;
                    }
                }
            }
        
            return result;
        }
        
        template <typename ForwardIterator>
        ForwardIterator not_unique(ForwardIterator pFirst, ForwardIterator pLast)
        {
            return not_unique(pFirst, pLast,
                        std::equal_to<typename ForwardIterator::value_type>());
        }
        
        
        //test
        int main()
        {
            typedef std::vector<int> vec;
        
            int data[] = {1, 4, 7, 7, 2, 2, 2, 3, 9, 9, 5, 4, 2, 8};
            vec v(data, endof(data));
        
            // precondition
            std::sort(v.begin(), v.end());
            print("before", v);
        
            // duplicatify (it's a word now)
            vec::iterator iter = not_unique(v.begin(), v.end());
            print("after", v);
        
            // remove extra
            v.erase(iter, v.end());
            print("erased", v);
        }
        

        【讨论】:

        • james 算法中唯一困扰我的是我们不检查它是否实际排序。但是,通过要求二元谓词是 sort 操作使用的谓词(而不是相等谓词),我们可以实现它。
        • @Matthieu:谢谢。嗯,这是一个先决条件。就像在unique 中一样。
        • 我添加了保护子句来处理first == last 的情况,以便语义匹配unique 的空范围。除此之外,它看起来真的很好。
        • 这不适用于 { 4, 4, 1, 2, 3, 4, 2, 4, 3 }。输出应为 { 2, 3, 4 } 但改为 { 2, 3, 4, 4, 4 }。
        【解决方案6】:

        您甚至可以使用不匹配来获得加分!
        顺便说一句:很好的锻炼。

        template<class TIter>
        /** Moves duplicates to front, returning end of duplicates range.
         *  Use a sorted range as input. */
        TIter Duplicates(TIter begin, TIter end) {
            TIter dup = begin;
            for (TIter it = begin; it != end; ++it) {
                TIter next = it;
                ++next;
                TIter const miss = std::mismatch(next, end, it).second;
                if (miss != it) {
                    *dup++ = *miss;
                    it = miss;
                }
            }
            return dup;
        }
        

        【讨论】:

          【解决方案7】:

          另一个:

          template <typename T>
          void keep_duplicates(vector<T>& v) 
          {
              set<T> 
                  u(v.begin(), v.end()), // unique 
                  d; // duplicates
              for (size_t i = 0; i < v.size(); i++)
                  if (u.find(v[i]) != u.end())
                      u.erase(v[i]);
                  else
                      d.insert(v[i]);
          
              v = vector<T>(d.begin(), d.end());
          }
          

          【讨论】:

          • 很好的解决方案,但当 n 很大 (10B) 时,内存或空间效率不高。在所有元素都是唯一的情况下,u 是 v 的相同副本(想想所有的动态分配!)。创建 u 是 O(n log n),for 循环是 O(n log n)。
          • 这是对 OP 的答案,其中 T 是 int,n 是 7 :-) 对于昂贵的 T 副本,您应该使用 T* 或 T& 作为输入向量。对于较大的 n,调用者应该并行化。无论如何将其与其他答案进行基准比较:-)
          【解决方案8】:

          这修复了James McNellis's 原始版本中的错误。我还提供就地和非就地版本。

          // In-place version.  Uses less memory and works for more container
          // types but is slower.
          template <typename It>
          It not_unique_inplace(It first, It last)
          {
              if (first == last)
                  return last;
          
              It new_last = first;
              for (It current = first, next = first + 1; next != last; ++current, ++next)
              {
                  if (*current == *next && 
                      (new_last == first || *current != *(new_last-1)))
                      *new_last++ = *current;
              }
              return new_last;
          }
          
          // Out-of-place version. Fastest.
          template <typename It, typename Container>
          void not_unique(It first, It last, Container pout)
          {
              if (first == last || !pout)
                  return;
          
              for (It current = first, next = first + 1; next != last; ++current, ++next)
              {
                  if (*current == *next && 
                      (pout->empty() || *current != pout->back()))
                      pout->push_back(*current);
              }
          }
          

          【讨论】:

            【解决方案9】:

            “与 std::unique 一样高效”是什么意思?在运行时、开发时间、内存使用等方面效率高吗?

            正如其他人指出的那样,std::unique 需要排序输入,而您没有提供,因此一开始就不是一个公平的测试。

            就我个人而言,我只需要一个 std::map 来为我完成所有工作。它有很多属性,我们可以使用它来实现最大的优雅/简洁。它保持其元素已经排序,如果键不存在,则 operator[] 将插入一个零值。通过利用这些属性,我们可以在两三行代码中完成这项工作,并且仍然可以实现合理的运行时复杂性。

            基本上,我的算法是这样的:对于向量中的每个元素,将由该元素的值作为键的映射条目加一。之后,只需遍历地图,输出任何值大于 1 的键。再简单不过了。

            #include <iostream>
            #include <vector>
            #include <map>
            
            void
            output_sorted_duplicates(std::vector<int>* v)
            {
               std::map<int, int> m;  
            
               // count how many of each element there are, putting results into map
               // map keys are elements in the vector, 
               // map values are the frequency of that element
               for (std::vector<int>::iterator vb = v->begin(); vb != v->end(); ++vb)
                  ++m[*vb];
            
               // output keys whose values are 2 or more
               // the keys are already sorted by the map
               for (std::map<int, int>::iterator mb = m.begin(); mb != m.end(); ++mb)
                  if ( (*mb).second >= 2 ) 
                     std::cout << (*mb).first << " "; 
               std::cout << std::endl;
            }
            
            int main(void) 
            { 
               int initializer[] = { 4, 4, 1, 2, 3, 2, 3 };
               std::vector<int> data(&initializer[0], &initializer[0] + 7);
               output_sorted_duplicates(&data);
            }
            
            janks@phoenix:/tmp$ g++ test.cc && ./a.out
            2 3 4
            

            所以,我们访问你的向量中的每个元素一次,然后访问我的地图中的每个元素一次,我的地图中的元素数量最多不超过你的向量。我的解决方案的缺点是存储空间比涉及就地重新排列矢量的解决方案要多得多。然而,优势是显而易见的。它非常简短和简单,显然是正确的,无需进行大量测试或代码审查,并且具有合理的性能属性。

            让我的函数成为一个模板,并让它在 STL 风格的范围上运行,而不仅仅是 int 的向量,留作练习。

            【讨论】:

              【解决方案10】:

              比之前的条目更短,更符合 STL。假设输入已排序。

              #include <algorithm>
              #include <functional>
              
              template< class I, class P >
              I remove_unique( I first, I last, P pred = P() ) {
                  I dest = first;
                  while (
                      ( first = std::adjacent_find( first, last, pred ) )
                          != last ) {
                      * dest = * first;
                      ++ first;
                      ++ dest;
                      if ( ( first = std::adjacent_find( first, last, std::not2( pred ) ) )
                          == last ) break;
                      ++ first;
                  }
                  return dest;
              }
              
              template< class I >
              I remove_unique( I first, I last ) {
                  return remove_unique( first, last,
                      std::equal_to< typename std::iterator_traits<I>::value_type >() );
              }
              

              【讨论】:

              • +1; 非常不错。我不熟悉adjacent_find。很遗憾这个问题已成为社区 wiki。
              • @James:谢谢。我之前错过了 Jan 的 mismatch 条目,我认为这可能更优雅。如果我将boost::bindstd::equal_to 与交替标志一起使用而不是交替not2,我的会更好。但可能更慢。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-12-21
              • 2013-09-29
              • 2020-01-15
              • 2018-10-09
              • 2019-09-05
              • 2010-09-12
              相关资源
              最近更新 更多