セットからランダムな要素を選ぶ


180

セットからランダムな要素を選択するにはどうすればよいですか?JavaでHashSetまたはLinkedHashSetからランダムな要素を選択することに特に興味があります。他の言語のソリューションも歓迎します。


5
これが本当に必要かどうかを確認するには、いくつかの条件を指定する必要があります。-ランダムな要素をどのくらいの頻度で選択しますか?-データをHashSetまたはLinkedHashSetに格納する必要がありますか。どちらもランダムにアクセスできません。-ハッシュセットは大きいですか。キーは小さいですか?
David Nehme 2008

回答:


88
int size = myHashSet.size();
int item = new Random().nextInt(size); // In real life, the Random object should be rather more shared than this
int i = 0;
for(Object obj : myhashSet)
{
    if (i == item)
        return obj;
    i++;
}

94
myHashSetが大きい場合、ランダムオブジェクトを見つけるために平均して(n / 2)回の反復が必要になるため、これはかなり遅いソリューションになります。
ダニエル

6
データがハッシュセットにある場合、O(n)時間必要です。単一の要素を選択するだけで、データがHashSetに格納されている場合は、回避策がありません。
David Nehme 2008

8
@David Nehme:これは、JavaでのHashSetの仕様の欠点です。C ++では、通常、ハッシュセットを構成するバケットに直接アクセスできるため、ランダムな要素をより効率的に選択できます。Javaでランダムな要素が必要な場合は、ユーザーが内部で確認できるカスタムハッシュセットを定義することは価値があるかもしれません。これについてもう少し詳しくは、[boostのドキュメント] [1]を参照してください。[1] boost.org/doc/libs/1_43_0/doc/html/unordered/buckets.html
Aaron McDaid

11
セットが複数のアクセスで変異していない場合は、セットを配列にコピーして、O(1)にアクセスできます。myHashSet.toArray()を使用するだけ
ykaganovich

2
@ykaganovichは、セットを新しい配列にコピーする必要があるため、事態を悪化させませんか?docs.oracle.com/javase/7/docs/api/java/util/… "このメソッドは、このコレクションが配列に支えられている場合でも、新しい配列を割り当てる必要があります"
anton1980

73

やや関連するご存知ですか:

そこに有用な方法ですjava.util.Collectionsコレクション全体をシャッフルするためには:Collections.shuffle(List<?>)Collections.shuffle(List<?> list, Random rnd)


驚くばかり!これは、Java docのどこにも相互参照されていません。Pythonのrandom.shuffle()
smci

25
しかし、これはリスト、つまり.get()関数を持つ構造体でのみ機能します。
bourbaki4481472 2015

4
@ bourbaki4481472は完全に正しいです。これは、Listインターフェースを拡張するコレクションでのみ機能Setし、OPで説明されているインターフェースでは機能しません。
トーマス

31

ArrayListand を使用したJavaの高速ソリューションHashMap:[要素->インデックス]。

動機:RandomAccess特にセットからランダムなアイテムを選択するために、プロパティ付きのアイテムのセットが必要でした(pollRandomメソッドを参照)。バイナリツリーのランダムナビゲーションは正確ではありません。ツリーは完全にバランスが取れていないため、均一な分布にはなりません。

public class RandomSet<E> extends AbstractSet<E> {

    List<E> dta = new ArrayList<E>();
    Map<E, Integer> idx = new HashMap<E, Integer>();

    public RandomSet() {
    }

    public RandomSet(Collection<E> items) {
        for (E item : items) {
            idx.put(item, dta.size());
            dta.add(item);
        }
    }

    @Override
    public boolean add(E item) {
        if (idx.containsKey(item)) {
            return false;
        }
        idx.put(item, dta.size());
        dta.add(item);
        return true;
    }

