web-dev-qa-db-ja.com

整数のストリームからランニングメジアンを見つける

重複している可能性があります:
Cのローリングメジアンアルゴリズム

整数がデータストリームから読み取られると仮定します。これまでに読んだ要素の中央値を効率的に見つける。

私が読んだ解決策:有効中央値より小さい要素を表すために左側に最大ヒープを使い、有効中央値より大きい要素を表すために右側に最小ヒープを使うことができます。

入力要素を処理した後、ヒープ内の要素数は最大で1要素異なります。両方のヒープに同数の要素が含まれている場合、ヒープのルートデータの平均が有効中央値となります。ヒープのバランスが取れていない場合は、より多くの要素を含むヒープの根から有効中央値を選択します。

しかし、どのようにして最大ヒープと最小ヒープを構成するのでしょうか。つまり、ここで実効メジアンをどのようにして知るのでしょうか。 max-heapに1つの要素を挿入し、次にmin-heapに次の1つの要素を挿入するとします。すべての要素についても同様です。私がここで間違っているなら私を訂正してください。

210
Luv

ストリーミングデータからランニングメジアンを見つけるためのさまざまな解決策がいくつかあります。回答の最後にそれらについて簡単に説明します。

問題は、特定のソリューション(最大ヒープ/最小ヒープソリューション)の詳細、およびヒープベースのソリューションの仕組みについての説明です。

最初の2つの要素では、左側のmaxHeapに小さい方を追加し、右側のminHeapに大きい方を追加します。その後、ストリームデータを1つずつ処理します。

Step 1: Add next item to one of the heaps

   if next item is smaller than maxHeap root add it to maxHeap,
   else add it to minHeap

Step 2: Balance the heaps (after this step heaps will be either balanced or
   one of them will contain 1 more item)

   if number of elements in one of the heaps is greater than the other by
   more than 1, remove the root element from the one containing more elements and
   add to the other one

その後、いつでもあなたはこのように中央値を計算することができます:

   If the heaps contain equal amount of elements;
     median = (root of maxHeap + root of minHeap)/2
   Else
     median = root of the heap with more elements

それでは、答えの冒頭で約束したように、私は問題について一般的に話します。データのストリームからランニングメジアンを見つけることは難しい問題であり、メモリ制約を伴う厳密解を効率的に見つけることは一般的なケースではおそらく不可能です。一方、データに利用可能な特性がある場合は、効率的な特殊ソリューションを開発できます。たとえば、データが整数型であることがわかっている場合は、 カウンティングソート を使用できます。これにより、定数メモリ定数時間アルゴリズムが得られます。ヒープベースのソリューションは他のデータ型(double)にも使用できるため、より一般的なソリューションです。そして最後に、正確な中央値が必要ではなく、近似で十分な場合は、データの確率密度関数を推定し、それを使用して中央値を推定するだけで済みます。

364
Hakan Serce

一度にすべての項目をメモリに保持できない場合、この問題はさらに困難になります。ヒープソリューションでは、すべての要素を一度にメモリに保持する必要があります。これは、この問題のほとんどの実社会のアプリケーションでは不可能です。

代わりに、数字を見ながら、各整数を見た回数のcountを記録してください。 4バイト整数を仮定すると、それは2 ^ 32バケット、または最大2 ^ 33整数(各intのキーおよびカウント)、つまり2 ^ 35バイトまたは32GBです。あなたがキーを保存したり、0であるエントリを数える必要がないので(pythonのdefaultdictのように)、これはおそらくこれよりずっと少ないでしょう。これは新しい整数を挿入するのに一定の時間がかかります。

それからどの時点でも、中央値を見つけるために、ちょうどどの整数が中間の要素であるかを決定するためにカウントを使います。これには一定の時間がかかります(大きな定数ではありますが、それでもなお一定です)。

48
Andrew C

入力の分散が統計的に分布している(例えば正規、対数正規など)場合、リザーバサンプリングは任意に長い数のストリームからパーセンタイル/中央値を推定するための合理的な方法である。

int n = 0;  // Running count of elements observed so far  
#define SIZE 10000
int reservoir[SIZE];  

