web-dev-qa-db-ja.com

std :: setの中央(中央値)を取得する効率的な方法?

_std::set_はソートされたツリーです。これはbeginおよびendメソッドを提供するので、最小値と最大値、およびバイナリ検索用の_lower_bound_と_upper_bound_を取得できます。しかし、イテレータが中央の要素(または要素が偶数の場合はそのうちの1つ)を指すようにするにはどうすればよいですか?

それを行うための効率的な方法はありますか(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: ロシア語で同じ質問です

14
Qwertiy

アイテムを挿入/削除する頻度と中央/中央値を検索する頻度に応じて、明白なソリューションよりもおそらくより効率的なソリューションは、中間の要素への永続イテレータを保持し、セットにアイテムを挿入/削除するたびにそれを更新することです。処理が必要なEdgeケースは多数あります(奇数対偶数のアイテム、中間アイテムの削除、空のセットなど)。ただし、基本的な考え方は、現在の中間アイテムよりも小さいアイテムを挿入すると、 、中間のイテレータはデクリメントが必要な場合がありますが、大きなイテレータを挿入する場合は、インクリメントする必要があります。これは、削除の別の方法です。

ルックアップ時には、これはもちろんO(1)ですが、本質的にO(1)挿入/削除ごとのコスト、つまりO(N) N挿入後。ブルートフォースよりも効率的にするには、十分な数のルックアップにわたって償却する必要があります。

18
pmdj

O(size))になり、バイナリ検索ツリーの真ん中を取得します。次のようにstd::advance()を使用して取得できます。

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

この提案は純粋な魔法であり、重複するアイテムがある場合は失敗します

アイテムを挿入/削除する頻度と中央/中央値を検索する頻度に応じて、明白なソリューションよりもおそらくより効率的なソリューションは、中間の要素への永続イテレータを保持し、セットにアイテムを挿入/削除するたびにそれを更新することです。処理が必要なEdgeケースは多数あります(奇数対偶数のアイテム、中間アイテムの削除、空のセットなど)。ただし、基本的な考え方は、現在の中間アイテムよりも小さいアイテムを挿入すると、 、中間のイテレータはデクリメントが必要な場合がありますが、大きなイテレータを挿入する場合は、インクリメントする必要があります。これは、削除の別の方法です。

提案

  1. 最初の提案は、std :: setの代わりにstd :: multisetを使用することです。これにより、アイテムが複製される可能性がある場合にうまく機能します。
  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セットを検索する必要があります
6
Clark

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::setまたはstd::multisetで13〜15秒でテストを完了しました。あなたの走行距離は、あなたが持っている重複する値のサイズと数によって異なります。

2
Norgannon