【问题标题】:Efficient way to get middle (median) of an std::set?获得std :: set中间(中位数)的有效方法?
【发布时间】:2018-05-02 17:54:10
【问题描述】:

std::set 是一个排序树。它提供了beginend 方法,因此我可以获得最小值和最大值以及lower_boundupper_bound 进行二分搜索。但是,如果我想让迭代器指向中间元素(或者如果那里有偶数个元素,则其中之一)怎么办?

有没有一种有效的方法(O(log(size)) 而不是O(size))来做到这一点?

{1} => 1
{1,2} => 1 or 2
{1,2,3} => 2
{1,2,3,4} => 2 or 3 (but in the same direction from middle as for {1,2})
{1,312,10000,14000,152333} => 10000

PS:Same question in Russian.

【问题讨论】:

  • 排序二叉树可能并且通常是 std::set 的实现细节,但这不是必需的。如果您需要排序数组或二叉树,那么最好使用您需要的。
  • @ÖöTiib,我需要动态插入元素并获得集合的中间位置。排序的数组/向量将导致插入为O(n),但我希望插入和查询都可以工作O(lb(n))。我知道带有隐式键的 Decart 树允许这样做,但我不想实现它,并希望 std::set 足以实现这一点。
  • @Qwertiy 在大多数用例中,由于缓存局部性,插入向量会非常快。 std::set 和链表一样,使用指向分散在各处的子元素的指针,因此在很多情况下它可能会比较慢。阅读Why you should never, ever, EVER use linked-list in your code againBjarne Stroustrup: Why you should avoid Linked ListsAre lists evil?
  • 您真的需要排序元素还是只需要最小、最大和中值?在后一种情况下,请考虑使用std::nth_elementstd::vector
  • @DDrmmr,我只需要中等,但需要对数来获得它,而不是完整扫描。目前我认为保留对应迭代器的想法是最好的。

标签: c++ stl set median


【解决方案1】:

根据您插入/删除项目与查找中间/中位数的频率,一种可能比显而易见的解决方案更有效的解决方案是为中间元素保留一个持久迭代器,并在您插入/删除项目时更新它放。有一堆需要处理的边缘情况(奇数与偶数项目,删除中间项目,空集等),但基本思想是当您插入一个小于当前中间项目的项目时,您的中间迭代器可能需要递减,而如果您插入更大的迭代器,则需要递增。删除则相反。

在查找时,这当然是 O(1),但它在每次插入/删除时也有本质上 O(1) 的成本,即 N 次插入后的 O(N),这需要在足够的时间摊销查找次数,使其比暴力破解更有效。

