【问题标题】:Merging two lists efficiently with limited bound在有限的范围内有效地合并两个列表
【发布时间】:2013-10-19 18:55:04
【问题描述】:

我正在尝试合并两个数组/列表,其中必须比较数组的每个元素。如果它们两者中存在相同的元素,我将它们的总出现次数增加一。数组都是二维的,其中每个元素都有一个计数器用于其出现。我知道这两个数组都可以与 O(n^2) 中的双循环进行比较,但是我受到 O(nlogn) 的限制。如果出现不止一次,则最终数组将包含两个列表中的所有元素及其增加的计数器

Array A[][] = [[8,1],[5,1]]
Array B[][] = [[2,1],[8,1]]

合并完成后我应该得到一个这样的数组

Array C[][] = [[2,1],[8,2],[8,2],[5,1]]

元素的排列不是必须的。

根据读数,Mergesort 采用O(nlogn) 来合并两个列表,但是我目前遇到了我的绑定问题。任何伪代码视觉效果将不胜感激。

【问题讨论】:

  • 你确定这是预期的输出吗? 8 出现两次,每次计数为 2?
  • 这就是我想要的方式。最初,我想在合并两个数组后缩小数组并增加元素的计数,但是我认为这可能会超过我的 O(nlogn) 限制。所以我会有 C[][]= [[2,1],[8,2],[5,1]]
  • 那么你需要哪个输出,打包还是解包?算法可能会有很大的不同。
  • @IuriCovalisin unpacked 是我的首选

标签: c++ arrays big-o


【解决方案1】:

我很喜欢Stepanov's Efficient Programming,虽然它们很慢。在第 6 节和第 7 节中(如果我没记错的话),他讨论了算法 add_to_counter()reduce_counter()。当然,这两种算法都是微不足道的,但可以用来实现非递归合并排序而不需要太多努力。唯一可能不明显的见解是组合操作可以将两个元素简化为一个序列,而不仅仅是一个元素。要就地执行操作,您实际上需要使用合适的类来存储迭代器(即数组中的指针)来表示数组的部分视图。

我还没有看过会话 7 之后的会话(实际上甚至还没有观看完整的会话 7),但我完全希望他实际上展示了如何使用会话 7 中生成的counter 来实现,例如,合并排序。当然,merge-sort 的运行时复杂度是O(n ln n),当使用计数器方法时,它将使用O(ln n) 辅助空间。

