Создание случайных чисел без дубликатов - PullRequest
80 голосов
/ 28 октября 2010

В этом случае MAX только 5, так что я могу проверить дубликаты один за другим, но как я могу сделать это проще?Например, что если MAX имеет значение 20?Спасибо.

int MAX = 5;

for (i = 1 , i <= MAX; i++)
{
        drawNum[1] = (int)(Math.random()*MAX)+1;

        while (drawNum[2] == drawNum[1])
        {
             drawNum[2] = (int)(Math.random()*MAX)+1;
        }
        while ((drawNum[3] == drawNum[1]) || (drawNum[3] == drawNum[2]) )
        {
             drawNum[3] = (int)(Math.random()*MAX)+1;
        }
        while ((drawNum[4] == drawNum[1]) || (drawNum[4] == drawNum[2]) || (drawNum[4] == drawNum[3]) )
        {
             drawNum[4] = (int)(Math.random()*MAX)+1;
        }
        while ((drawNum[5] == drawNum[1]) ||
               (drawNum[5] == drawNum[2]) ||
               (drawNum[5] == drawNum[3]) ||
               (drawNum[5] == drawNum[4]) )
        {
             drawNum[5] = (int)(Math.random()*MAX)+1;
        }

}

Ответы [ 16 ]

144 голосов
/ 28 октября 2010

Самый простой способ - создать список возможных чисел (1..20 или любой другой), а затем перемешать их с помощью Collections.shuffle. Тогда просто возьмите столько элементов, сколько захотите. Это замечательно, если ваш диапазон равен количеству элементов, которые вам нужны в конце (например, для перетасовки колоды карт).

Это не очень хорошо работает, если вы хотите (скажем) 10 случайных элементов в диапазоне 1..10,000 - вы в конечном итоге сделаете много работы без необходимости. В этот момент, вероятно, лучше сохранить набор значений, которые вы сгенерировали до сих пор, и просто продолжать генерировать числа в цикле, пока не появится следующее значение:

if (max < numbersNeeded)
{
    throw new IllegalArgumentException("Can't ask for more numbers than are available");
}
Random rng = new Random(); // Ideally just create one instance globally
// Note: use LinkedHashSet to maintain insertion order
Set<Integer> generated = new LinkedHashSet<Integer>();
while (generated.size() < numbersNeeded)
{
    Integer next = rng.nextInt(max) + 1;
    // As we're adding to a set, this will automatically do a containment check
    generated.add(next);
}

Будьте осторожны с выбором набора - я очень сознательно использовал LinkedHashSet, поскольку он поддерживает порядок вставки, о котором мы здесь заботимся.

Еще один вариант - всегда делать успехи, уменьшая каждый раз диапазон и компенсируя существующие значения. Например, предположим, что вы хотели 3 значения в диапазоне 0..9. На первой итерации вы сгенерируете любое число в диапазоне 0..9 - скажем, вы сгенерируете 4.

На второй итерации вы затем сгенерируете число в диапазоне 0..8. Если сгенерированное число меньше 4, вы бы оставили его как есть ... в противном случае вы добавите его к нему. Это дает вам диапазон результатов 0,9 без 4. Предположим, что мы получаем 7 таким образом.

На третьей итерации вы генерируете число в диапазоне 0..7. Если сгенерированное число меньше 4, вы бы оставили его как есть. Если это 4 или 5, вы бы добавили один. Если это 6 или 7, вы бы добавили два. Таким образом, диапазон результатов равен 0,9 без 4 или 6.

18 голосов
/ 28 октября 2010

Вот как я бы это сделал

import java.util.ArrayList;
import java.util.Random;

public class Test {
    public static void main(String[] args) {
        int size = 20;

        ArrayList<Integer> list = new ArrayList<Integer>(size);
        for(int i = 1; i <= size; i++) {
            list.add(i);
        }

        Random rand = new Random();
        while(list.size() > 0) {
            int index = rand.nextInt(list.size());
            System.out.println("Selected: "+list.remove(index));
        }
    }
}

