Создание всех индексов последовательности, как правило, является плохой идеей, так как это может занять много времени, особенно если соотношение выбираемых чисел 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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
}
}
Где 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 {
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset {
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;
return r_value < n.n + n_index;
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return n.n + n_index < r_value;
}
};
И наконец:
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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
}
}
Я проверил это на трех тестах. Сначала было выбрано 3 числа из 7 элементов, а гистограмма выбранных элементов была собрана за 10 000 прогонов:
4265 4229 4351 4267 4267 4364 4257
Это показывает, что каждый из 7 пунктов был выбран примерно одинаковое количество раз, и нет явного отклонения, вызванного алгоритмом. Все последовательности также проверялись на правильность (уникальность содержания).
Второй тест включал выбор 7 чисел из 5000 пунктов. Время нескольких версий алгоритма было набрано более 10 000 000 прогонов. Результаты обозначаются в комментариях кода как b1
. Простая версия алгоритма немного быстрее.
Третий тест включал выбор 700 номеров из 5000 элементов. Снова набралось время нескольких версий алгоритма, на этот раз более 10 000 прогонов. Результаты обозначаются в комментариях кода как b2
. Версия алгоритма бинарного поиска теперь более чем в два раза быстрее, чем простая.
Второй метод начинает быстрее выбирать более 75 элементов на моей машине (обратите внимание, что сложность любого алгоритма не зависит от количества элементов MAX
).
Стоит отметить, что приведенные выше алгоритмы генерируют случайные числа в порядке возрастания. Но было бы просто добавить еще один массив, в котором числа будут сохраняться в том порядке, в котором они были сгенерированы, и вместо этого вернуть его (с незначительной дополнительной стоимостью O(n)
). Нет необходимости перетасовывать вывод: это будет намного медленнее.
Обратите внимание, что исходники написаны на C ++, у меня на машине нет Java, но концепция должна быть ясной.
ИЗМЕНИТЬ :
Для развлечения я также реализовал подход, который генерирует список со всеми индексами
0 .. MAX
, выбирает их случайным образом и удаляет их из списка, чтобы гарантировать уникальность. Поскольку я выбрал достаточно высокое MAX
(5000), производительность катастрофическая:
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
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);
rand_num.push_back(all_numbers[n]);
all_numbers.erase(all_numbers.begin() + n);
}
Я также реализовал подход с помощью set
(коллекции C ++), которая фактически занимает второе место в тесте b2
, будучи лишь примерно на 50% медленнее, чем подход с двоичным поиском. Это понятно, так как set
используется двоичное дерево, стоимость вставки которого аналогична двоичному поиску. Единственное отличие - шанс получить повторяющиеся предметы, что замедляет прогресс.
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1));
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
Полный исходный код здесь .