用不同的键对std :: map进行排序?

时间:2009-11-24 06:33:57

标签: c++ stl

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

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

(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

由于数据集完全未排序,我首先需要聚合键来计算它们并总结大小。然后我需要通过总大小来求助该数据结构以产生最终输出。这是我用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'ing更快并删除此处使用的每个节点。这是一个合理的假设还是你认为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,我想我有答案。谢谢大家!

9 个答案:

答案 0 :(得分:6)

multimap<>可能会对您有所帮助。

数据集中有多个条目使用相同的密钥。
multimap<>可以处理重复的密钥,地图则不能。

数据集中每个密钥有多少
multimap<>::count()获取一个键并返回匹配元素的数量。

对于每个键,它们的总大小是什么
multimap<>::equal_range()接受一个键并返回std::pair< multimap<>::iterator, multimap<>::iterator >,其中第一个迭代器是匹配键的第一个元素,第二个是最后一个。他们可以按照他们开始和结束的想法进行迭代。因此,使用这些将是一个简单的循环来计算每个键的总大小。

显然,它并不完全符合您的需求,如果您要在大型数据集上运行,也许您可​​以获得实现自定义容器的一些有价值的性能。祝你好运!

答案 1 :(得分:2)

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

第一个是使用multimap容器来保存值,并迭代地使用equal_range来生成输出。在这里你可以快速插入,但输出速度慢。

第二个选项是使用带有成员函数的boost::multi_index作为索引来计算插入时的sum和count值。在这里你会慢速插入,但输出速度很快。

答案 2 :(得分:2)

以下是使用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

答案 3 :(得分:2)

如果您可以使用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。您需要稍微修改它,以便计算已分配和解除分配的数字字节。

答案 4 :(得分:2)

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

答案 5 :(得分:1)

这是您解决方案的一个小改进。插入一个新的时间更短并避免第二个键搜索(请注意它依赖于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会得到更好的结果,因为您似乎不需要按键排序(由地图使用)。

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

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

stl::map<Key, Data, Compare, Alloc>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;

答案 8 :(得分:0)

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

实际上,地图插入会触发重新平衡,所以它比列表插入更糟糕。