Как отметил уважаемый мистер Скит:
Если n - это число случайно выбранных чисел, которые вы хотите выбратьи N - это общее пространство выборки чисел, доступных для выбора:

  1. Если n << <em>N , вам следует просто сохранитьчисла, которые вы выбрали, и проверьте список, чтобы увидеть, есть ли в нем выбранное число.
  2. Если n ~ = N , вам, вероятно, следует использовать мой методпутем заполнения списка, содержащего все пробное пространство, а затем удаления чисел из него по мере их выбора.
14 голосов
/ 13 апреля 2012
//random numbers are 0,1,2,3 
ArrayList<Integer> numbers = new ArrayList<Integer>();   
Random randomGenerator = new Random();
while (numbers.size() < 4) {

    int random = randomGenerator .nextInt(4);
    if (!numbers.contains(random)) {
        numbers.add(random);
    }
}
4 голосов
/ 16 июня 2015

Другой подход, который позволяет вам указать, сколько чисел вы хотите с size и min и max значениями возвращенных чисел

public static int getRandomInt(int min, int max) {
    Random random = new Random();

    return random.nextInt((max - min) + 1) + min;
}

public static ArrayList<Integer> getRandomNonRepeatingIntegers(int size, int min,
        int max) {
    ArrayList<Integer> numbers = new ArrayList<Integer>();

    while (numbers.size() < size) {
        int random = getRandomInt(min, max);

        if (!numbers.contains(random)) {
            numbers.add(random);
        }
    }

    return numbers;
}

Чтобы использовать его, возвращаем 7 цифр от 0 до 25.

    ArrayList<Integer> list = getRandomNonRepeatingIntegers(7, 0, 25);
    for (int i = 0; i < list.size(); i++) {
        System.out.println("" + list.get(i));
    }
3 голосов
/ 16 декабря 2014

Генерация всех индексов последовательности, как правило, является плохой идеей, поскольку это может занять много времени, особенно если отношение чисел, которые нужно выбрать, к MAX низкое (сложность становится доминируемой O(MAX)).Это ухудшается, если отношение чисел, которые будут выбраны, к MAX приближается к единице, так как тогда удаление выбранных индексов из последовательности всех также становится дорогим (мы приближаемся к O(MAX^2/2)).Но для небольших чисел это обычно работает хорошо и не особенно подвержено ошибкам.

Фильтрация сгенерированных индексов с использованием коллекции также является плохой идеей, поскольку некоторое время затрачивается на вставку индексов в последовательность,и прогресс не гарантируется, поскольку одно и то же случайное число может быть нарисовано несколько раз (но для достаточно большого MAX это маловероятно).Это может быть близко к сложности
O(k n log^2(n)/2), игнорируя дубликаты и предполагая, что коллекция использует дерево для эффективного поиска (но со значительными постоянными затратами k на выделение узлов дерева и, возможно, с ребалансировкой ).

Другой вариант - генерировать случайные значения уникальным образом с самого начала, гарантируя прогресс.Это означает, что в первом раунде генерируется случайный индекс в [0, MAX]:

items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0       ^^             (index 2)

Во втором раунде генерируется только [0, MAX - 1] (так как один элемент уже выбран):

items i0 i1    i3 i4 i5 i6 (total 6 items)
idx 1          ^^          (index 2 out of these 6, but 3 out of the original 7)

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

Для коротких последовательностей это довольно быстрый алгоритм O(n^2/2):

void RandomUniqueSequence(std::vector<int> &rand_num,
    const size_t n_select_num, const size_t n_item_num)
{
    assert(n_select_num <= n_item_num);

    rand_num.clear(); // !!

    // b1: 3187.000 msec (the fastest)
    // b2: 3734.000 msec
    for(size_t i = 0; i < n_select_num; ++ i) {
        int n = n_Rand(n_item_num - i - 1);
        // get a random number

        size_t n_where = i;
        for(size_t j = 0; j < i; ++ j) {
            if(n + j < rand_num[j]) {
                n_where = j;
                break;
            }
        }
        // see where it should be inserted

        rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
        // insert it in the list, maintain a sorted sequence
    }
    // tier 1 - use comparison with offset instead of increment
}

