Является ли Collections.shuffle() достаточно случайным? Практические примеры, похоже, опровергают это утверждение.

У меня есть 1000 уникальных объектов в java.util.List, каждый из которых относится к изображению, каждое изображение в списке 1000 уникально, и теперь я хотел бы перетасовать их, чтобы я мог использовать первые 20 объектов и представить их на веб-сайте. пользователь. Затем пользователь может нажать кнопку с надписью «Перемешать», и я снова извлеку 1000 изображений с нуля и снова вызову shuffle(). Тем не менее, кажется, что из 1000 объектов изображения я очень часто вижу одно и то же изображение снова и снова между 20 выборками изображений.

Кажется, что-то не так, какие-нибудь лучшие предложения, советы?

Мой код очень прост:

List<String> imagePaths = get1000Images();
Collections.shuffle(imagePaths);

int i = 0;
for (String path: imagePaths) {
  ... do something with the path ...
  i++;
  if (i >= 20) break;
}

Я знаю, что Collections.shuffle() хорошо распространяется: см., например, http://blog.ryanrampersad.com/2012/03/03/more-on-shuffling-an-array-correctly/

Однако у меня просто есть ощущение, что вероятность увидеть одно и то же изображение снова и снова в наборе из 20 изображений из 1000 должно быть намного меньше...

Вклады высоко оценены.


person basZero    schedule 14.03.2012    source источник
comment
Не видя статистического анализа того, что вы видите, трудно понять, является ли это аномальным.   -  person Dave Newton    schedule 14.03.2012
comment
Я предполагаю, что на самом деле у вас есть один и тот же путь к изображению несколько раз или несколько путей к изображениям, которые фактически содержат одни и те же данные изображения. Кроме того, трудно сказать с этой небольшой информацией...   -  person Jon Skeet    schedule 14.03.2012
comment
нет, все пути к изображениям уникальны (пути к изображениям исходят из Lucene, и каждое изображение индексируется только один раз)   -  person basZero    schedule 14.03.2012
comment
Посмотрите мой ответ здесь, может оказаться полезным. Может стоит подключить другую Random реализацию?   -  person Tomasz Nurkiewicz    schedule 14.03.2012
comment
соответствующий дилберт   -  person oers    schedule 14.03.2012
comment
@TomaszNurkiewicz спасибо! Я проверю с другим экземпляром Random, который создается для каждого запроса на перетасовку. Может быть, это помогает.   -  person basZero    schedule 14.03.2012
comment
@basZero: На самом деле вам не следует не создавать новый экземпляр Random для shuffle, если только у вас нет сильного источника случайных seed. В противном случае повторно используйте тот же самый. Может SecureRandom с хорошим сидом?   -  person Tomasz Nurkiewicz    schedule 14.03.2012


Ответы (6)


Если вы показываете 20 изображений из 1000, вероятность того, что любое из этих 20 повторится в следующей итерации, составляет примерно 0,34, поэтому вам не следует удивляться повторению изображений.

Шансы увидеть конкретное изображение по-прежнему один к тысяче, но если вы ищете двадцать изображений, шансы намного выше.

Мы можем рассчитать вероятность того, что ни одно из предыдущих 20 изображений не повторится, как:

 980   979         961
———— × ——— × ... × ——— ≈ 0.66
1000   999         981

Таким образом, вероятность увидеть повтор равна единице минус это или примерно 0,34.

И вероятность увидеть изображение, повторяющееся в любой из следующих двух итераций, составляет:

1 - (0.66 × 0.66) ≈ 0.56

Другими словами, более вероятно, что вы увидите повторяющееся изображение в течение двух следующих циклов. (И это не считая изображений, повторенных из второго цикла в третьем, что только сделает это более вероятным.)

Для чего это стоит, вот некоторый код Java для выполнения вышеуказанного расчета:

float result = 1.0f;
int totalImages = 1000;
int displayedImages = 20;

for (int i = 0; i < displayedImages; i++) {
  result = result * (totalImages - displayedImages - i) / (totalImages - i);
}

System.out.println(result);
person Dave Webb    schedule 14.03.2012
comment
@ChristofferHammarström — исправлено. - person Dave Webb; 14.03.2012
comment
Или, может быть, это должно быть 1 - 0.67 = 0.33? - person Christoffer Hammarström; 14.03.2012
comment
Приведенный выше код возвращает 0.6649897, который я округляю до 0.66. Я не уверен, что точные значения имеют слишком большое значение, дело в том, что вы можете ожидать, что одно из предыдущих 20 изображений будет повторяться примерно одно из каждых трех раз. - person Dave Webb; 14.03.2012

