37


23

リンクリストからランダムな要素のセットを効率的に選択する

たとえば、長さNのリンクリストがあるとします。 「N」は非常に大きいので、「N」の正確な値を事前に知りません。

リストから `k`を完全にランダムな数字で返すような関数を最も効率的に書くにはどうすればいいですか?

6 Answer


37


これには、* reservoir sampling *と呼ばれる方法を使用した、非常に優れた効率的なアルゴリズムがあります。

私はあなたにその*歴史*を与えることから始めましょう:

  • Knuth *はpでこのアルゴリズムRを呼び出します。 1997年版のSeminumerical Algorithms(The Art of Computer Programmingの第2巻)の144ページに、そのためのコードが掲載されています。 KnuthはアルゴリズムをAlan Gに帰します。 ウォーターマン 時間のかかる検索にもかかわらず、Watermanのオリジナル文書が存在する場合、それを見つけることができませんでした。そのため、Knuthがこのアルゴリズムの出典として引用されているのを最もよく思う理由かもしれません。

McLeod and Bellhouse、1983 *(1)は、Knuthよりも徹底的な議論と、このアルゴリズムが機能するという(私が知っている)最初の公開証明を提供しています。

  • Vitter 1985 *(2)はアルゴリズムRをレビューしてから、同じ出力を提供するがねじれのある追加の3つのアルゴリズムを提示します。 各着信要素を含めるかスキップするかを選択するのではなく、彼のアルゴリズムはスキップされる着信要素の数を事前に決定します。 彼のテスト(これは確かに、時代遅れです)では、乱数の生成と各着信数の比較を避けることで実行時間を劇的に短縮しました。

*擬似コード*では、アルゴリズムは次のとおりです。

Let R be the result array of size s
Let I be an input queue

> Fill the reservoir array
for j in the range [1,s]:
  R[j]=I.pop()

elements_seen=s
while I is not empty:
  elements_seen+=1
  j=random(1,elements_seen)       > This is inclusive
  if j<=s:
    R[j]=I.pop()
  else:
    I.pop()

入力サイズを指定しないように、コードを具体的に書いたことに注意してください。 これは、このアルゴリズムの優れた特性の1つです。入力のサイズを事前に知る必要なしにそれを実行することができ、それが遭遇する各要素が等しい確率で `R`になることを保証します。バイアスはありません)。 さらに、「R」は、アルゴリズムが常に考慮した要素の公平で代表的なサンプルを含む。 これは、https://en.wikipedia.org/wiki/Online_algorithm [オンラインアルゴリズム]としてこれを使用できることを意味します。

これはなぜうまくいくのですか?

McLeod and Bellhouse(1983)は、組み合わせの数学を使った証明を提供しています。 それはきれいですが、それをここで再構築するのは少し難しいでしょう。 したがって、私は説明がより簡単な代替証明を生成しました。

帰納法による証明で進めます。

s`要素の集合を生成したいとし、すでに n> s`要素を見たとしましょう。

現在の s`要素がすでに s / n`の確率で選択されているとしましょう。