    /**
     * Override element at position <code>id</code> with last element.
     * @param id
     */
    public E removeAt(int id) {
        if (id >= dta.size()) {
            return null;
        }
        E res = dta.get(id);
        idx.remove(res);
        E last = dta.remove(dta.size() - 1);
        // skip filling the hole if last is removed
        if (id < dta.size()) {
            idx.put(last, id);
            dta.set(id, last);
        }
        return res;
    }

    @Override
    public boolean remove(Object item) {
        @SuppressWarnings(value = "element-type-mismatch")
        Integer id = idx.get(item);
        if (id == null) {
            return false;
        }
        removeAt(id);
        return true;
    }

    public E get(int i) {
        return dta.get(i);
    }

    public E pollRandom(Random rnd) {
        if (dta.isEmpty()) {
            return null;
        }
        int id = rnd.nextInt(dta.size());
        return removeAt(id);
    }

    @Override
    public int size() {
        return dta.size();
    }

    @Override
    public Iterator<E> iterator() {
        return dta.iterator();
    }
}

うまくいきますが、問題はSetインターフェースに関するものでした。このソリューションでは、RandomSetの具体的な型参照をユーザーに強制します。
JohanTidén15年

私はこのソリューションが本当に好きですが、スレッドセーフではないため、マップとリストの間に不正確さが生じる可能性があるため、同期ブロックをいくつか追加します
Kostas Chalkias

@KonstantinosChalkiasの組み込みコレクションもスレッドセーフではありません。名前Concurrentが付いたものだけが本当に安全で、ラップされたものCollections.synchronized()は半安全です。また、OPは同時実行性について何も言わなかったので、これは有効で良い答えです。
TWiStErRob 2016

ここで返されるイテレータは、要素を削除できないようにする必要がありますdta(これはIterators.unmodifiableIterator、たとえばグアバを使用して実現できます)。そうでない場合、AbstractSetのremoveAllやretainAllのデフォルト実装と、そのイテレータで動作するその親はあなたのRandomSet
16

素晴らしい解決策。各ノードに、そのルートとなるサブツリー内のノード数が含まれている場合、実際にはツリーを使用できます。次に、0..1でランダムな実数を計算し、ノード数に基づいて各ノードで重み付けされた3方向の決定(現在のノードを選択するか、左または右のサブツリーに下降)を行います。しかし、あなたのソリューションはもっといいです。
Gene

29

これは、受け入れられた回答のfor-eachループよりも高速です。

int index = rand.nextInt(set.size());
Iterator<Object> iter = set.iterator();
for (int i = 0; i < index; i++) {
    iter.next();
}
return iter.next();

for-each構文はIterator.hasNext()すべてのループを呼び出しますが、それ以降index < set.size()、そのチェックは不要なオーバーヘッドです。速度が10〜20%向上しましたが、YMMVでした。(また、これは余分なreturnステートメントを追加する必要なくコンパイルされます。)

このコード(および他のほとんどの回答)は、セットだけでなく、任意のコレクションに適用できることに注意してください。ジェネリックメソッド形式:

public static <E> E choice(Collection<? extends E> coll, Random rand) {
    if (coll.size() == 0) {
        return null; // or throw IAE, if you prefer
    }

    int index = rand.nextInt(coll.size());
    if (coll instanceof List) { // optimization
        return ((List<? extends E>) coll).get(index);
    } else {
        Iterator<? extends E> iter = coll.iterator();
        for (int i = 0; i < index; i++) {
            iter.next();
        }
        return iter.next();
    }
}

15

Javaで行う場合は、要素をある種のランダムアクセスコレクション(ArrayListなど)にコピーすることを検討してください。セットが小さい場合を除き、選択した要素へのアクセスにはコストがかかるためです(O(1)ではなくO(n))。[ed:リストのコピーもO(n)]

または、要件にさらに一致する別のSet実装を探すこともできます。CommonsコレクションのListOrderedSetは有望に見えます。


8
リストへのコピーは時間内にO(n)のコストがかかり、O(n)メモリも使用するので、なぜマップから直接フェッチするよりも良い選択なのでしょうか?
mdma 2010