Человеку свойственно видеть закономерности, которых нет. Многие люди считают, что закономерности в планетах и ​​звездах определяют их жизнь.

В первых 1000 цифр числа PI шесть девяток подряд. Означает ли это, что цифры PI не случайны? нет. Паттерн не повторяется больше, чем вы могли бы ожидать.

Сказав это, Random не является полностью случайным и будет повторяться после 2 ^ 48 вызовов. (он использует 48-битное начальное число) Это означает, что невозможно создать все возможные long или double, используя его. Если вы хотите больше случайности, вы можете вместо этого использовать SecureRandom с перемешиванием.

Похоже, что вы хотите что-то вроде этого

List<String> imagePaths = new ArrayList<>();

// called repeatedly
if (imagePaths.size() <= 500) {
    imagePaths = get1000Images();
    Collections.shuffle(imagePaths);
}

for (String path: imagePaths.subList(0, 20)) {
  ... do something with the path ...
}

imagePaths = imagePaths.subList(20, imagePaths.size());

Это гарантирует, что вы не увидите одно и то же изображение в последних 500 вызовах.

person Peter Lawrey    schedule 14.03.2012
comment
Я тоже об этом думал сегодня утром :) Спасибо, что нашли время для примера кода... - person basZero; 14.03.2012
comment
Чтобы использовать SecureRandom, вы можете сделать следующее: Collections.shuffle(imagePaths, new SecureRandom()); - person Ascalonian; 04.02.2015

Ваша интуиция верна для определенного изображения [вы вряд ли будете видеть конкретное изображение снова и снова], но не для общего изображения [вы, вероятно, увидите какое-то изображение повторяющийся]. Вероятно, это одно из мест, где наша автоматическая интуиция ошибается...

Это напоминает мне парадокс дня рождения, который противоречит интуиции и говорит - для группы из 23 человек вероятность того, что двое из них родятся в один день, составляет 0,5, что намного больше, чем предполагает интуиция!

person amit    schedule 14.03.2012

Я перетасовывал 52 карты четыре раза и отмечал каждый раз, когда каждая итерация повторяла одну и ту же карту в одном и том же слоте, что дало мне примерно 14 из 208 карт, что было примерно на 93,3% случайным.

person Nicholas    schedule 24.10.2013

Следуя вашему вопросу, я написал следующую программу. Я создал список последовательных целых чисел и перетасовал его 10, 100, 1000 и 10000 раз. После каждой серии перетасовок я проверял значение элемента на 5-й позиции массива и создавал массив счетчиков: сколько раз каждое число встречается на 5-й позиции.

Вот программа:

public class MyTest {
    public static void main(String[] args) {
        int n = 10;
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0;  i < n;  i++) {
            list.add(i);
        }

        int[] counters = new int[n];

        for(int shuffles : new int[] {10, 100, 1000, 10000}) {
            Arrays.fill(counters, 0);
            for (int i = 0;  i < shuffles; i++) {
                Collections.shuffle(list);
                // check 5-th element
                int fifth = list.get(5);
                counters[fifth] = counters[fifth] + 1;
            }
            System.out.println(shuffles + ": " + Arrays.toString(counters));
        }
    }
}

И вот результаты:

10: [0, 1, 1, 1, 2, 0, 0, 3, 2, 0] 100: [11, 9, 9, 7, 10, 12, 13, 13, 8, 8] 1000: [100, 101, 107, 101, 95, 96, 109, 83, 93, 115] 10000: [1015, 942, 990, 1003, 1015, 1037, 977, 1060, 950, 1011]

Как видите, «случайность» зависит от количества перетасовок. Если вы перетасовываете массив 10 раз, минимальный счетчик равен 0, а максимальный — 3. Разница между этими значениями для 100 перетасовок (в процентах) намного меньше. Цифры почти одинаковы для 10000 перетасовок.

Я думаю, что этот тест моделирует ваш вариант использования: вы показываете изображения в определенной позиции перетасованной коллекции.

См. сообщение @amit, в котором описывается значение перемешивания.

Итак, решение для вас состоит в том, чтобы перетасовать массив 10 раз.

РЕДАКТИРОВАТЬ: @Dave Webb дал идеальное объяснение этому делу.