アルゴリズムの定義により、確率 `s /(n 1)`で要素 `n 1 'を選択します。

すでに結果セットの一部になっている各要素は、置き換えられる可能性が「1 / s」になります。

したがって、「n」見た結果セットからの要素が「n 1」見た結果セット内で置き換えられる確率は、「(1 / s)* s /(n 1)= 1 /(n 1)」である。 逆に、要素が置き換えられない確率は `1-1 /(n 1)= n /(n 1)`です。

したがって、 n 1`表示の結果セットは、それが n`表示の結果セットの一部であり置き換えられなかった場合でも要素を含みます---この確率は (s / n)* n /(n 1 )= s /(n 1) `---または要素が選択された場合---確率 s /(n 1) `で---

アルゴリズムの定義は、最初の s`要素が結果セットの最初の n = s`メンバーとして自動的に含まれることを教えてくれます。 したがって、「n-seen」結果セットには「s / n」(= 1)の確率で各要素が含まれ、帰納法に必要な基本ケースが与えられます。

参考資料

  1. マクラウド、A. イアン、そしてデビッドR. ベルハウス 「のための便利なアルゴリズム 単純なランダムサンプルの描画。」王立統計学会誌。 シリーズC(応用統計)32.2(1983):182〜184。 (http://www.jstor.org/stable/10.2307/2347297 [Link])

  2. ビター、ジェフリーS. 「貯水池でのランダムサンプリング。」 ACM Mathematical Software(TOMS)11.1(1985)のトランザクション:37-57。 (http://www.mathcs.emory.edu/~cheung/Courses/584-StreamDB/Syllabus/papers/RandomSampling/1985-Vitter-Random-sampling-with-reservior.pdf [リンク])


34


これは Reservoir Sampling問題と呼ばれています。 簡単な解決策は、リストの各要素に表示されているとおりに乱数を割り当ててから、乱数の順序に従って上位(または下位)のk個の要素を保持することです。


2


私はお勧めします:まずあなたのk乱数を見つけます。 それらを並べ替えます。 それからリンクリストとあなたの乱数の両方を一度たどります。

リンクリストの長さがわからない場合(どうやって?)、最初のkを配列に取り込み、次にノードrに対して[0、r)に乱数を生成することができます。 kよりも大きい場合は、配列の番目の項目を置き換えます。 (それがバイアスされていないことを完全に確信していない…​)

それ以外のこと:「私があなただったら、ここから出発しない」リンクリストがあなたの問題に適していると確信していますか? 古き良きフラット配列リストのような、より良いデータ構造はありませんか。


1


リストの長さがわからない場合は、ランダムに選択するためにリストを完全にトラバースする必要があります。 私がこのケースで使った方法はTom Hawtin(https://stackoverflow.com/questions/54059/efficiently-selecting-a-set-of-random-elements-from-a-linked-list)によって記述されたものです。 #54070 [54070])。 リストをたどっている間、あなたはその時点までのあなたのランダムな選択を形成する k`要素を保持します。 (最初は遭遇した最初の `k`要素を追加するだけです。)そして、確率 k / i`で、選択したものからランダムな要素をリストの `i`番目の要素に置き換えます。 あなたが今いる要素、その瞬間)

これがランダムな選択を与えることを示すのは簡単です。 m`要素を見た後( m> k`)、リストの最初の m`要素はそれぞれ確率「k / m」でランダム選択の一部になります。 これが最初に成り立つことは些細です。 それから、各要素 `m 1`に対して、確率 k /(m 1) `で選択(ランダム要素の置き換え)します。 他のすべての要素もまた選択される確率 `k /(m 1)`を持つことを示す必要があります。 確率は、「k / m *(k /(m 1)*(1−1 / k)(1 − k /(m 1)))」であるとする。 要素がリストに含まれていた可能性と、それがまだ存在している可能性とを掛けたもの。 微積分学を使えば、これが `k /(m 1)`に等しいことを簡単に示すことができます。


-3


さて、あなたはこれがそれらを数えるためにリストの上で余分なパスをすることを含むとしても、少なくとも実行時にNが何であるかを知る必要があります。 これを行うための最も簡単なアルゴリズムは、単にNの乱数を選び、そのアイテムを取り除き、k回繰り返すことです。 または、繰り返し番号を返すことが許可されている場合は、アイテムを削除しないでください。

非常に大きなNと非常に厳しいパフォーマンス要件がない限り、このアルゴリズムは `O(N * k)`の複雑さで動作しますが、許容できるはずです。

編集:決して気にしないで、トムホーティンの方法ははるかに優れています。 まず乱数を選択してから、リストを1回トラバースします。 同じ理論上の複雑さ、私は思うが、はるかに良いランタイムを期待しています。


-3


どうしてそんなことできないの?

List GetKRandomFromList(リスト入力、int k)リストret = new List(); (i = 0; iの場合

私はあなたがそれほど単純な何かを意味していないと確信しているので、あなたはさらに詳細を指定することができますか?