Где n_select_numэто ваши 5 и n_number_num это ваш MAX.n_Rand(x) возвращает случайные целые числа в [0, x] (включительно).Это можно сделать немного быстрее, если выбрать много элементов (например, не 5, а 500) с помощью двоичного поиска, чтобы найти точку вставки.Для этого нам нужно убедиться, что мы удовлетворяем требованиям.

Мы выполним бинарный поиск со сравнением n + j < rand_num[j], которое совпадает с
n < rand_num[j] - j.Нам нужно показать, что rand_num[j] - j все еще является отсортированной последовательностью для отсортированной последовательности rand_num[j].К счастью, это легко показать, поскольку наименьшее расстояние между двумя элементами оригинального rand_num равно единице (сгенерированные числа уникальны, поэтому всегда есть разница не менее 1).В то же время, если мы вычтем индексы j из всех элементов
rand_num[j], различия в индексе будут ровно 1. Так что в «худшем» случае мы получим постоянную последовательность - но никогда не уменьшаться.Поэтому можно использовать бинарный поиск, в результате чего получается алгоритм O(n log(n)):

struct TNeedle { // in the comparison operator we need to make clear which argument is the needle and which is already in the list; we do that using the type system.
    int n;

    TNeedle(int _n)
        :n(_n)
    {}
};

class CCompareWithOffset { // custom comparison "n < rand_num[j] - j"
protected:
    std::vector<int>::iterator m_p_begin_it;

public:
    CCompareWithOffset(std::vector<int>::iterator p_begin_it)
        :m_p_begin_it(p_begin_it)
    {}

    bool operator ()(const int &r_value, TNeedle n) const
    {
        size_t n_index = &r_value - &*m_p_begin_it;
        // calculate index in the array

        return r_value < n.n + n_index; // or r_value - n_index < n.n
    }

    bool operator ()(TNeedle n, const int &r_value) const
    {
        size_t n_index = &r_value - &*m_p_begin_it;
        // calculate index in the array

        return n.n + n_index < r_value; // or n.n < r_value - n_index
    }
};

И наконец:

void RandomUniqueSequence(std::vector<int> &rand_num,
    const size_t n_select_num, const size_t n_item_num)
{
    assert(n_select_num <= n_item_num);

    rand_num.clear(); // !!

    // b1: 3578.000 msec
    // b2: 1703.000 msec (the fastest)
    for(size_t i = 0; i < n_select_num; ++ i) {
        int n = n_Rand(n_item_num - i - 1);
        // get a random number

        std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
            TNeedle(n), CCompareWithOffset(rand_num.begin()));
        // see where it should be inserted

        rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
        // insert it in the list, maintain a sorted sequence
    }
    // tier 4 - use binary search
}

Я проверил это на трех тестах.Во-первых, 3 числа были выбраны из 7 элементов, и гистограмма выбранных элементов была собрана за 10000 прогонов:

4265 4229 4351 4267 4267 4364 4257

Это показывает, что каждый из 7 элементов был выбран примерно одинаковое количество раз,и нет никакого очевидного смещения, вызванного алгоритмом.Все последовательности также были проверены на правильность (уникальность содержания).

Второй критерий включал выбор 7 чисел из 5000 наименований.За время нескольких версий алгоритма было накоплено более 10 000 000 прогонов.Результаты обозначаются в комментариях в коде как b1.Простая версия алгоритма немного быстрее.

Третий тест - выбор 700 номеров из 5000.Время нескольких версий алгоритма было снова накоплено, на этот раз более 10000 прогонов.Результаты обозначаются в комментариях в коде как b2.Бинарная поисковая версия алгоритма теперь более чем в два раза быстрее, чем простая.

Второй метод начинает работать быстрее при выборе более чем приблизительно 75 элементов на моем компьютере (обратите внимание, что сложность любого алгоритма делаетне зависит от количества предметов, MAX).

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

Обратите внимание, что исходники находятся в C ++, у меня нет Java на моей машине, но концепция должна быть ясной.

РЕДАКТИРОВАТЬ :

