Генерація всіх індексів послідовності, як правило, погана ідея, оскільки це може зайняти багато часу, особливо якщо співвідношення чисел, які слід обрати, 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 елементів, а гістограма вибраних предметів була накопичена за 10000 прогонів:
4265 4229 4351 4267 4267 4364 4257
Це показує, що кожен із 7 пунктів був обраний приблизно однаковим числом разів, і немає явного упередження, спричиненого алгоритмом. Усі послідовності також перевірялись на правильність (унікальність змісту).
Другий орієнтир передбачав вибір 7 чисел із 5000 предметів. Час декількох версій алгоритму було накопичено понад 10 000 000 прогонів. Результати позначаються в коментарях у коді як b1. Проста версія алгоритму трохи швидша.
Третій орієнтир передбачав вибір 700 чисел із 5000 предметів. Час кількох версій алгоритму знову накопичився, цього разу понад 10000 прогонів. Результати позначаються в коментарях у коді як b2. Бінарний варіант пошуку алгоритму зараз більш ніж у два рази швидший, ніж простий.
Другий метод починає бути швидшим для вибору понад cca 75 елементів на моїй машині (зауважте, що складність будь-якого алгоритму не залежить від кількості елементів, MAX).
Варто згадати, що вищезазначені алгоритми генерують випадкові числа у порядку зростання. Але було б просто додати ще один масив, до якого числа будуть зберігатися в тому порядку, в якому вони були згенеровані, і повертати його натомість (за незначно додаткових витрат O(n)). Не потрібно перетасовувати результати: це було б набагато повільніше.
Зверніть увагу, що джерела знаходяться на C ++, у мене немає машини на моїй машині, але концепція повинна бути чіткою.
РЕДАГУВАТИ :
Для розваги я також застосував підхід, який формує список з усіма індексами
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());
Повний вихідний код тут .