获得std :: set中间(中位数)的有效方法?

时间:2017-11-19 11:29:49

标签: c++ stl set median

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.

5 个答案:

答案 0 :(得分:16)

根据您插入/删除项目的频率与查找中间/中间值的比例,可能比明显的解决方案更有效的解决方案是将持久性迭代器保留到中间元素并在每次插入/删除项目时更新它组。有一堆边缘情况需要处理(奇数与偶数项目,删除中间项目,空集等),但基本的想法是当你插入一个小于当前的中间项,你的中间迭代器可能需要递减,而如果你插入一个更大的,你需要递增。这是删除的另一种方式。

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

答案 1 :(得分:6)

获得二叉搜索树的中间部分将是O(大小)。您可以使用std::advance()获取它,如下所示:

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

答案 2 :(得分: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(n)
  • 您仍然可以在O(log n)中搜索项目,但是您需要搜索2套

答案 3 :(得分:0)

请注意,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至15s内完成了测试。您的里程可能会有所不同,具体取决于您拥有的重复值的大小和数量。

答案 4 :(得分:0)

正如@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。