Для развлечения я также применил подход, который генерирует список со всеми индексами
0 .. MAX, выбирает их случайным образом и удаляет их из списка вгарантия уникальности.Так как я выбрал довольно высокий MAX (5000), производительность катастрофическая:

// b1: 519515.000 msec
// b2: 20312.000 msec
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
// generate all the numbers

for(size_t i = 0; i < n_number_num; ++ i) {
    assert(all_numbers.size() == n_item_num - i);
    int n = n_Rand(n_item_num - i - 1);
    // get a random number

    rand_num.push_back(all_numbers[n]); // put it in the output list
    all_numbers.erase(all_numbers.begin() + n); // erase it from the input
}
// generate random numbers

Я также реализовал подход с set (коллекция C ++), которая на самом деле идет вторым послебенчмарк b2, только на 50% медленнее, чем подход с двоичным поиском.Это понятно, поскольку set использует двоичное дерево, где стоимость вставки аналогична двоичному поиску.Единственное отличие - это шанс получить дубликаты предметов, что замедляет прогресс.

// b1: 20250.000 msec
// b2: 2296.000 msec
std::set<int> numbers;
while(numbers.size() < n_number_num)
    numbers.insert(n_Rand(n_item_num - 1)); // might have duplicates here
// generate unique random numbers

rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
// copy the numbers from a set to a vector

Полный исходный код здесь .

3 голосов
/ 02 октября 2012

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

// get 5 unique random numbers, possible values 0 - 19
// (assume desired number of selections < number of choices)

const int POOL_SIZE = 20;
const int VAL_COUNT = 5;

declare Array mapping[POOL_SIZE];
declare Array results[VAL_COUNT];

declare i int;
declare r int;
declare max_rand int;

// create mapping array
for (i=0; i<POOL_SIZE; i++) {
   mapping[i] = i;
}

max_rand = POOL_SIZE-1;  // start loop searching for maximum value (19)

for (i=0; i<VAL_COUNT; i++) {
    r = Random(0, max_rand); // get random number
    results[i] = mapping[r]; // grab number from map array
    mapping[r] = max_rand;  // place item past range at selected location

    max_rand = max_rand - 1;  // reduce random scope by 1
}

Предположим, что первая итерация генерирует случайное число 3 для начала (от 0 до 19).Это приведет к результатам [0] = mapping [3], т. Е. К значению 3. Затем мы назначим mapping [3] для 19.

На следующей итерации случайное число было 5 (из 0- 18)Это даст результаты [1] = mapping [5], т. Е. Значение 5. Затем мы назначим mapping [5] для 18.

Теперь предположим, что на следующей итерации снова выбрано 3 (из 0 - 17).результатам [2] будет присвоено значение отображения [3], но теперь это значение не 3, а 19.

Эта же защита сохраняется для всех чисел, даже если вы получили одно и то же число 5 разв ряд.Например, если бы генератор случайных чисел давал вам 0 пять раз подряд, результаты были бы: [0, 19, 18, 17, 16].

Вы никогда не получите одно и то же число дважды.

3 голосов
/ 24 сентября 2012

Есть еще один способ сделать "случайные" упорядоченные числа с LFSR, взгляните на:

http://en.wikipedia.org/wiki/Linear_feedback_shift_register

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

Но это не ИСТИННЫЕ случайные числа, потому что случайная генерация детерминирована.

Но в зависимости от вашего случая вы можете использовать эту технику, уменьшая объем обработки при генерации случайных чисел при использовании тасования.

Здесь алгоритм LFSR в Java, (я взял его где-то, я не помню):

public final class LFSR {
    private static final int M = 15;

    // hard-coded for 15-bits
    private static final int[] TAPS = {14, 15};

    private final boolean[] bits = new boolean[M + 1];

    public LFSR() {
        this((int)System.currentTimeMillis());
    }

    public LFSR(int seed) {
        for(int i = 0; i < M; i++) {
            bits[i] = (((1 << i) & seed) >>> i) == 1;
        }
    }