while(streamHasData())
{
  int x = readNumberFromStream();

  if (n < SIZE)
  {
       reservoir[n++] = x;
  }         
  else 
  {
      int p = random(++n); // Choose a random number 0 >= p < n
      if (p < SIZE)
      {
           reservoir[p] = x;
      }
  }
}

その場合、「貯水池」は、規模に関係なく、実行中の、統一された(公平な)すべての入力のサンプルです。中央値(または任意の百分位数)を見つけるのは、貯水池をソートして興味深い点をポーリングするという直接的な問題です。

リザーバは固定サイズなので、ソートは事実上O(1)と考えることができ、このメソッドは一定の時間とメモリ消費の両方で実行されます。

44

私が見つけたストリームのパーセンタイルを計算する最も効率的な方法は、P²アルゴリズムです: Raj Jain、Imrich Chlamtac:観測値を保存せずにクォンタイルとヒストグラムを動的に計算するためのP²アルゴリズムコミュニケーション。 ACM 28(10):1076-1085(1985)

このアルゴリズムは実装が簡単で、非常にうまく機能します。ただし、これは推定値であるため、留意してください。要約から:

中央値および他の分位数の動的計算のための発見的アルゴリズムが提案されています。推定値は、観測値が生成されると動的に生成されます。観測は保存されません。したがって、アルゴリズムには、観測の数に関係なく、非常に小さく固定されたストレージ要件があります。これにより、産業用コントローラーおよびレコーダーで使用できる分位チップに実装するのに最適です。このアルゴリズムは、ヒストグラムプロットにまで拡張されています。アルゴリズムの精度が分析されます。

28
Hellblazer

この問題はnの最近見た要素をメモリに保持するだけでよい厳密な解決策を持っています。それは速くてスケーラブルです。

indexable skiplist は、ソート順を維持しながら、任意の要素のO(ln n)の挿入、削除、およびインデックス検索をサポートします。 n番目に古いエントリを追跡する FIFOキュー と組み合わせると、解決策は簡単です。

class RunningMedian:
    'Fast running median with O(lg n) updates where n is the window size'

    def __init__(self, n, iterable):
        self.it = iter(iterable)
        self.queue = deque(islice(self.it, n))
        self.skiplist = IndexableSkiplist(n)
        for elem in self.queue:
            self.skiplist.insert(elem)

    def __iter__(self):
        queue = self.queue
        skiplist = self.skiplist
        midpoint = len(queue) // 2
        yield skiplist[midpoint]
        for newelem in self.it:
            oldelem = queue.popleft()
            skiplist.remove(oldelem)
            queue.append(newelem)
            skiplist.insert(newelem)
            yield skiplist[midpoint]

完全な作業コードへのリンクは以下のとおりです(わかりやすいクラスバージョンとインデックス可能なスキリストコードをインライン化した最適化されたジェネレータバージョン)。

26

これについて考える直感的な方法は、あなたが完全にバランスのとれた二分探索木を持っていたなら、根が中央値の要素になるだろうということです。さて、ツリーがいっぱいでなければ、最後のレベルから欠けている要素があるので、これはそれほど当てはまりません。

そのため、代わりにできることは、中央値と、中央値よりも小さい要素用の中央値、中央値よりも大きい要素用の2つのバランスのとれた二分木です。 2本の木は同じ大きさに保たれなければなりません。

データストリームから新しい整数を取得すると、それを中央値と比較します。中央値よりも大きい場合は、右側のツリーに追加します。 2つの木のサイズが1より大きく異なる場合は、右側の木のmin要素を削除し、それを新しい中央値にして、古い中央値を左側の木に配置します。小さくても同じです。

効率は文脈に依存する言葉です。この問題の解決策は、挿入数に対する実行されたクエリの量によって異なります。あなたが中央値に興味を持っていた最後の方にN個の数字とK回を挿入しているとしましょう。ヒープベースのアルゴリズムの複雑さはO(N log N + K)になります。

次の方法を検討してください。配列内の数値を切り捨て、クエリごとに(クイッククイックピボットを使用して)線形選択アルゴリズムを実行します。これで、実行時間O(K N)のアルゴリズムが完成しました。

Kが十分に小さい場合(まれなクエリ)、後者のアルゴリズムは実際にはより効率的であり、逆もまた同様です。

6
Peteris