web-dev-qa-db-ja.com

コレクションからランダムなサブセットを選択する最良の方法は?

ベクターに一連のオブジェクトがあり、そこからランダムなサブセットを選択します(たとえば、100個のアイテムが戻ってきます。ランダムに5個を選択します)。私の最初の(非常に急いで)パスで、私は非常にシンプルで、おそらく非常に賢い解決策を行いました:

Vector itemsVector = getItems();

Collections.shuffle(itemsVector);
itemsVector.setSize(5);

これには、ニースでシンプルであるという利点がありますが、あまりうまくスケーリングできないと思います。つまり、Collections.shuffle()は少なくともO(n)でなければなりません。

Vector itemsVector = getItems();

Random Rand = new Random(System.currentTimeMillis()); // would make this static to the class    

List subsetList = new ArrayList(5);
for (int i = 0; i < 5; i++) {
     // be sure to use Vector.remove() or you may get the same item twice
     subsetList.add(itemsVector.remove(Rand.nextInt(itemsVector.size())));
}

コレクションからランダムなサブセットを引き出すより良い方法に関する提案はありますか?

66
Tom

Jon Bentleyは、「Programming Pearls」または「More Programming Pearls」でこれについて説明しています。 N of Mの選択プロセスに注意する必要がありますが、表示されているコードは正しく機能していると思います。すべてのアイテムをランダムにシャッフルするのではなく、最初のNポジションだけシャッフルするランダムシャッフルを実行できます。これは、N << Mの場合に便利な節約になります。

Knuthはこれらのアルゴリズムについても説明します。これは第3巻の「並べ替えと検索」だと思いますが、私のセットは家の移動が保留されているため、正式に確認することはできません。

10

@ジョナサン、

私はこれがあなたが話している解決策であると信じています:

void genknuth(int m, int n)
{    for (int i = 0; i < n; i++)
         /* select m of remaining n-i */
         if ((bigrand() % (n-i)) < m) {
             cout << i << "\n";
             m--;
         }
}

Jon BentleyによるProgramming Pearlsの127ページにあり、Knuthの実装に基づいています。

編集:129ページでさらに修正を見ました:

void genshuf(int m, int n)
{    int i,j;
     int *x = new int[n];
     for (i = 0; i < n; i++)
         x[i] = i;
     for (i = 0; i < m; i++) {
         j = randint(i, n-1);
         int t = x[i]; x[i] = x[j]; x[j] = t;
     }
     sort(x, x+m);
     for (i = 0; i< m; i++)
         cout << x[i] << "\n";
}

これは、「...最初のm配列の要素のみをシャッフルする必要がある...」という考えに基づいています。

8
daniel

これの効率的な実装 数週間前に書きました。 C#にありますが、Javaへの翻訳は簡単です(基本的に同じコード)。プラス面は、それも完全に公平です(既存の回答のいくつかはそうではありません)- テストする方法はここにあります

Fisher-YatesシャッフルのDurstenfeld実装に基づいています。

4
Greg Beech

Nのリストからk個の異なる要素を選択しようとする場合、上記のメソッドはO(n)またはO(kn)になります。ベクターから要素を削除すると、 arraycopyを実行して、すべての要素を下にシフトします。

最良の方法を求めているので、入力リストで何を許可するかによって異なります。

例のように入力リストを変更することが許容される場合、k個のランダムな要素をリストの先頭に単純に交換し、次のようにO(k) timeでそれらを返すことができます。

public static <T> List<T> getRandomSubList(List<T> input, int subsetSize)
{
    Random r = new Random();
    int inputSize = input.size();
    for (int i = 0; i < subsetSize; i++)
    {
        int indexToSwap = i + r.nextInt(inputSize - i);
        T temp = input.get(i);
        input.set(i, input.get(indexToSwap));
        input.set(indexToSwap, temp);
    }
    return input.subList(0, subsetSize);
}

リストを開始時と同じ状態にする必要がある場合は、交換した位置を追跡し、選択したサブリストをコピーした後、リストを元の状態に戻すことができます。これは、まだO(k)ソリューションです。

ただし、入力リストをまったく変更できず、kがnよりはるかに小さい場合(100から5など)、毎回選択した要素を削除せずに、単に各要素を選択することをお勧めします。複製、それを投げ出し、再選択します。これにより、nがkを支配している場合、O(k)に近いO(kn /(nk)))が得られます(たとえば、kがn/2より小さい場合、 O(k)になります)。

Kがnに支配されておらず、リストを変更できない場合、元のリストをコピーして最初の解決策を使用することもできます。O(n)はOと同じくらい良いからです(k)。

