лучший способ выбрать случайный набор из коллекции? - PullRequest
66 голосов
/ 26 сентября 2008

У меня есть набор объектов в векторе, из которого я хотел бы выбрать случайное подмножество (например, 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())));
}

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

Ответы [ 10 ]

10 голосов
/ 26 сентября 2008

Джон Бентли обсуждает это либо в «Программировании жемчуга», либо в «Более программировании жемчуга». Вы должны быть осторожны с процессом выбора N из M, но я думаю, что приведенный код работает правильно. Вместо случайного перемешивания всех элементов, вы можете выполнить случайное перемешивание, только перетасовывая первые N позиций - что полезно при N << M. </p>

Кнут также обсуждает эти алгоритмы - я полагаю, что это будет Том 3 "Сортировка и поиск", но мой набор упакован в ожидании переезда, поэтому я не могу официально проверить это.

8 голосов
/ 26 сентября 2008

@ Джонатан,

Я полагаю, что это решение, о котором вы говорите:

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

Это на странице 127 «Программирование жемчуга» Джона Бентли и основано на реализации Кнута.

РЕДАКТИРОВАТЬ: я только что увидел дополнительную модификацию на странице 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 элементы массива ..."

4 голосов
/ 26 сентября 2008

Если вы пытаетесь выбрать k отдельных элементов из списка из n, приведенные выше методы будут O (n) или O (kn), потому что удаление элемента из вектора приведет к сдвигу массива элементы вниз.

Поскольку вы запрашиваете наилучший способ, это зависит от того, что вам разрешено делать с вашим списком ввода.

Если допустимо изменить список ввода, как в ваших примерах, то вы можете просто поменять k случайных элементов в начале списка и вернуть их за O (k) время следующим образом:

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 (например, 5 из 100), было бы намного лучше не удалять выбранные элементы каждый раз, а просто выбирать каждый элемент, и если Вы когда-либо получаете дубликат, выбрасываете его и повторно выбираете. Это даст вам O (kn / (n-k)), который все еще близок к O (k), когда n доминирует над k. (Например, если k меньше n / 2, то оно уменьшается до O (k)).

Если в k не доминирует n, и вы не можете изменить список, вы также можете скопировать свой исходный список и использовать свое первое решение, потому что O (n) будет так же хорошо, как O (k).

Как уже отмечали другие, если вы зависите от сильной случайности, где возможен (и беспристрастен) каждый подсписок, вам определенно понадобится что-то более сильное, чем java.util.Random. Смотри java.security.SecureRandom.

4 голосов
/ 26 сентября 2008

Я написал эффективную реализацию этого несколько недель назад. Это на C #, но перевод на Java тривиален (по сути, тот же код). Плюсом является то, что это также совершенно беспристрастно (что не так с некоторыми из существующих ответов) - способ проверить это здесь .

Он основан на реализации Дюрстенфельдом шаффла Фишера-Йейтса.

2 голосов
/ 26 сентября 2008

Ваше второе решение использования Random для выбора элемента кажется разумным, однако:

0 голосов
/ 04 марта 2012

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

Соль 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 числа, x, которые взаимно просты с n, т.е. chhose gcd (x, n) = 1, и другое, a, которое является «отправной точкой», - затем ряд : a% n, a + x% n, a + 2 * x% n, ... a + (k-1) * x% n - это последовательность различных чисел (до тех пор, пока k <= n). </p>

0 голосов
/ 26 сентября 2008

Этот очень похож на вопрос stackoverflow.

Подводя итог моим любимым ответам с этой страницы (самый лучший от пользователя Кайл):

  • O (n) решение : перебрать свой список и скопировать элемент (или ссылку на него) с вероятностью (#needed / #remaining). Пример: если k = 5 и n = 100, то вы берете первый элемент с вероятностью 5/100. Если вы копируете это, то вы выбираете следующее с пробой 4/99; но если вы не взяли первый, вероятность 5/99.
  • O (k log k) или O (k 2 ) : создание отсортированного списка из k индексов (чисел в {0, 1, ..., n-1) }) случайным образом выбирая число = 43, то вы добавляете 1 к нему. Таким образом, если ваш второй выбор 50, вы добавляете 1 к нему, и у вас есть {43, 51}. Если ваш следующий выбор - 51, вы добавляете 2 , чтобы получить {43, 51, 53}.

Вот немного псевдопиона -

# 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 (k 2 ) или O (k log k), потому что это зависит от того, насколько быстро вы можете искать и вставлять в свой контейнер для с. Если s - нормальный список, одна из этих операций является линейной, и вы получаете k ^ 2. Однако, если вы хотите построить s как сбалансированное двоичное дерево, вы можете получить время O (k log k).

0 голосов
/ 26 сентября 2008
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 голосов
/ 26 сентября 2008

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

0 голосов
/ 26 сентября 2008

Сколько стоит убрать стоимость? Потому что если для этого нужно переписать массив в новый фрагмент памяти, то вы сделали O (5n) операций во второй версии, а не 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) в случайном порядке, но никаких коллизий из внутреннего контроллера проверки и меньше, чем потенциальный O (5n), который может удалить, может стоить.

...