【问题标题】:Sorting an std::map by a different key?用不同的键对 std::map 进行排序?
【发布时间】:2009-11-24 06:33:57
【问题描述】:

我正在尝试使用 STL 来解决以下问题(如果不需要,我不想实现自己的数据结构)。我想出了一个可行的实现,但我希望有更快的方法......或者我有什么代码是最好的方法?

我有一个大型数据集,其中每个条目包含两个项目:一个键和一个大小。数据集中有多个条目具有相同的键。我需要知道的是:对于每个键,数据集中有多少个键,每个键的总大小是多少。例如,给定这个数据集(key, size):

(1, 3)
(3, 27)
(7, 7)
(3, 2)
(1, 1)

我想生成这个输出,按大小升序排序:

Key 1:  Size 4, Count 2
Key 7:  Size 7, Count 1
Key 3:  Size 29, Count 2

由于数据集是完全未排序的,所以我首先需要对keys进行聚合统计并总结大小。然后我需要通过总大小来使用该数据结构来生成最终输出。这是我使用 std::map 和 std::vector 完成任务的代码:

struct Node
{
    int Size;
    int Count;

    Node()
        : Size(0), Count(0)
    {
    }

    Node(int size)
        : Size(size), Count(1)
    {
    }
};

void map_insert(std::map<int, Node> &map, int key, int size)
{
    std::map<int, Node>::iterator itr = map.find(key);

    if (itr != map.end())
    {
        itr->second.Count++;
        itr->second.Size += size;
    }
    else
    {
        map[key] = Node(size);
    }
}

bool compare(const std::pair<int, Node> &a1, const std::pair<int, Node> &a2)
{
    return a1.second.Size < a2.second.Size;
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::map<int, Node> _map;

    map_insert(_map, 1, 3);
    map_insert(_map, 3, 27);
    map_insert(_map, 7, 7);
    map_insert(_map, 3, 2);
    map_insert(_map, 1, 1);

    std::vector<std::pair<int, Node>> v(_map.begin(), _map.end());
    std::sort(v.begin(), v.end(), compare);

    return 0;
}

减去输出代码,这会产生正确的排序。我讨厌使用两个单独的数据结构,但似乎没有一种方法可以根据不同的键“重新排序”树。我可以避免这里有什么严重的低效率吗?谁能想到更好的方法来做到这一点?

请注意,我假设使用 Node 实例(而不是 Node 指针)将比新建和删除此处使用的每个节点更快。这是一个合理的假设,还是您认为 new/delete 会比复制这些小型结构更快?

编辑:有趣的是,我从来不知道 multimap,但是使用下面提供的实现(感谢 Naveen),看起来 Multimap 的性能更差。 (注意我的意图是快速实现,内存不是问题,我应该指出这一点。)使用这个实现:

class Timer
{
public:
    Timer()
        : mStart(0)
    {
    }

    void Start()
    {
        mStart = std::clock();
    }

    double Mark()
    {
        std::clock_t curr = std::clock();
        double f = (curr - mStart)/((double)CLOCKS_PER_SEC);
        mStart = curr;
        return f;
    }

private:
    std::clock_t mStart;
};

struct Node
{
    int Size;
    int Count;

    Node()
        : Size(0), Count(0)
    {
    }

    Node(int size)
        : Size(size), Count(1)
    {
    }


};

void map_insert(std::map<int, Node> &map, int key, int size)
{
    std::map<int, Node>::iterator itr = map.find(key);

    if (itr != map.end())
    {
        itr->second.Count++;
        itr->second.Size += size;
    }
    else
    {
        map[key] = Node(size);
    }
}

bool compare(const std::pair<int, Node> &a1, const std::pair<int, Node> &a2)
{
    return a1.second.Size < a2.second.Size;
}

int make_size(int i, int size_max)
{
    return (7 * i) % size_max;
}

int make_key(int i, int key_max)
{
    return (11 * i) % key_max;
}

void first_impl(int max, int size_max, int key_max)
{
    std::cout << "first_impl:" << std::endl;
    double total = 0;
    double curr = 0;
    Timer t;
    t.Start();

    {
        std::map<int, Node> _map;

        for (int i = 0; i < max; ++i)
            map_insert(_map, make_key(i, key_max), make_size(i, size_max));

        total += curr = t.Mark();
        std::cout << "\tinsert: " << curr << std::endl;

        std::vector<std::pair<int, Node>> v(_map.begin(), _map.end());

        total += curr = t.Mark();
        std::cout << "\tcreate: " << curr << std::endl;

        std::sort(v.begin(), v.end(), compare);

        total += curr = t.Mark();
        std::cout << "\tsort: " << curr << std::endl;
    }

    total += curr = t.Mark();
    std::cout << "\tcleanup: " << curr << std::endl;

    std::cout << "\ttotal: " << total << std::endl;
}