12
セットから何回ピックするかによります。コピーは1回限りの操作であり、必要な回数だけセットから選択できます。要素を1つだけ選択している場合、はい、コピーを行っても処理は速くなりません。
Dan Dyer、

繰り返しピックできるようにしたいなら、一度だけの操作です。選択したアイテムをセットから削除する場合は、O(n)に戻ります。
TurnipEntropy 2017

12

Java 8の場合:

static <E> E getRandomSetElement(Set<E> set) {
    return set.stream().skip(new Random().nextInt(set.size())).findFirst().orElse(null);
}

9

Javaの場合:

Set<Integer> set = new LinkedHashSet<Integer>(3);
set.add(1);
set.add(2);
set.add(3);

Random rand = new Random(System.currentTimeMillis());
int[] setArray = (int[]) set.toArray();
for (int i = 0; i < 10; ++i) {
    System.out.println(setArray[rand.nextInt(set.size())]);
}

11
あなたの答えは機能しますが、set.toArray()部分があるため、あまり効率的ではありません。
手がかり

12
toArrayをループの外側に移動する必要があります。
David Nehme

8
List asList = new ArrayList(mySet);
Collections.shuffle(asList);
return asList.get(0);

21
これは非常に非効率的です。ArrayListコンストラクタは、提供されたセットで.toArray()を呼び出します。ToArray(すべてではないにしてもほとんどの標準的なコレクション実装)は、コレクション全体を反復処理し、配列を埋めていきます。次に、リストをシャッフルし、各要素をランダムな要素と交換します。セットをランダムな要素に反復するだけのほうがはるかによいでしょう。
Chris Bode 2013年

4

これが受け入れ答え(Khoth)と同じですが、不要とsizeし、i変数を削除します。

    int random = new Random().nextInt(myhashSet.size());
    for(Object obj : myhashSet) {
        if (random-- == 0) {
            return obj;
        }
    }

前述の2つの変数を取り除きますが、上記のソリューションは依然としてランダムなままです(ランダムに選択されたインデックスから開始する)が0、反復ごとに減少するためです。


