Generowanie wszystkich indeksów sekwencji jest ogólnie złym pomysłem, ponieważ może zająć dużo czasu, zwłaszcza jeśli stosunek liczb do wyboru MAX
jest niski (złożoność staje się dominująca O(MAX)
). Sytuacja pogarsza się, gdy stosunek liczb do wyboru MAX
zbliża się do jedności, ponieważ wtedy usunięcie wybranych wskaźników z ciągu wszystkich również staje się kosztowne (podchodzimy doO(MAX^2/2)
). Ale w przypadku małych liczb działa to zwykle dobrze i nie jest szczególnie podatne na błędy.
Filtrowanie wygenerowanych indeksów za pomocą kolekcji jest również złym pomysłem, ponieważ trochę czasu zajmuje wstawianie indeksów do sekwencji, a postęp nie jest gwarantowany, ponieważ ta sama liczba losowa może być losowana kilka razy (ale dla wystarczająco dużej MAX
jest to mało prawdopodobne ). Może to być bardzo skomplikowane
O(k n log^2(n)/2)
, zignorowanie duplikatów i założenie, że kolekcja używa drzewa do wydajnego wyszukiwania (ale przy znacznym stałym koszcie k
przydzielania węzłów drzewa i prawdopodobnie konieczności ponownego równoważenia ).
Inną opcją jest generowanie wartości losowych w sposób unikalny od samego początku, gwarantując postęp. Oznacza to, że w pierwszej rundzie [0, MAX]
generowany jest losowy indeks w :
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
W drugiej rundzie [0, MAX - 1]
generowany jest tylko (ponieważ jeden element został już wybrany):
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)
Wartości indeksów należy następnie skorygować: jeśli drugi wskaźnik przypada w drugiej połowie ciągu (po pierwszym indeksie), należy go zwiększyć, aby uwzględnić lukę. Możemy to zaimplementować jako pętlę, co pozwala nam wybrać dowolną liczbę unikalnych elementów.
Dla krótkich sekwencji jest to dość szybki O(n^2/2)
algorytm:
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);
}
}
Gdzie n_select_num
jest twoja 5 i n_number_num
jest twoja MAX
. Do n_Rand(x)
zwraca losowe liczby całkowite [0, x]
(włącznie). Można to zrobić nieco szybciej, wybierając wiele elementów (np. Nie 5, ale 500), używając wyszukiwania binarnego do znalezienia punktu wstawienia. Aby to zrobić, musimy upewnić się, że spełniamy wymagania.
Przeprowadzimy wyszukiwanie binarne z porównaniem, n + j < rand_num[j]
które jest takie samo jak
n < rand_num[j] - j
. Musimy pokazać, że rand_num[j] - j
nadal jest to posortowana sekwencja dla posortowanej sekwencji rand_num[j]
. Na szczęście jest to łatwe do pokazania, ponieważ najmniejsza odległość między dwoma elementami oryginału rand_num
to jeden (wygenerowane liczby są unikalne, więc zawsze jest różnica co najmniej 1). Jednocześnie, jeśli odejmiemy wskaźniki j
od wszystkich elementów
rand_num[j]
, różnice w indeksie wynoszą dokładnie 1. Tak więc w „najgorszym” przypadku otrzymujemy stałą sekwencję - ale nigdy nie malejącą. Można zatem użyć wyszukiwania binarnego, uzyskując O(n log(n))
algorytm:
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;
}
};
I w końcu:
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());
}
}
Przetestowałem to na trzech testach porównawczych. Najpierw wybrano 3 liczby z 7 pozycji, a histogram wybranych pozycji zgromadził ponad 10000 serii:
4265 4229 4351 4267 4267 4364 4257
Pokazuje to, że każdy z 7 elementów został wybrany mniej więcej taką samą liczbę razy i nie ma wyraźnego błędu spowodowanego przez algorytm. Wszystkie sekwencje zostały również sprawdzone pod kątem poprawności (niepowtarzalności treści).
Drugi benchmark obejmował wybór 7 liczb spośród 5000 pozycji. W czasie kilku wersji algorytmu zgromadzono ponad 10 000 000 uruchomień. Wyniki są oznaczane w komentarzach w kodzie jako b1
. Prosta wersja algorytmu jest nieco szybsza.
Trzeci benchmark obejmował wybór 700 liczb z 5000 pozycji. Ponownie skumulowano czas kilku wersji algorytmu, tym razem ponad 10 000 przebiegów. Wyniki są oznaczane w komentarzach w kodzie jako b2
. Wersja algorytmu wyszukiwania binarnego jest teraz ponad dwa razy szybsza niż wersja prosta.
Druga metoda zaczyna być szybsza w przypadku wybierania więcej niż około 75 pozycji na moim komputerze (zwróć uwagę, że złożoność obu algorytmów nie zależy od liczby pozycji MAX
).
Warto wspomnieć, że powyższe algorytmy generują liczby losowe w kolejności rosnącej. Ale byłoby łatwo dodać kolejną tablicę, do której liczby byłyby zapisywane w kolejności, w jakiej zostały wygenerowane, i zamiast tego zwracać ją (przy nieistotnych dodatkowych kosztach O(n)
). Nie ma potrzeby tasowania wyjścia: byłoby to znacznie wolniejsze.
Zauważ, że źródła są w C ++, nie mam Javy na moim komputerze, ale koncepcja powinna być jasna.
EDYCJA :
Dla rozrywki zaimplementowałem również podejście, które generuje listę ze wszystkimi indeksami
0 .. MAX
, wybiera je losowo i usuwa z listy, aby zagwarantować unikalność. Ponieważ wybrałem dość wysoką MAX
(5000), wydajność jest katastrofalna:
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);
}
Zaimplementowałem również to podejście z set
(kolekcją C ++), która faktycznie zajmuje drugie miejsce w teście porównawczym b2
, będąc tylko o około 50% wolniejsze niż podejście z wyszukiwaniem binarnym. Jest to zrozumiałe, ponieważ set
wykorzystuje drzewo binarne, w którym koszt wstawienia jest podobny do wyszukiwania binarnego. Jedyna różnica to szansa na zdobycie zduplikowanych przedmiotów, co spowalnia postęp.
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());
Pełny kod źródłowy jest tutaj .