void second_impl(int max, int size_max, int key_max)
{
    std::cout << "second_impl:" << std::endl;
    double total = 0;
    double curr = 0;
    Timer t;
    t.Start();

    {
        std::map<int, Node> res;
        typedef std::multimap<int, int> MultiMap;
        MultiMap mMap;

        for (int i = 0; i < max; ++i)
            mMap.insert(std::make_pair(make_key(i, key_max), make_size(i, size_max)));

        total += curr = t.Mark();
        std::cout << "\tinsert: " << curr << std::endl;

        std::multimap<int, int>::iterator iter = mMap.begin();
        std::multimap<int, int>::iterator endIter = mMap.end();
        for(; iter != endIter; ++iter)
        {
            int val = iter->first;
            if(res.find(val) != res.end())
            {
                    continue;
            }

            std::pair<MultiMap::iterator, MultiMap::iterator> iterPair = mMap.equal_range(val);
            Node n;
            n.Size = val;
            n.Count = mMap.count(val);

            int size = 0;
            for(; iterPair.first != iterPair.second; ++iterPair.first)
            {
                    size += iterPair.first->second;                 
            }

            res[size] = n;
        }
        total += curr = t.Mark();
        std::cout << "\tsort: " << curr << std::endl;
    }

    total += curr = t.Mark();
    std::cout << "\tcleanup: " << curr << std::endl;

    std::cout << "\ttotal: " << total << std::endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    const int size_max = 31;
    const int key_max = 1019;
    const int max = 1000000;
    first_impl(max, size_max, key_max);
    second_impl(max, size_max, key_max);

    return 0;
}

结果如下所示:

first_impl:
        insert: 0.094
        create: 0
        sort: 0
        cleanup: 0
        total: 0.094
second_impl:
        insert: 1.653
        sort: 46.894
        cleanup: 66.081
        total: 114.628

第二种实现显然更慢。看起来键的总数低于项目总数(唯一键的总数约为 1000 代表我的数据集)使得 std::map 在这里获胜因为它很快就达到了不需要更多节点的稳定状态。在我进行二次调查之前,我完全忽略了这个事实。

看起来我的原始实现比 multimap 更好,并且由于我不愿意依赖 Boost,我想我有我的答案。谢谢大家!

【问题讨论】:

  • 再看一下使用 multimap 的代码,我并不完全相信这个实现是正确的,但是事实上插入 multimap 的时间比完全计算我的时间要长用map/vector/sort寻找,这似乎仍然很清楚,原来的实现就是我要使用的。
  • 为了更快的速度,你可以查看 hash_map(或 tr1:unordered_map)。
  • 是的,迭代非常慢,所以 MultiMap 已经出来了,而且它在技术上是不正确的,因为您不需要保留大小和计数,只需将它们总结起来,就像您正确完成的那样。 STL 无序映射的哈希表大约是映射的 3 倍,但对于您的适度数据大小,不值得麻烦。一个有趣的想法是使用 1 个映射来汇总数据,并使用第 2 个映射对其进行排序 - 并不是说​​ qsort() 在汇总数据后就不能正常工作,但是第 2 个映射会在实时。

标签: c++ stl


【解决方案1】:

multimap&lt;&gt; 可以帮助您。

数据集中有多个条目具有相同的key。
multimap&lt;&gt;可以处理重复的key,map不能。

数据集中每个键有多少个
multimap&lt;&gt;::count() 接受一个键并返回匹配元素的数量。

对于每个键,它们的总大小是多少
multimap&lt;&gt;::equal_range() 接受一个键并返回一个std::pair&lt; multimap&lt;&gt;::iterator, multimap&lt;&gt;::iterator &gt;,其中第一个迭代器是第一个匹配键的元素,第二个是最后。它们可以像认为它们是开始和结束一样被迭代。因此,使用它们将是一个简单的循环来计算每个键的总大小。

显然,它并不完全满足您的需求,如果您要在大型数据集上进行操作,也许您会在实现自定义容器时获得一些有价值的性能。祝你好运!

【讨论】:

  • 使用下面的 Naveen 代码看起来好像 map/vector/sort 更快,因为与数据集中的条目总数相比,我的唯一键总数较低。针对多地图实现进行测试让我更有信心,我想出的原始解决方案应该足以满足我的目的。感谢您的信息!
  • 使用 MultiMap 的任何答案都是错误的。无需保留离散数据以供以后求和。这是错误的,因为它消耗了大量资源来满足不存在的需求。 OPs 方法是正确的。
  • 我同意多地图是浪费的,但不会说它错了。多地图版本更容易实现并且可能有更少的错误。易于实施是一种完全有效的交易,可以降低性能。这完全取决于您的需求。
【解决方案2】:

根据您需要的复杂程度,您有两种选择。

第一个是使用multimap 容器来保存值并迭代使用equal_range 来生成输出。在这里,您将获得快速插入,但输出缓慢。

第二个选项是使用boost::multi_index 和成员函数作为索引,在插入时计算总和和计数值。在这里你会得到缓慢的插入,但快速的输出。