1
3行目もif (--random < 0) {、どこにrandom到達するかもしれません-1
サルバドール

3

Clojureソリューション:

(defn pick-random [set] (let [sq (seq set)] (nth sq (rand-int (count sq)))))

1
nth要素を取得するにはseq、もトラバースする必要があるため、このソリューションも線形です。
Bruno Kim

1
それは一行にうまく収まるように、それはまた、線形だ:D
クシシュトフWolny

2

Perl 5

@hash_keys = (keys %hash);
$rand = int(rand(@hash_keys));
print $hash{$hash_keys[$rand]};

これを行う1つの方法を次に示します。


2

C ++。セット全体を繰り返し処理したり、並べ替えたりする必要がないため、これはかなり高速です。これは、tr1をサポートしていることを前提として、ほとんどの最新のコンパイラーでそのまま使用できます。そうでない場合は、Boostを使用する必要があります。

ブーストドキュメントは、あなたがブーストを使用しない場合でも、これを説明するためにここに役立ちます。

秘訣は、データがバケットに分割されているという事実を利用し、ランダムに選択されたバケットを(適切な確率で)すばやく識別することです。

//#include <boost/unordered_set.hpp>  
//using namespace boost;
#include <tr1/unordered_set>
using namespace std::tr1;
#include <iostream>
#include <stdlib.h>
#include <assert.h>
using namespace std;

int main() {
  unordered_set<int> u;
  u.max_load_factor(40);
  for (int i=0; i<40; i++) {
    u.insert(i);
    cout << ' ' << i;
  }
  cout << endl;
  cout << "Number of buckets: " << u.bucket_count() << endl;

  for(size_t b=0; b<u.bucket_count(); b++)
    cout << "Bucket " << b << " has " << u.bucket_size(b) << " elements. " << endl;

  for(size_t i=0; i<20; i++) {
    size_t x = rand() % u.size();
    cout << "we'll quickly get the " << x << "th item in the unordered set. ";
    size_t b;
    for(b=0; b<u.bucket_count(); b++) {
      if(x < u.bucket_size(b)) {
        break;
      } else
        x -= u.bucket_size(b);
    }
    cout << "it'll be in the " << b << "th bucket at offset " << x << ". ";
    unordered_set<int>::const_local_iterator l = u.begin(b);
    while(x>0) {
      l++;
      assert(l!=u.end(b));
      x--;
    }
    cout << "random item is " << *l << ". ";
    cout << endl;
  }
}

2

上記のソリューションはレイテンシの観点から説明していますが、各インデックスが選択される確率が等しいことを保証するものではありません。
それを考慮する必要がある場合は、貯水池サンプリングを試してください。http://en.wikipedia.org/wiki/Reservoir_sampling
Collections.shuffle()(少数の人が示唆)は、そのようなアルゴリズムの1つを使用します。


1

「他の言語のソリューションも歓迎します」と言ったので、Pythonのバージョンは次のとおりです。

>>> import random
>>> random.choice([1,2,3,4,5,6])
3
>>> random.choice([1,2,3,4,5,6])
4

3
ただし、[1,2,3,4,5,6]はセットではなくリストです。これは、高速ルックアップなどをサポートしていないためです。
Thomas Ahle、

あなたはまだ行うことができます:>>> random.choice(list(set(range(5))))>>> 4理想的ではありませんが、絶対に必要な場合はそれを行います。
SapphireSun

1

セット/配列のサイズ/長さを取得し、0からサイズ/長さまでの乱数を生成して、その番号に一致するインデックスを持つ要素を呼び出すことはできませんか?HashSetには.size()メソッドがあります。

擬似コードで-

function randFromSet(target){
 var targetLength:uint = target.length()
 var randomIndex:uint = random(0,targetLength);
 return target[randomIndex];
}

これは、問題のコンテナがランダムインデックスルックアップをサポートしている場合にのみ機能します。多くのコンテナ実装はそうではありません(例えば、ハッシュテーブル、バイナリツリー、リンクリスト)。
David Haley

1

PHP、「set」が配列であると仮定:

$foo = array("alpha", "bravo", "charlie");
$index = array_rand($foo);
$val = $foo[$index];

Mersenne Twister関数はより優れていますが、PHPにはarray_randに相当するMTはありません。


ほとんどのセット実装にはget(i)またはインデックス演算子がないため、idは、OPがセットを指定した理由と見なします
DownloadPizza

1

アイコンにはセット型とランダム要素演算子、単項「?」があるため、式

? set( [1, 2, 3, 4, 5] )

1から5の間の乱数を生成します。

プログラムの実行時にランダムシードは0に初期化されるため、実行ごとに異なる結果が生成されます。 randomize()


1

C#で

        Random random = new Random((int)DateTime.Now.Ticks);

        OrderedDictionary od = new OrderedDictionary();

        od.Add("abc", 1);
        od.Add("def", 2);
        od.Add("ghi", 3);
        od.Add("jkl", 4);


        int randomIndex = random.Next(od.Count);

        Console.WriteLine(od[randomIndex]);

        // Can access via index or key value:
        Console.WriteLine(od[1]);
        Console.WriteLine(od["def"]);

くだらないjava辞書(または、いわゆるLinkedHashSet)に「ランダムにアクセス」(キーでアクセスされている)できないため、反対票が投じられたように見えます。Javaがらくたは私をとても笑わせます
フェデリコ・ベラサテギ

1

JavaScriptソリューション;)

function choose (set) {
    return set[Math.floor(Math.random() * set.length)];
}

var set  = [1, 2, 3, 4], rand = choose (set);

または代わりに:

Array.prototype.choose = function () {
    return this[Math.floor(Math.random() * this.length)];
};

[1, 2, 3, 4].choose();

