Візьміть n випадкових елементів зі списку <E>?


79

Як я можу взяти n випадкових елементів з an ArrayList<E>? В ідеалі я хотів би мати можливість здійснювати послідовні виклики take()методу, щоб отримати інші елементи x без заміни.


що у вас поки що? Якщо ви отримали ще один елемент x, чи можете ви знову вибрати елементи з попереднього набору, або вони повинні бути різними весь час, поки не будуть вибрані ВСІ елементи? (Тоді, що далі?)
Янік Рошон

Без заміни. Коли у вас більше не залишиться, ви нічого не повинні повернути.

Відповіді:


111

Два основних способи.

  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 Stream; ні, вбудованого немає. Немає такої речі, як Comparator#randomOrder()у стандартному API (ще?). Ви можете спробувати щось на кшталт нижче, все ще виконуючи суворий 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()замість цього.


У мене є список із 4000 слів, і я повинен витягувати з нього 5 слів кожного разу, коли я натискаю кнопку оновлення, я використовую 2-й варіант вашої відповіді. Наскільки це гарантує, що я постійно отримуватиму унікальні значення, тобто яка ймовірність?
Prateek

1
@Prateek: Якщо у вас є питання, натисніть кнопку "Задати питання". Не натискайте кнопку "Додати коментар" або "Опублікувати відповідь".
BalusC

3
Я знаю, коли використовувати яку кнопку, мій коментар певною мірою пов’язаний з вашою вже опублікованою відповіддю, тому я не хотів створювати новий потік if і шукав відповідь в рядку, в будь-якому випадку спасибі.
Prateek

8
Майте на увазі, що Collections.shuffle () використовує версію алгоритму перетасовки Fisher-Yates з внутрішнім екземпляром Random. Клас Random використовує long для значення насіння, тобто він може запропонувати вам лише до 2 ^ 32 можливих перестановок. Цього недостатньо для перетасовки будь-яких 12 елементів з однаковою ймовірністю всіх перестановок (тобто деякі перестановки ніколи не з’являться). Замість цього ви захочете використовувати Collections.shuffle (list, random), де random - це або екземпляр SecureRandom, або власне власне розширення Random, якщо ви вирішуєте це завдання.
Матунос

Матунос - для чого він вартий, ефективний розмір насіння java.util.Random дорівнює 2 ^ 48, але, як ви кажете, все-таки варто пам’ятати, що, можливо, вам доведеться вибрати кращий генератор. Я все одно відстоюю метод, про який я згадую, простий вибір елементів з відповідною ймовірністю (вам все одно потрібна така ж кількість випадкових чисел, як і перетасовка, але вам не доведеться міняти місцями всі покажчики, можливо, кращий обсяг пам'яті, і є шанс закінчити цикл "достроково", як тільки ви вибрали всі необхідні елементи).
Ніл Коффі

33

Більшість із запропонованих рішень дотепер пропонують або перетасування повного списку, або послідовний випадковий вибір, перевіряючи унікальність та повторюючи, якщо потрібно.

Але ми можемо скористатися алгоритмом Дюрстенфельда (найпопулярніший у наш час варіант Фішер-Йейтса).

Рішення Дурстенфельда полягає в тому, щоб перемістити "зачеплені" числа в кінець списку, помінявши їх на останній незнімений номер на кожній ітерації.

Через вищесказане, нам не потрібно перемішувати весь список , а запускати цикл на стільки етапів, скільки потрібно елементів для повернення. Алгоритм гарантує, що останні N елементів у кінці списку є 100% випадковими, якщо ми використовували ідеальну випадкову функцію.

Серед багатьох реальних сценаріїв, коли нам потрібно вибрати заздалегідь визначену (макс.) Кількість випадкових елементів із масивів / списків, цей оптимізований метод дуже корисний для різних карткових ігор, таких як Texas Poker, де ви апріорі знаєте кількість карт, що використовуватимуться в грі; із колоди зазвичай потрібно лише обмежена кількість карт.

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());
}

1
Дякуємо, що вказали на це. У мене ситуація, коли мені потрібно видалити невелику кількість елементів із великого списку, і я був впевнений, що перетасовка цілого списку - не найкращий спосіб це зробити, але я зациклювався на тому, як видалити кілька елементів з список одним махом. Переміщення їх до кінця списку, а потім його скорочення - це елегантне рішення.
Matt

10

