web-dev-qa-db-ja.com

List <E>からn個のランダム要素を取得しますか?

_ArrayList<E>_からn個のランダム要素を取得するにはどうすればよいですか?理想的には、take()メソッドを連続して呼び出して、置換せずに別のx要素を取得できるようにします。

65
user568866

2つの主な方法。

  1. Random#nextInt(int) を使用します。

    _List<Foo> list = createItSomehow();
    Random random = new Random();
    Foo foo = list.get(random.nextInt(list.size()));
    _

    ただし、連続したn呼び出しが一意の要素を返すことは保証されません。

  2. Collections#shuffle() を使用します。

    _List<Foo> list = createItSomehow();
    Collections.shuffle(list);
    Foo foo = list.get(0);
    _

    これにより、増分インデックスによってn一意の要素を取得できます(リスト自体に一意の要素が含まれていると仮定)。


Java 8ストリームアプローチがあるかどうか疑問に思っている場合、いいえ、組み込みのアプローチはありません。標準APIにはComparator#randomOrder()のようなものはありません。 (まだ?)。厳密なComparator契約を満たしながら、以下のようなことを試すことができます(ただし、ディストリビューションはかなりひどいです)。

_List<Foo> list = createItSomehow();
int random = new Random().nextInt();
Foo foo = list.stream().sorted(Comparator.comparingInt(o -> System.identityHashCode(o) ^ random)).findFirst().get();
_

代わりにCollections#shuffle()を使用する方が適切です。

95
BalusC

これまでに提案されたソリューションのほとんどは、一意性をチェックして完全なリストシャッフルまたは連続的なランダムピッキングを提案し、必要に応じて再試行します。

しかし、Durstenfeldのアルゴリズム(今日では最も人気のあるFisher-Yatesのバリアント)を利用できます。

Durstenfeldの解決策は、各反復で「打ち消された」数値を最後の打ち消されていない数値と交換することにより、リストの最後に移動することです。

上記により、リスト全体をシャッフルする必要はありませんですが、返されるのに必要な要素の数と同じ数のステップでループを実行します。アルゴリズムは、完全なランダム関数を使用した場合、リストの最後の最後のN個の要素が100%ランダムであることを保証します。

配列/リストから事前に決められた(最大)量のランダム要素を選択する必要がある多くの現実世界のシナリオの中で、この最適化された方法は、数が事前にわかっているテキサスポーカーなどのさまざまなカードゲームに非常に役立ちますゲームごとに使用されるカード。通常、限られた数のカードのみがデッキから必要です。

public static <E> List<E> pickNRandomElements(List<E> list, int n, Random r) {
    int length = list.size();

    if (length < n) return null;

    //We don't need to shuffle the whole list
    for (int i = length - 1; i >= length - n; --i)
    {
        Collections.swap(list, i , r.nextInt(i + 1));
    }
    return list.subList(length - n, length);
}

public static <E> List<E> pickNRandomElements(List<E> list, int n) {
    return pickNRandomElements(list, n, ThreadLocalRandom.current());
}
28
Kostas Chalkias

リストからn個の要素を連続して選択し、何度も何度も置換せずに選択できるようにする場合は、おそらく要素をランダムに並べ替えてから、n個のブロックでチャンクを削除することをお勧めします。リストをランダムに並べ替えると、選択した各ブロックの統計的なランダム性が保証されます。これを行う最も簡単な方法は、おそらく Collections.shuffle

10
templatetypedef

シンプルで明確

   // define ArrayList to hold Integer objects
    ArrayList<Integer> arrayList = new ArrayList<>();

    for (int i = 0; i < maxRange; i++) {
        arrayList.add(i + 1);
    }

    // shuffle list
    Collections.shuffle(arrayList);

    // adding defined amount of numbers to target list
    ArrayList<Integer> targetList = new ArrayList<>();
    for (int j = 0; j < amount; j++) {
        targetList.add(arrayList.get(j)); 
    }

    return targetList;
7
Serge

これを行うための公正な方法は、リストを調べることです。n回目の反復で、n番目の要素を選択するかどうかの確率を計算します。リストの残りで利用可能です。例えば:

public static <T> T[] pickSample(T[] population, int nSamplesNeeded, Random r) {
  T[] ret = (T[]) Array.newInstance(population.getClass().getComponentType(),
                                    nSamplesNeeded);
  int nPicked = 0, i = 0, nLeft = population.length;
  while (nSamplesNeeded > 0) {
    int Rand = r.nextInt(nLeft);
    if (Rand < nSamplesNeeded) {
      ret[nPicked++] = population[i];
      nSamplesNeeded--;
    }
    nLeft--;
    i++;
  }
  return ret;
}