私は2番目の選択肢を好みます。:-)
マルコスペレイラ

ああ、私は新しい配列メソッドを追加して拡張するのが好きです!
マットローカンプ2008

1

Lispで

(defun pick-random (set)
       (nth (random (length set)) set))

これはリストでのみ機能しますよね?でELT、それは、任意の配列のために働くことができます。
Ken

1

Mathematicaでは:

a = {1, 2, 3, 4, 5}

a[[  Length[a] Random[]  ]]

または、最近のバージョンでは、単に:

RandomChoice[a]

これは、おそらく説明が足りないために反対票を受け取ったので、ここに1つあります。

Random[]0と1の間の疑似乱数浮動小数点数を生成します。これにリストの長さが乗算され、次に上限関数を使用して次の整数に切り上げられます。次に、このインデックスはから抽出されaます。

ハッシュテーブル機能はMathematicaのルールで頻繁に行われ、ルールはリストに保存されるため、次のように使用できます。

a = {"Badger" -> 5, "Bird" -> 1, "Fox" -> 3, "Frog" -> 2, "Wolf" -> 4};


1

楽しみのために、私は拒絶サンプリングに基づいてRandomHashSetを書きました。HashMapではテーブルに直接アクセスできないため、少しハックですが、問題なく動作するはずです。

余分なメモリを使用せず、ルックアップ時間はO(1)で償却されます。(Java HashTableが密集しているため)。

class RandomHashSet<V> extends AbstractSet<V> {
    private Map<Object,V> map = new HashMap<>();
    public boolean add(V v) {
        return map.put(new WrapKey<V>(v),v) == null;
    }
    @Override
    public Iterator<V> iterator() {
        return new Iterator<V>() {
            RandKey key = new RandKey();
            @Override public boolean hasNext() {
                return true;
            }
            @Override public V next() {
                while (true) {
                    key.next();
                    V v = map.get(key);
                    if (v != null)
                        return v;
                }
            }
            @Override public void remove() {
                throw new NotImplementedException();
            }
        };
    }
    @Override
    public int size() {
        return map.size();
    }
    static class WrapKey<V> {
        private V v;
        WrapKey(V v) {
            this.v = v;
        }
        @Override public int hashCode() {
            return v.hashCode();
        }
        @Override public boolean equals(Object o) {
            if (o instanceof RandKey)
                return true;
            return v.equals(o);
        }
    }
    static class RandKey {
        private Random rand = new Random();
        int key = rand.nextInt();
        public void next() {
            key = rand.nextInt();
        }
        @Override public int hashCode() {
            return key;
        }
        @Override public boolean equals(Object o) {
            return true;
        }
    }
}

1
まさに私が考えていたもの!ベストアンサー!
mmm

実際に戻ってみると、ハッシュマップに多くの衝突があり、私たちが多くのクエリを実行する場合、これは均一ではないと思います。これは、Javaハッシュマップがバケット/チェーンを使用しており、このコードが常に特定のバケットの最初の要素を返すためです。ただし、ハッシュ関数のランダム性についてはまだ統一されています。
Thomas Ahle

1

Java 8で最も簡単な方法は次のとおりです。

outbound.stream().skip(n % outbound.size()).findFirst().get()

どこnランダムな整数です。もちろん、これはfor(elem: Col)


1

グアバ我々は少し良くKhothの答えより行うことができます。