【讨论】:

    【解决方案3】:

    std::map 是一个关联容器,因此映射将按键排序。在这里,由于您使用的是重复键,因此 multimap 将解决您的目的。

    【讨论】:

      【解决方案4】:

      以下是使用std::multimapstd::map 实现此功能的示例代码,然后使用equal_range 和这些类的count 方法。

          std::map<int, Node> res;
          typedef std::multimap<int, int> MultiMap;
          MultiMap mMap;
          mMap.insert(std::make_pair(1,3));
          mMap.insert(std::make_pair(3,27));
          mMap.insert(std::make_pair(7,7));
          mMap.insert(std::make_pair(3,2));
          mMap.insert(std::make_pair(1,1));
      
      
      std::multimap<int, int>::iterator iter = mMap.begin();
      std::multimap<int, int>::iterator endIter = mMap.end();
      while( iter != endIter)
      {
          int val = iter->first;
          std::pair<MultiMap::iterator, MultiMap::iterator> iterPair = mMap.equal_range(val);
          Node n;
          n.val = val;
          n.count = mMap.count(val);
      
          int size = 0;
          for(; iterPair.first != iterPair.second; ++iterPair.first)
          {
              size += iterPair.first->second;         
          }
      
          res[size] = n;
      
          iter = iterPair.second;
      }
      

      节点定义为:

      struct Node
      {
          int val;
          int count;
      
          Node() : val(0), count(0)
          {
          }
      };
      

      请注意,结果映射的键是size,而不是count

      【讨论】:

      • 感谢您的代码。这确实有助于缩小 map/vector/sort 在我的特定情况下表现更好的事实。我会支持你,但我没有代表。 =/
      【解决方案5】:

      如果您可以使用 Boost,则可以使用 Boost.Multiindex。它允许您拥有一个具有两个有序索引的容器(在您的示例中,一个键索引和一个大小索引)。至于内存效率或效率低下,据说在 Boost.Multiindex ordered indices node compression 中实现了,结果是:

      在大多数平台上,有序索引节点标头的大小已减少 25%

      还请查看此示例及其结果:Results for 2 ordered indices。因此,即使您只是将 boost::multiindex 与有序索引一起使用,它使用的内存也比 MS VS 8.0 或 gcc 中的 std::multiset 少。

      至于您的解决方案,我认为您可以预期 Boost.Multiindex 与您的实现相比将使用更少的内存。但是,如果您想准确比较两个解决方案,您可以这样做。编写您自己的计数分配器,将其添加到您的容器中并找出已使用的内存量。然后使用 Boost.Multiindex 和计数分配器做同样的事情。这是example of allocator。您需要稍微修改它以计算已分配和释放的字节数。

      【讨论】:

        【解决方案6】:

        这里对您的解决方案进行了小幅改进。插入新的时更短并避免第二次键搜索(注意它依赖于 Node 构造函数的 0)

        void map_insert(std::map<int, Node> &map, int key, int size) {
            Node & n = map[key];
            ++n.Count;
            n.Size+=size;
        }
        

        但最佳方式可能取决于您的键范围。如果总是很小(比如说 1..1000),简单的向量是最好的选择。如果更大,hash_map 会提供更好的结果,因为您似乎不需要键排序(由 map 使用)。

        我进行了测试,它似乎为您的约 1000 个密钥情况提供了合理的改进,但这也取决于您的密钥分配。您只需将std::map 替换为std::hash_map,然后修复标题内容。但是,std::hash_map 可能存在一些可移植性问题。不过,您仍然可以编写自己的散列系统(甚至可以根据您的密钥分配对其进行调整)。

        编辑:unordered_map 似乎是hash_map 的未来标准。至少,如果修复了 gcc 4.3 上的弃用警告。

        【讨论】:

        • 啊,好电话。我错过了更好的插入模式和 hash_map/unordered_map。谢谢!
        • 阅读 OP 的要求.....“我想生成这个输出,按大小升序排序:”。排除了使用任何散列的东西。
        【解决方案7】:

        需要注意的一点:至少在 VS2008 中,当您分配 map[key] = Node(size); 时,您实际上最终构建了三个独立的 Node 实例。结果是,只有您声明的一个在堆栈上创建 - 其他两个在堆上创建,因此通过使用此版本,您实际上会产生两倍的开销,如果您使用指针并承担删除的责任最后你的所有实例。

        【讨论】:

          【解决方案8】:

          stl::map&lt;Key, Data, Compare, Alloc&gt;Compare,只需要提供一个弱排序函数即可。

          struct Node
          {
              int Size;
              int Count;
          };
          
          bool compareNode(const Node& a, const Node& b) {
            return a.Size < b.Size;
          }
          
          stl::map<Node, stlstring, compareNode>  xxx;
          

          【讨论】:

            【解决方案9】:

            如果您只插入一次,那么多重映射将比简单地按向量中键的顺序排序要慢。重复插入到地图中相当于列表插入排序,而对向量进行排序可以使用快速排序。

            其实map插入会触发rebalances,所以比list插入更糟糕。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2018-09-05
              • 1970-01-01
              • 2017-12-24
              • 2013-06-27
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多