【讨论】:

    【解决方案2】:

    得到二叉搜索树的中间需要 O(size) 。您可以通过std::advance() 获取它,如下所示:

    std::set<int>::iterator it = s.begin();
    std::advance(it, s.size() / 2);
    

    【讨论】:

    • 我认为 Martin 的意思是 O(height),其中 平衡 二叉树的高度与树的大小成对数。
    • @chepner, nope, std::advance 在这种情况下只是调用 ++ 相应的次数。
    【解决方案3】:

    这个建议是纯粹的魔法,如果有一些重复的项目会失败

    根据您插入/删除项目与查找中间/中位数的频率,一种可能比显而易见的解决方案更有效的解决方案是为中间元素保留一个持久迭代器,并在您插入/删除项目时更新它放。有一堆需要处理的边缘情况(奇数与偶数项目,删除中间项目,空集等),但基本思想是当您插入一个小于当前中间项目的项目时,您的中间迭代器可能需要递减,而如果您插入更大的迭代器,则需要递增。删除则相反。

    建议

    1. 第一个建议是使用 std::multiset 而不是 std::set,这样当项目可以复制时它可以很好地工作
    2. 我的建议是使用 2 个多组来跟踪较小的药水和较大的药水并平衡它们之间的大小

    算法

    1。保持集合平衡,因此 size_of_small==size_of_big 或 size_of_small + 1 == size_of_big

    void balance(multiset<int> &small, multiset<int> &big)
    {
        while (true)
        {
            int ssmall = small.size();
            int sbig = big.size();
    
            if (ssmall == sbig || ssmall + 1 == sbig) break; // OK
    
            if (ssmall < sbig)
            {
                // big to small
                auto v = big.begin();
                small.emplace(*v);
                big.erase(v);
            }
            else 
            {
                // small to big
                auto v = small.end();
                --v;
                big.emplace(*v);
                small.erase(v);
            }
        }
    }
    

    2。如果集合是平衡的,则中项始终是大集合中的第一项

    auto medium = big.begin();
    cout << *medium << endl;
    

    3。添加新项目时要小心

    auto v = big.begin();
    if (v != big.end() && new_item > *v)
        big.emplace(new_item );
    else
        small.emplace(new_item );
    
    balance(small, big);
    

    复杂性解释

    • 找到中间值是 O(1)
    • 添加一个新项目需要 O(log n)
    • 你仍然可以在 O(log n) 中搜索一个项目,但你需要搜索 2 个集合

    【讨论】:

    • 添加是 O(log(n)) 而不是 O(n)。无论如何,保持中位数对我来说效果很好。
    • 对我来说,您似乎回答了问题“获得 std::multiset 中间(中位数)的有效方法?”因为std::set 不能fail if there are some duplicated items,因为根据定义它不能有这样的。我建议您创建有关std::multiset 的新问题并将此答案移到那里。 PS。 Mods 可以在问题之间移动答案而不会丢失分数。
    【解决方案4】:

    请注意std::set 不存储重复值。如果您插入以下值{1, 2, 3, 3, 3, 3, 3, 3, 3},您将检索到的中位数是2

    std::set<int>::iterator it = s.begin();
    std::advance(it, s.size() / 2);
    int median = *it;
    

    如果您想在考虑中位数时包含重复项,您可以使用std::multiset{1, 2, 3, 3, 3, 3, 3, 3, 3} 中位数将是3):

    std::multiset<int>::iterator it = s.begin();
    std::advance(it, s.size() / 2);
    int median = *it;
    

    如果您希望对数据进行排序的唯一原因是获取中位数,那么在我看来,您最好使用普通的 std::vector + std::sort

    通过大量测试样本和多次迭代,我用std::vectorstd::sort 在5 秒内完成了测试,用std::setstd::multiset 在13 到15 秒内完成了测试。您的里程数可能会因您拥有的重复值的大小和数量而异。

    【讨论】:

    • 它与我的问题有什么关系?
    • 我认为在大多数用例中,当您需要中位数时,您希望从完整的数据集而不是唯一值的子集中获取它。我犯了这个错误,所以我想我会在std::multiset 中添加一个提及,以防止像我这样的人犯同样的错误。但你是对的,它没有直接回答这个问题。但是辅助答案中的更多信息不会受到伤害吗?
    【解决方案5】:

    正如@pmdj 所说,我们使用迭代器来跟踪中间元素。下面是下面的代码实现:

    class RollingMedian {
    public:
    multiset<int> order;
    multiset<int>::iterator it;
    RollingMedian() {
    }
    
    void add(int val) {
        order.insert(val);
        if (order.size() == 1) {
            it = order.begin();
        } else {
            if (val < *it and order.size() % 2 == 0) {
                --it;
            }
            if (val >= *it and order.size() % 2 != 0) {
                ++it;
            }
        }
    }
    
    double median() {
        if (order.size() % 2 != 0) {
            return double(*it);
        } else {
            auto one = *it, two = *next(it);
            return double(one + two) / 2.0;
        }
    }  };
    

    随意复制和使用此代码的任何部分。此外,如果没有重复,您可以使用 set 代替 multiset。

    【讨论】:

      【解决方案6】:

      如果您的数据是静态的,您可以预先计算它而不插入新元素 - 使用 vector 、对其进行排序以及仅通过 O(1) 中的索引访问中位数会更简单

      vector<int> data;
      // fill data
      std::sort(data.begin(), data.end());
      auto median = data[data.size() / 2];
      

      【讨论】:

      • 但是你不能在 O(1) 中得到中位数
      猜你喜欢
      • 1970-01-01
      • 2011-06-10
      • 1970-01-01
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-24
      相关资源
      最近更新 更多