public static E random(Set<E> set) {
  int index = random.nextInt(set.size();
  if (set instanceof ImmutableSet) {
    // ImmutableSet.asList() is O(1), as is .get() on the returned list
    return set.asList().get(index);
  }
  return Iterables.get(set, index);
}

0

PHP、MTを使用:

$items_array = array("alpha", "bravo", "charlie");
$last_pos = count($items_array) - 1;
$random_pos = mt_rand(0, $last_pos);
$random_item = $items_array[$random_pos];

0

また、配列を使用して配列に配列を転送することもできます。これはおそらく小規模で機能します。最も投票された答えのforループはとにかくO(n)です。

Object[] arr = set.toArray();

int v = (int) arr[rnd.nextInt(arr.length)];

0

Setランダム性を保証せずに、本当に「任意の」オブジェクトを選択したい場合は、イテレータから返された最初のオブジェクトを取得するのが最も簡単です。

    Set<Integer> s = ...
    Iterator<Integer> it = s.iterator();
    if(it.hasNext()){
        Integer i = it.next();
        // i is a "random" object from set
    }

1
ただし、これはランダムな選択ではありません。同じセットに対して同じ操作を複数回実行することを想像してください。順番は同じだと思います。
Menezes Sousa

0

Khothの回答を出発点として使用する一般的なソリューション。

/**
 * @param set a Set in which to look for a random element
 * @param <T> generic type of the Set elements
 * @return a random element in the Set or null if the set is empty
 */
public <T> T randomElement(Set<T> set) {
    int size = set.size();
    int item = random.nextInt(size);
    int i = 0;
    for (T obj : set) {
        if (i == item) {
            return obj;
        }
        i++;
    }
    return null;
}

0

残念ながら、これは標準ライブラリセットのコンテナでは効率的に(O(n)よりも優れて)行うことはできません。

ランダムなピック関数をハッシュセットとバイナリセットに追加するのは非常に簡単なので、これは奇妙です。スパースでないハッシュセットでは、ヒットするまでランダムなエントリを試すことができます。バイナリツリーの場合、最大O(log2)ステップで、左または右のサブツリーからランダムに選択できます。以下の後者のデモを実装しました:

import random

class Node:
    def __init__(self, object):
        self.object = object
        self.value = hash(object)
        self.size = 1
        self.a = self.b = None

class RandomSet:
    def __init__(self):
        self.top = None

    def add(self, object):
        """ Add any hashable object to the set.
            Notice: In this simple implementation you shouldn't add two
                    identical items. """
        new = Node(object)
        if not self.top: self.top = new
        else: self._recursiveAdd(self.top, new)
    def _recursiveAdd(self, top, new):
        top.size += 1
        if new.value < top.value:
            if not top.a: top.a = new
            else: self._recursiveAdd(top.a, new)
        else:
            if not top.b: top.b = new
            else: self._recursiveAdd(top.b, new)

    def pickRandom(self):
        """ Pick a random item in O(log2) time.
            Does a maximum of O(log2) calls to random as well. """
        return self._recursivePickRandom(self.top)
    def _recursivePickRandom(self, top):
        r = random.randrange(top.size)
        if r == 0: return top.object
        elif top.a and r <= top.a.size: return self._recursivePickRandom(top.a)
        return self._recursivePickRandom(top.b)

if __name__ == '__main__':
    s = RandomSet()
    for i in [5,3,7,1,4,6,9,2,8,0]:
        s.add(i)

    dists = [0]*10
    for i in xrange(10000):
        dists[s.pickRandom()] += 1
    print dists

[995、975、971、995、1057、1004、966、1052、984、1001]を出力として取得したので、分布の継ぎ目は良好です。

私自身も同じ問題に悩んでおり、このより効率的なピックによるパフォーマンスの向上は、Pythonベースのコレクションを使用するオーバーヘッドに見合うだけの価値があるかどうか、まだ判断していません。もちろん、それを改良してCに翻訳することもできますが、それは今日の私にとっては大変な作業です:)


1
これがバイナリツリーで実装されていないと思う理由は、そのような方法ではアイテムを均一に選択できないためです。それらは左/右の子のないノードであるため、左の子が右の子よりも多くのアイテムを含む(またはその逆)状況が発生する可能性があり、これにより右(または左)の子でアイテムを選択する可能性が高くなります。
Willem Van Onsem

1
@CommuSoft:そのため、各サブツリーのサイズを保存するので、それらに基づいて確率を選択できます。
Thomas Ahle
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.