    /* generate a random int uniformly on the interval [-2^31 + 1, 2^31 - 1] */
    public short nextShort() {
        //printBits();

        // calculate the integer value from the registers
        short next = 0;
        for(int i = 0; i < M; i++) {
            next |= (bits[i] ? 1 : 0) << i;
        }

        // allow for zero without allowing for -2^31
        if (next < 0) next++;

        // calculate the last register from all the preceding
        bits[M] = false;
        for(int i = 0; i < TAPS.length; i++) {
            bits[M] ^= bits[M - TAPS[i]];
        }

        // shift all the registers
        for(int i = 0; i < M; i++) {
            bits[i] = bits[i + 1];
        }

        return next;
    }

    /** returns random double uniformly over [0, 1) */
    public double nextDouble() {
        return ((nextShort() / (Integer.MAX_VALUE + 1.0)) + 1.0) / 2.0;
    }

    /** returns random boolean */
    public boolean nextBoolean() {
        return nextShort() >= 0;
    }

    public void printBits() {
        System.out.print(bits[M] ? 1 : 0);
        System.out.print(" -> ");
        for(int i = M - 1; i >= 0; i--) {
            System.out.print(bits[i] ? 1 : 0);
        }
        System.out.println();
    }


    public static void main(String[] args) {
        LFSR rng = new LFSR();
        Vector<Short> vec = new Vector<Short>();
        for(int i = 0; i <= 32766; i++) {
            short next = rng.nextShort();
            // just testing/asserting to make 
            // sure the number doesn't repeat on a given list
            if (vec.contains(next))
                throw new RuntimeException("Index repeat: " + i);
            vec.add(next);
            System.out.println(next);
        }
    }
}
2 голосов
/ 09 сентября 2018

Это было бы намного проще в java-8:

Stream.generate(new Random()::ints)
            .distinct()
            .limit(16) // whatever limit you might need
            .toArray(Integer[]::new);
2 голосов
/ 27 декабря 2013

Ваша проблема, кажется, сводится к выбору k элементов случайным образом из набора n элементов.Ответ Collections.shuffle, таким образом, является правильным, но, как указано, неэффективным: его O (n).

Википедия: Fisher-Yates shuffle имеет версию O (k), когда массив уже существует.В вашем случае нет массива элементов, и создание массива элементов может быть очень дорогим, скажем, если бы max было 10000000 вместо 20.

Алгоритм перемешивания включает инициализацию массива размера n, где каждый элементравным его индексу, выбирая k случайных чисел каждое число в диапазоне с максимальным значением, меньшим, чем предыдущий диапазон, затем меняя элементы в конец массива.

Вы можете проделать ту же самую операцию за O (k) время с хэш-картой, хотя я допускаю ее вид боли.Обратите внимание, что это имеет смысл, только если k намного меньше, чем n.(т.е. k ~ lg (n) или около того), в противном случае вы должны использовать shuffle напрямую.

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

  1. Выберите k случайных чисел: первое находится в диапазоне от 0 до n-1, второе от 0 до n-2, третье от 0 до n-3 и так далее, через nk,

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

</p>

<code>int getValue(i)
{
    if (map.contains(i)) 
        return map[i];
    return i;
}

void setValue(i, val)
{   
    if (i == val)
        map.remove(i);
    else
        map[i] = val;
}

int[] chooseK(int n, int k)
{
    for (int i = 0; i < k; i++)
    {
        int randomIndex = nextRandom(0, n - i); //(n - i is exclusive)
        int desiredIndex = n-i-1;

        int valAtRandom = getValue(randomIndex);
        int valAtDesired = getValue(desiredIndex);

        setValue(desiredIndex, valAtRandom);
        setValue(randomIndex, valAtDesired);
    }

    int[] output = new int[k];
    for (int i = 0; i < k; i++)
    {
        output[i] = (getValue(n-i-1));
    }

    return output;
}
</code>

2 голосов
/ 14 января 2011

Вместо того, чтобы делать все это, создайте объект LinkedHashSet и случайные числа для него с помощью функции Math.random() .... если произойдет какая-либо дублирующаяся запись, объект LinkedHashSet не добавит это число в свой список ...Так как в этом классе коллекций повторяющиеся значения не допускаются .. в конце концов вы получаете список случайных чисел, не имеющих дублированных значений ....: D

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...