他の人が指摘したように、すべてのサブリストが可能な(そして偏りのない)強いランダム性に依存している場合、Java.util.Random。見る Java.security.SecureRandom

4
Dave L.

ただし、Randomを使用して要素を選択する2番目のソリューションは適切に思えます。

2
qualidafial

これ は、stackoverflowに関する非常によく似た質問です。

そのページからの私のお気に入りの回答を要約するには(ユーザーカイルからの最後の1つ):

  • O(n)solution:リストを繰り返し処理し、要素(またはその参照)を確率(#needed/#remaining)でコピーします。例:k = 5およびn = 100の場合、prob 5/100で最初の要素を取得します。それをコピーする場合、prob 4/99で次を選択します。ただし、最初のものを選択しなかった場合、問題は5/99です。
  • O(k log k)またはO(k2:数値<nをランダムに選択し、次に数値<nをランダムに選択することにより、kインデックス({0、1、...、n-1}の数値)のソート済みリストを作成します-1など。各ステップで、衝突を避け、確率を均等に保つために、選択を思い出す必要があります。例として、k = 5およびn = 100で、最初の選択肢が43の場合、次の選択肢は範囲[0、98]であり、> = 43の場合、1を追加します。 2番目の選択肢が50の場合、1を追加すると{43、51}になります。次の選択肢が51の場合、2を追加して{43、51、53}を取得します。

ここにいくつかの擬似Pythonがあります-

# Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
    s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
  return s 

時間の複雑さはO(k2orO(k log k)sのコンテナに検索して挿入できる速さに依存するため。 sが通常のリストの場合、これらの演算の1つは線形であり、k ^ 2を取得します。ただし、バランスの取れたバイナリツリーとしてsを作成する場合は、O(k log k)時間を取得できます。

0
Tyler
Set<Integer> s = new HashSet<Integer>()
// add random indexes to s
while(s.size() < 5)
{
    s.add(Rand.nextInt(itemsVector.size()))
}
// iterate over s and put the items in the list
for(Integer i : s)
{
    out.add(itemsVector.get(i));
}
0
Wesley Tarle

費用はどれくらいかかりますか?配列を新しいメモリチャンクに書き換える必要がある場合は、以前のO(5n)ではなく、2番目のバージョンでO(n)操作を行ったためです。

Falseに設定されたブール値の配列を作成すると、次のようになります。

for (int i = 0; i < 5; i++){
   int r = Rand.nextInt(itemsVector.size());
   while (boolArray[r]){
       r = Rand.nextInt(itemsVector.size());
   }
   subsetList.add(itemsVector[r]);
   boolArray[r] = true;
}

このアプローチは、サブセットが合計サイズよりも大幅に小さい場合に機能します。これらのサイズが互いに近づくと(つまり、サイズの1/4など)、その乱数ジェネレーターでさらに衝突が発生します。その場合、整数のリストをより大きな配列のサイズにし、その整数のリストをシャッフルし、そこから最初の要素を取り出して、(衝突しない)インデックスを取得します。そうすれば、整数配列の作成にO(n)、シャッフルに別のO(n)のコストがかかりますが、内部whileチェッカーからの衝突はなく、潜在的なO(5n)削除するとコストがかかる場合があります。

0
mmr

私はあなたの初期の実装を個人的に選択します:非常に簡潔です。パフォーマンステストでは、拡張性が示されます。適切に悪用された方法で非常によく似たコードブロックを実装しましたが、十分に拡張されました。特定のコードは、10,000個以上のアイテムを含む配列にも依存していました。

0
daniel

ここに表示されるとは思わない2つの解決策-対応するものは非常に長く、いくつかのリンクが含まれていますが、すべての投稿がN個の要素の集合からK要素を選択する問題に関連するとは思わない。 [「セット」とは、数学用語を指します。つまり、すべての要素が一度出現します。順序は重要ではありません]。

ソル1:

//Assume the set is given as an array:
Object[] set ....;
for(int i=0;i<K; i++){
randomNumber = random() % N;
    print set[randomNumber];
    //swap the chosen element with the last place
    temp = set[randomName];
    set[randomName] = set[N-1];
    set[N-1] = temp;
    //decrease N
    N--;
}

これはダニエルが与えた答えに似ていますが、実際には非常に異なっています。 O(k)ランタイムです。

別の解決策は、いくつかの数学を使用することです:配列インデックスをZ_nとして、ランダムに2つの数を選択できるようにします。 「開始点」-その後、シリーズ:a%n、a + x%n、a + 2 * x%n、... a +(k-1)* x%nは、一連の個別の数字です(ただし、 k <= n)。

0
user967710