Второе соображение заключается в следующем: на самом деле вам не нужно тасовать список из 1000 элементов, чтобы взять из него 20 первых элементов. Достаточно взять 20 случайных элементов. Вы получите тот же эффект, но гораздо более эффективное решение:

Set<Image> show = new HashSet<Image>();
Random r = new Random(System.currentTimeMillis());
for (int i = 0;  show.size() < 20;  i++) {
    show.add(list.get(r.nextInt()));
}
person AlexR    schedule 14.03.2012
comment
Отлично, и мне нравится ваше предложение выбрать 20 случайных элементов вместо того, чтобы перетасовывать их 10 раз... Простое примечание: выбор 20 случайных элементов также может привести к выбору одного и того же дважды. Так что это нужно немного изменить, но ваш пример кода — хорошее начало! - person basZero; 14.03.2012
comment
@basZero, в моем образце кода учтено, что одни и те же элементы используются дважды: я использовал Set для хранения результатов и повторял до тех пор, пока размер набора не стал 20. - person AlexR; 14.03.2012
comment
Верно, извините, я думал, что вы использовали бы List - person basZero; 14.03.2012
comment
Но r.nextInt() надо заменить на r.nextInt() % list.size(), нет? - person basZero; 14.03.2012
comment
@basZero И есть действительно простое (и гораздо более эффективное) решение, позволяющее избежать этой проблемы; см. здесь. Да, верно, если бы мы сделали это правильно, мы бы просто повторно реализовали алгоритм перемешивания, который уже используется. Чтобы доказать случайность, вы никогда не верите своей интуиции — она всегда будет ошибочной. Для этого есть статистические тесты (Хи-квадрат, Колмогорова-Смирнова,..). Также никогда не делайте nextInt() % size, если вам нужен однородный дистрибутив, очевидно, что это сработает только в редких случаях. - person Voo; 14.03.2012
comment
@Voo Итак, вы говорите: повторите реализацию ... Но тогда, прежде чем тратить 1 час или более на окончательный алгоритм, я бы предпочел использовать 10 раз вызов Collections.shuffle() ... - person basZero; 14.03.2012
comment
@basZero Вызов перетасовки несколько раз не имеет никакого смысла с точки зрения статистики (если вы считаете иначе, я всегда играю в некоторые тесты хи-квадрат, обратите внимание, что не выглядит случайным неинтересно). Но дело в том, что алгоритм перемешивания в основном удаляет случайные элементы из списка. Следовательно, отредактированное решение в основном представляет собой не очень эффективный алгоритм перетасовки с некоторыми ошибками. - person Voo; 14.03.2012
comment
@Voo спасибо Voo, я не смотрел реализацию shuffl() в JavaSE6, поэтому, если она работает так же, как в приведенном выше примере для случайно выбранных элементов, она бесполезна. Но комментарий интересный: вызов 10x shuffle() лучше, чем один раз, но 100x не дает дополнительной случайности... Что вы думаете об этом утверждении? - person basZero; 14.03.2012
comment
@basZero Откуда вы взяли, что я говорю, что вызывать 10-кратное перемешивание лучше, чем один раз? Только подумайте об этом на секунду: Shuffle равномерно распределяет все элементы списка, не обращая внимания на их более раннее положение. Следовательно, вызов его несколько раз подряд означает, что все, кроме последнего вызова, бесполезны. - person Voo; 14.03.2012
comment
@Voo Я не говорил, что ты это сказал. См. этот пост от AlexR - person basZero; 14.03.2012

С этим кодом, если вы видите одно и то же изображение снова и снова, это означает, что одно и то же изображение присутствует в списке много раз. Откуда бы вы ни взяли свои 1000 изображений, всегда есть дубликаты.

person Graham Borland    schedule 14.03.2012
comment
Я могу гарантировать, что все изображения в списке разные. они поступают непосредственно из индекса lucene, где путь является «первичным ключом» в индексе lucene. - person basZero; 14.03.2012
comment
Если ваш код действительно такой, как у вас есть, когда вы просто перебираете список и не изменяете список после начальной перетасовки, тогда единственный способ вы можете получить дубликаты в выбранных вами 20 изображениях, если в списке есть дубликаты для начала. Collections.shuffle() не вставляет копии, а просто переупорядочивает существующие элементы. - person Graham Borland; 14.03.2012
comment
Он видит одно и то же изображение среди выбранных 20 снова и снова при нескольких последующих перетасовках. - person aioobe; 14.03.2012