【问题标题】:Optimized way to partition a class of objects based on an attribute in C++基于 C++ 中的属性划分对象类的优化方法
【发布时间】:2017-07-26 19:25:14
【问题描述】:

我有一个包含数百万个项目的类,每个项目都有一个 int 类型的 label。我需要根据它们相似的标签对项目进行分区,所以最后我返回一个vector<MyClass>。首先,我根据标签对所有项目进行排序。然后,在 for 循环中,我将每个标签值与前一个值进行比较,如果相同,我将其存储在 myclass_temp 中,直到 label != previous_label。如果label != previous_label 我将这个myclass_temp 添加到vector<MyClass>,然后我删除myclass_temp。我认为代码是不言自明的。 该程序运行良好,但速度很慢,有没有更好的方法来加速它?我相信因为我一开始就对项目进行排序,所以应该有一种更快的方法来简单地对具有相似标签的项目进行分区。

第二个问题是如何计算此算法的 O 分数以及任何建议的更快解决方案? 请随时更正我的代码。

 vector <MyClass> PartitionByLabels(MyClass &myclass){

    /// sort MyClass items based on label number
    printf ("Sorting items by label number... \n");
    std::sort(myclass.begin(), myclass.end(), compare_labels);

    vector <MyClass> myClasses_vec;
    MyClass myclass_temp;

    int previous_label=0, label=0;
    int total_items;

    /// partition myclass items based on similar labels
    for (int i=0; i < myclass.size(); i++){

        label = myclass[i].label;
        if (label == previous_label){
            myclass_temp.push_back(myclass[i]);
            previous_label = label;

            /// add the last similar items
            if (i == myclass.size()-1){
                myClasses_vec.push_back(myclass_temp);
                total_items +=myclass_temp.size();
            }
        } else{
            myClasses_vec.push_back(myclass_temp);
            total_items +=myclass_temp.size();

            myclass_temp.EraseItems();
            myclass_temp.push_back(myclass[i]);
            previous_label = label;
        }
    }

    printf("Total number of items: %d \n", total_items);
    return myClasses_vec;
}

【问题讨论】:

标签: c++ sorting optimization big-o partitioning


【解决方案1】:

这个算法应该可以做到。我删除了模板,以便更轻松地检查 Godbolt。

应该很容易放回去。

这个方法的 O 分数是 std::sort - O(N.log(N))

#include <vector>
#include <algorithm>
#include <string>
#include <iterator>

struct thing
{
    std::string label;
    std::string value;
};

using MyClass = std::vector<thing>;
using Partitions = std::vector<MyClass>;

auto compare_labels = [](thing const& l, thing const& r) {
    return l.label < r.label;
};

// pass by value - we need a copy anyway and we might get copy elision
Partitions PartitionByLabels(MyClass myclass){

    /// sort MyClass items based on label number
    std::sort(myclass.begin(), myclass.end(), compare_labels);

    Partitions result;

    auto first = myclass.begin();
    auto last = myclass.end();

    // because the range is sorted, we can partition it in linear time.
    // choosing the correct algorithm is always the best optimisation
    while (first != last) 
    {
        auto next = std::find_if(first, last, [&first](auto const& x) { return x.label != first->label; });

        // let's move the items - that should speed things up a little
        // this is safe because we took a copy
        result.push_back(MyClass(std::make_move_iterator(first), 
                                 std::make_move_iterator(next)));
        first = next;
    }

    return result;
}

我们当然可以用无序地图做得更好,如果

  • 标签是可散列且可相等比较的

  • 我们不需要对输出进行排序(如果这样做,我们将使用多映射)

 

此方法的 O-score 是线性时间 O(N)

#include <vector>
#include <algorithm>
#include <string>
#include <iterator>
#include <unordered_map>

struct thing
{
    std::string label;
    std::string value;
};

using MyClass = std::vector<thing>;
using Partitions = std::vector<MyClass>;

// pass by value - we need a copy anyway and we might get copy elision
Partitions PartitionByLabels(MyClass const& myclass){

    using object_type = MyClass::value_type;
    using label_type = decltype(std::declval<object_type>().label);
    using value_type = decltype(std::declval<object_type>().value);

    std::unordered_multimap<label_type, value_type> inter;
    for(auto&& x : myclass) {
        inter.emplace(x.label, x.value);
    }

    Partitions result;

    auto first = inter.begin();
    auto last = inter.end();

    while (first != last) 
    {
        auto range = inter.equal_range(first->first);
        MyClass tmp;
        tmp.reserve(std::distance(range.first, range.second));
        for (auto i = range.first ; i != range.second ; ++i) {
            tmp.push_back(object_type{i->first, std::move(i->second)});
        }
        result.push_back(std::move(tmp));
        first = range.second;
    }

    return result;
}

【讨论】:

  • 感谢您的回复。 std::make_move_iterator 是做什么的?为什么我们需要它?
  • @Bruce 它创建一个迭代器,在它取消引用的对象上调用std::move。这意味着只要你遵守了 0、3 或 5 的规则,那么在容器之间移动物体会变得非常快。但是,您可以忽略该部分,因为它只是一个优化。真正的重点是我们已将整理算法简化为单遍。
  • 好的,谢谢。那么您的意思是使用第二种算法,我不需要事先对数据进行排序?另一个问题是,我是否需要将auto 替换为myclass::iterator 类型?为什么用auto,有什么区别吗?
  • @Bruce 两种算法都分为两部分。第一个和你的一样,除了第二部分我们能够将它转换为线性时间,这与排序的成本相形见绌。第二个使用 unordered_map 查找是恒定时间的事实,并且插入平均为 O(1),这是一个巨大的胜利。
  • 那么auto 我应该将它们更改为 MyClass::iterator 吗?我的意思是使用auto而不是真实类型的代码有什么不同吗?
【解决方案2】:

为什么不创建一个从整数到向量的映射,遍历原始向量一次,将每个MyClass 对象添加到TheMap[myclass[i].label]?您的平均运行时间从 f(n + n*log(n)) 变为 f(n)

【讨论】:

  • 所以你的意思是我制作了一个类似 map 的地图并遍历所有项目并将 myclass 对象和标签插入到地图中。那么每张地图代表一个单一的项目,对吧?它没有给我类似标签的集合。
  • map> - 当您遍历对象时,您只需将每个对象推到 map_of_my_class[instance.label] 的向量后面。如果返回地图对您不起作用,并且您仍想返回向量向量,则可以遍历地图中的键以生成该结构,但这仍然比您现在所做的要快得多,尽管它有 f(n+n) 运行时的风险。
猜你喜欢
  • 2010-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-16
  • 1970-01-01
  • 1970-01-01
  • 2012-12-17
  • 2018-02-06
相关资源
最近更新 更多