Generare tutti gli indici di una sequenza è generalmente una cattiva idea, in quanto potrebbe richiedere molto tempo, soprattutto se il rapporto tra i numeri da scegliere MAX
è basso (la complessità diventa dominata da O(MAX)
). Ciò peggiora se il rapporto tra i numeri da scegliere si MAX
avvicina a uno, poiché anche rimuovere gli indici scelti dalla sequenza di tutti diventa costoso (ci avviciniamo O(MAX^2/2)
). Ma per piccoli numeri, questo generalmente funziona bene e non è particolarmente soggetto a errori.
Anche filtrare gli indici generati utilizzando una raccolta è una cattiva idea, poiché un po 'di tempo viene speso per inserire gli indici nella sequenza e il progresso non è garantito poiché lo stesso numero casuale può essere estratto più volte (ma per abbastanza grande MAX
è improbabile ). Questo potrebbe essere vicino alla complessità
O(k n log^2(n)/2)
, ignorando i duplicati e assumendo che la raccolta utilizzi un albero per una ricerca efficiente (ma con un costo costante significativo per l' k
allocazione dei nodi dell'albero e possibilmente dover ribilanciare ).
Un'altra opzione è generare i valori casuali in modo univoco dall'inizio, garantendo il progresso. Ciò significa che nel primo round [0, MAX]
viene generato un indice casuale in :
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
Nel secondo round, [0, MAX - 1]
viene generato solo (poiché un elemento era già selezionato):
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)
I valori degli indici devono quindi essere adeguati: se il secondo indice cade nella seconda metà della sequenza (dopo il primo indice), deve essere incrementato per tenere conto del divario. Possiamo implementarlo come un ciclo, permettendoci di selezionare un numero arbitrario di elementi unici.
Per sequenze brevi, questo è un O(n^2/2)
algoritmo abbastanza veloce :
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);
}
}
Dov'è il n_select_num
tuo 5 ed n_number_num
è il tuo MAX
. I n_Rand(x)
rendimenti interi casuali a [0, x]
(compreso). Questo può essere reso un po 'più veloce se si selezionano molti elementi (es. Non 5 ma 500) utilizzando la ricerca binaria per trovare il punto di inserimento. Per farlo, dobbiamo assicurarci di soddisfare i requisiti.
Faremo una ricerca binaria con il confronto n + j < rand_num[j]
che è lo stesso di
n < rand_num[j] - j
. Dobbiamo dimostrare che rand_num[j] - j
è ancora una sequenza ordinata per una sequenza ordinata rand_num[j]
. Questo è fortunatamente facilmente mostrato, poiché la distanza più bassa tra due elementi dell'originale rand_num
è uno (i numeri generati sono unici, quindi c'è sempre una differenza di almeno 1). Allo stesso tempo, se sottraiamo gli indici j
da tutti gli elementi
rand_num[j]
, le differenze nell'indice sono esattamente 1. Quindi, nel caso "peggiore", otteniamo una sequenza costante, ma mai decrescente. La ricerca binaria può quindi essere utilizzata, producendo O(n log(n))
algoritmo:
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;
}
};
E infine:
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());
}
}
L'ho testato su tre benchmark. Innanzitutto, sono stati scelti 3 numeri su 7 elementi e un istogramma degli elementi scelti è stato accumulato su 10.000 esecuzioni:
4265 4229 4351 4267 4267 4364 4257
Ciò mostra che ciascuno dei 7 elementi è stato scelto approssimativamente lo stesso numero di volte e non vi è alcun pregiudizio apparente causato dall'algoritmo. È stata inoltre verificata la correttezza di tutte le sequenze (unicità dei contenuti).
Il secondo benchmark prevedeva la scelta di 7 numeri su 5000 articoli. Il tempo di diverse versioni dell'algoritmo è stato accumulato su 10.000.000 di esecuzioni. I risultati sono indicati nei commenti nel codice come b1
. La versione semplice dell'algoritmo è leggermente più veloce.
Il terzo benchmark prevedeva la scelta di 700 numeri su 5000 articoli. Il tempo di diverse versioni dell'algoritmo è stato nuovamente accumulato, questa volta oltre 10.000 esecuzioni. I risultati sono indicati nei commenti nel codice come b2
. La versione di ricerca binaria dell'algoritmo è ora più di due volte più veloce di quella semplice.
Il secondo metodo inizia ad essere più veloce per la scelta di più di circa 75 elementi sulla mia macchina (nota che la complessità di entrambi gli algoritmi non dipende dal numero di elementi MAX
).
Vale la pena ricordare che gli algoritmi di cui sopra generano i numeri casuali in ordine crescente. Ma sarebbe semplice aggiungere un altro array in cui i numeri sarebbero salvati nell'ordine in cui sono stati generati e restituirlo invece (a un costo aggiuntivo trascurabile O(n)
). Non è necessario mescolare l'output: sarebbe molto più lento.
Nota che i sorgenti sono in C ++, non ho Java sulla mia macchina, ma il concetto dovrebbe essere chiaro.
MODIFICA :
Per divertimento, ho anche implementato l'approccio che genera una lista con tutti gli indici
0 .. MAX
, li sceglie a caso e li rimuove dalla lista per garantire l'unicità. Dato che ho scelto abbastanza alto MAX
(5000), le prestazioni sono catastrofiche:
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);
}
Ho anche implementato l'approccio con un set
(una raccolta C ++), che in realtà arriva secondo nel benchmark b2
, essendo solo circa il 50% più lento rispetto all'approccio con la ricerca binaria. Ciò è comprensibile, poiché set
utilizza un albero binario, in cui il costo di inserimento è simile alla ricerca binaria. L'unica differenza è la possibilità di ottenere elementi duplicati, il che rallenta l'avanzamento.
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());
Il codice sorgente completo è qui .