(このコードは リストからランダムなサンプルを選択 で先ほど書いたページからコピーしたものです。)

5
Neil Coffey

次のクラスを使用します。

import Java.util.Enumeration;
import Java.util.Random;

public class RandomPermuteIterator implements Enumeration<Long> {
    int c = 1013904223, a = 1664525;
    long seed, N, m, next;
    boolean hasNext = true;

    public RandomPermuteIterator(long N) throws Exception {
        if (N <= 0 || N > Math.pow(2, 62)) throw new Exception("Unsupported size: " + N);
        this.N = N;
        m = (long) Math.pow(2, Math.ceil(Math.log(N) / Math.log(2)));
        next = seed = new Random().nextInt((int) Math.min(N, Integer.MAX_VALUE));
    }

    public static void main(String[] args) throws Exception {
        RandomPermuteIterator r = new RandomPermuteIterator(100);
        while (r.hasMoreElements()) System.out.print(r.nextElement() + " ");
    }

    @Override
    public boolean hasMoreElements() {
        return hasNext;
    }

    @Override
    public Long nextElement() {
        next = (a * next + c) % m;
        while (next >= N) next = (a * next + c) % m;
        if (next == seed) hasNext = false;
        return  next;
    }
}
2
aykutfirat

ランダムな要素を選択し続け、同じ要素を再度選択しないようにしてください。

public static <E> List<E> selectRandomElements(List<E> list, int amount)
{
    // Avoid a deadlock
    if (amount >= list.size())
    {
        return list;
    }

    List<E> selected = new ArrayList<>();
    Random random = new Random();
    int listSize = list.size();

    // Get a random item until we got the requested amount
    while (selected.size() < amount)
    {
        int randomIndex = random.nextInt(listSize);
        E element = list.get(randomIndex);

        if (!selected.contains(element))
        {
            selected.add(element);
        }
    }

    return selected;
}

理論的には、これは無限に実行できますが、実際には問題ありません。元のリスト全体を取得するほど、実行時間は明らかに遅くなりますが、それはランダムなサブリストを選択するポイントではありませんか?

2
BullyWiiPlaza

次のクラスは、任意のタイプのリストからN個のアイテムを取得します。シードを指定すると、実行ごとに同じリストが返されます。それ以外の場合は、実行ごとに新しいリストのアイテムが変更されます。 mainメソッドを実行すると、その動作を確認できます。

import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.Collections;
import Java.util.List;
import Java.util.Random;

public class NRandomItem<T> {
    private final List<T> initialList;

    public NRandomItem(List<T> list) {
        this.initialList = list;
    }

    /**
     * Do not provide seed, if you want different items on each run.
     * 
     * @param numberOfItem
     * @return
     */
    public List<T> retrieve(int numberOfItem) {
        int seed = new Random().nextInt();
        return retrieve(seed, numberOfItem);
    }

    /**
     * The same seed will always return the same random list.
     * 
     * @param seed,
     *            the seed of random item generator.
     * @param numberOfItem,
     *            the number of items to be retrieved from the list
     * @return the list of random items
     */
    public List<T> retrieve(int seed, int numberOfItem) {
        Random Rand = new Random(seed);

        Collections.shuffle(initialList, Rand);
        // Create new list with the number of item size
        List<T> newList = new ArrayList<>();
        for (int i = 0; i < numberOfItem; i++) {
            newList.add(initialList.get(i));
        }
        return newList;
    }

    public static void main(String[] args) {
        List<String> l1 = Arrays.asList("Foo", "Bar", "Baz", "Qux");
        int seedValue = 10;
        NRandomItem<String> r1 = new NRandomItem<>(l1);

        System.out.println(String.format("%s", r1.retrieve(seedValue, 2)));
    }
}
0
Memin

他の回答で述べたように、コピーのため、ソースリストが大きい場合は_Collections.shuffle_はあまり効率的ではありません。以下はJava 8のワンライナーです。

  • ソースから多くの要素を必要としない場合、ArrayListのようなランダムアクセスリストで十分効率的
  • ソースを変更しません
  • それがあなたにとって非常に重要でない場合、一意性を保証しません。 100から5を選択すると、要素が一意になる可能性が非常に高くなります。

コード:

_private static <E> List<E> pickRandom(List<E> list, int n) {
  return new Random().ints(n, 0, list.size()).mapToObj(list::get).collect(Collectors.toList());
}
_

ただし、クイックランダムアクセスのないリスト(LinkedListなど)の場合、複雑さはn*O(list_size)になります。

0
smok