Якщо ви хочете послідовно вибирати n елементів зі списку і мати можливість це робити без заміни знову і знову і знову, вам, мабуть, найкраще випадково переставляти елементи, а потім знімати фрагменти в блоках n. Якщо ви випадково переставляєте список, ви гарантуєте статистичну випадковість для кожного вибраного вами блоку. Мабуть, найпростішим способом зробити це було б використання Collections.shuffle.


3
І найпростіший спосіб це зробити - зателефонувати java.util.Collections.shuffle ()
biziclop

7

Просто і зрозуміло

   // 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;

Я не бачив кореляції між arrayListі targetList.
Девід

Це повинно було бути targetList.add (arrayList.get (j))
кочівник

6

Справедливим способом зробити це є перегляд списку на 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;
}

(Цей код скопійовано зі сторінки, яку я написав деякий час тому, вибравши випадкову вибірку зі списку .)


браво - це має бути нагороджена відповідь, оскільки вона є найбільш модульною та портативною
Дрю О'Меара

2

Використовуйте наступний клас:

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

Продовжуйте вибирати випадковий елемент і переконайтеся, що не вибираєте той самий елемент знову:

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

Як зазначалося в інших відповідях, Collections.shuffleне дуже ефективно, коли список джерел великий, через копіювання. Ось однолінійний Java 8, який:

  • досить ефективний у списках довільного доступу, як ArrayList, якщо вам не потрібно багато елементів з джерела
  • не змінює джерело
  • НЕ гарантує унікальності, якщо це не надмірно важливо для вас. Якщо ви виберете 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

Наступний клас отримує N елементів зі списку будь-якого типу. Якщо ви надаєте насіння, то при кожному запуску воно повертатиме той самий список, інакше елементи нового списку змінюватимуться при кожному запуску. Ви можете перевірити його поведінку під час запуску основних методів.

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

Це рішення не змінює оригінальний список або іншим чином масштабує складність розміру списку.

Щоб отримати вибірку 4 зі списку 7, ми просто вибираємо випадковий елемент із усіх 7, потім вибираємо випадковий елемент із решти 6 тощо. Якщо ми вже вибрали індекси 4, 0, 3, далі генеруємо випадкове число з 0, 1, 2, 3, відповідно представляючи індекс 1, 2, 5, 6.

static Random rand = new Random();

static <T> List<T> randomSample(List<T> list, int size) {
    List<T> sample = new ArrayList<>();

    for (int sortedSampleIndices[] = new int[size], i = 0; i < size; i++) {
        int index = rand.nextInt(list.size() - i);

        int j = 0;
        for (; j < i && index >= sortedSampleIndices[j]; j++)
            index++;
        sample.add(list.get(index));

        for (; j <= i; j++) {
            int temp = sortedSampleIndices[j];
            sortedSampleIndices[j] = index;
            index = temp;
        }
    }

    return sample;
}

0

Усі ці відповіді вимагають модифікованого списку або виходу в дію

Ось швидкий фрагмент, який вимагав додаткового місця O (k) і гарантовано запускається за час O (k) і не потребує модифікованого масиву. (Виконує перемішування на карті)

  func getRandomElementsFrom(array: [Int], count: Int = 8) -> [Int] {
    if array.count <= count {
        return array
    }

    var mapper = [Int: Int]()
    var results = [Int]()

    for i in 0..<count {
        let randomIndex = Int.random(in: 0..<array.count - i)

        if let existing = mapper[randomIndex] {
            results.append(array[existing])
        } else {
            let element = array[randomIndex]
            results.append(element)
        }

        let targetIndex = array.count - 1 - i
        mapper[randomIndex] = mapper[targetIndex] ?? targetIndex 
    }

    return results
}

0

Наступний метод повертає новий Список мінімальних (n, list.size ()) випадкових елементів, взятих зі списку списку параметрів. Майте на увазі, що список Списків змінюється після кожного дзвінка. Тому кожен виклик буде «споживати» вихідний список, повертаючи з нього n випадкових елементів:

public static <T> List<T> nextRandomN(List<T> list, int n) {
  return Stream
    .generate(() -> list.remove((int) (list.size() * Math.random())))
    .limit(Math.min(list.size(), n))
    .collect(Collectors.toList());
}

Зразок використання:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());
System.out.println(nextRandomN(list, 3).toString());

Вихідні дані:

[8, 2, 3]
[4, 10, 7]
[1, 5, 9]
[6]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.