【讨论】:

    【解决方案2】:

    需要两倍内存的简单算法是对两个输入进行排序 (O(n log n)),然后从两个列表的头部依次选择元素并进行合并 (O(n))。总成本将是 O(n log n) 和 O(n) 额外内存(两个输入中最小的额外大小)

    【讨论】:

    • 这取决于您使用的确切类型。考虑它是对的向量:std::vector<std::pair<int,int>>,然后您可以通过直接调用std::sort(v.begin(),v.end()) 来对O(n log n) 部分进行排序。这同样适用于一对数组,但可能不适用于数组数组。
    【解决方案3】:

    这是我的基于桶计数的算法

    时间复杂度:O(n)

    内存复杂度:O(max),其中 max 是数组中的最大元素

    输出: [8,2][5,1][2,1][8,2]

    代码:

    #include <iostream>
    #include <vector>
    #include <iterator>
    
    int &refreshCount(std::vector<int> &counters, int in) {
        if((counters.size() - 1) < in) {
            counters.resize(in + 1);
        }
        return ++counters[in];
    }
    
    void copyWithCounts(std::vector<std::pair<int, int> >::iterator it,
                        std::vector<std::pair<int, int> >::iterator end,
                        std::vector<int> &counters,
                        std::vector<std::pair<int, int&> > &result
                        ) {
        while(it != end) {
            int &count = refreshCount(counters, (*it).first);
            std::pair<int, int&> element((*it).first, count);
            result.push_back(element);
            ++it;
        }
    }
    
    void countingMerge(std::vector<std::pair<int, int> > &array1,
                       std::vector<std::pair<int, int> > &array2,
                       std::vector<std::pair<int, int&> > &result) {
        auto array1It = array1.begin();
        auto array1End = array1.end();
        auto array2It = array2.begin();
        auto array2End = array2.end();
    
        std::vector<int> counters = {0};
    
        copyWithCounts(array1It, array1End, counters, result);
        copyWithCounts(array2It, array2End, counters, result);
    }
    
    int main()
    {
        std::vector<std::pair<int, int> > array1 = {{8, 1}, {5, 1}};
        std::vector<std::pair<int, int> > array2 = {{2, 1}, {8, 1}};
    
        std::vector<std::pair<int, int&> > result;
        countingMerge(array1, array2, result);
    
        for(auto it = result.begin(); it != result.end(); ++it) {
            std::cout << "[" << (*it).first << "," << (*it).second << "] ";
        }
    
        return 0;
    }
    

    简短说明: 因为你提到,最后的安排是不必要的,我做了简单的合并(没有排序,谁问排序?)计数,其中结果包含对计数器的引用,所以不需要遍历数组来更新计数器。

    【讨论】:

      【解决方案4】:

      您可以编写一个算法来合并它们,方法是按顺序遍历两个序列,并在适当的地方插入。

      我在这里选择了一个(看起来更贴切的)数据结构:std::map&lt;Value, Occurence&gt;:

      #include <map>
      using namespace std;
      
      using Value     = int;
      using Occurence = unsigned;
      using Histo     = map<Value, Occurence>;
      

      如果您坚持连续存储,boost::flat_map&lt;&gt; 应该是您的朋友(并且可以直接替代)。

      算法(用您的输入进行测试,阅读 cmets 进行解释):

      void MergeInto(Histo& target, Histo const& other)
      {
          auto left_it  = begin(target), left_end  = end(target);
          auto right_it = begin(other),  right_end = end(other);
          auto const& cmp = target.value_comp();
      
          while (right_it != right_end)
          {
              if ((left_it == left_end) || cmp(*right_it, *left_it))
              {
                  // insert at left_it
                  target.insert(left_it, *right_it);
                  ++right_it; // and carry on
              } else if (cmp(*left_it, *right_it))
              {
                  ++left_it; // keep left_it first, so increment it
              } else
              {
                  // keys match!
                  left_it->second += right_it->second;
                  ++left_it;
                  ++right_it;
              }
          }
      }
      

      这真的很简单!

      一个测试程序:查看 Live On Coliru

      #include <iostream>
      
      // for debug output
      static inline std::ostream& operator<<(std::ostream& os, Histo::value_type const& v) { return os << "{" << v.first << "," << v.second << "}"; }
      static inline std::ostream& operator<<(std::ostream& os, Histo const& v) { for (auto& el : v) os << el << " "; return os; }
      //
      
      int main(int argc, char *argv[])
      {
          Histo A { { 8, 1 }, { 5, 1 } };
          Histo B { { 2, 1 }, { 8, 1 } };
      
          std::cout << "A: " << A << "\n";
          std::cout << "B: " << B << "\n";
      
          MergeInto(A, B);
          std::cout << "merged: " << A << "\n";
      }
      

      印刷:

      A: {5,1} {8,1} 
      B: {2,1} {8,1} 
      merged: {2,1} {5,1} {8,2} 
      

      如果您真的想合并到一个新对象中,您可以稍微调整一下界面 (C):

      // convenience
      Histo Merge(Histo const& left, Histo const& right)
      {
          auto copy(left);
          MergeInto(copy, right);
          return copy;
      }
      

      现在你可以写了

      Histo A { { 8, 1 }, { 5, 1 } };
      Histo B { { 2, 1 }, { 8, 1 } };
      auto C = Merge(A, B);
      

      看到 Live on Coliru, too

      【讨论】:

      • 添加了一个不修改A的示例
      • 你能简单回顾一下第一个if条件吗?
      • @Masterminder:如果 (a) 我们已经在左侧直方图的末尾,则需要将右侧的元素插入左侧直方图 (b) 右侧元素的键在左侧直方图中当前位置的项之前排序。
      • 我担心算法不正确。细节我没细说,不过没有测试right_it是结尾。
      • @DavidRodríguez-dribeas 感谢您的提醒。我认为这可以解决它(以及两个 Coliru 链接)? 编辑:现在测试更多案例coliru.stacked-crooked.com/a/13489a95b97d6111(这些确实与未修复的代码有问题)。
      猜你喜欢
      • 2021-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多