Gerar todos os índices de uma sequência geralmente é uma má ideia, pois pode levar muito tempo, especialmente se a proporção dos números a serem escolhidos MAX
for baixa (a complexidade é dominada por O(MAX)
). Isso fica pior se a proporção dos números a serem escolhidos se MAX
aproximar de um, pois então remover os índices escolhidos da sequência de todos também se torna caro (nos aproximamos O(MAX^2/2)
). Mas para números pequenos, isso geralmente funciona bem e não é particularmente sujeito a erros.
Filtrar os índices gerados usando uma coleção também é uma má ideia, pois algum tempo é gasto na inserção dos índices na sequência, e o progresso não é garantido, pois o mesmo número aleatório pode ser desenhado várias vezes (mas para grande o suficiente MAX
é improvável ) Isso pode ser quase complexo
O(k n log^2(n)/2)
, ignorando as duplicatas e assumindo que a coleção usa uma árvore para uma pesquisa eficiente (mas com um custo constante significativo k
de alocação dos nós da árvore e possivelmente tendo que ser rebalanceado ).
Outra opção é gerar os valores aleatórios exclusivamente desde o início, garantindo que o progresso esteja sendo feito. Isso significa que na primeira rodada, um índice aleatório [0, MAX]
é gerado:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
Na segunda rodada, apenas [0, MAX - 1]
é gerado (pois um item já foi selecionado):
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)
Os valores dos índices precisam então ser ajustados: se o segundo índice cair na segunda metade da sequência (após o primeiro índice), ele precisa ser incrementado para compensar a lacuna. Podemos implementar isso como um loop, permitindo-nos selecionar um número arbitrário de itens exclusivos.
Para sequências curtas, este é um O(n^2/2)
algoritmo bastante rápido :
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
}
Onde n_select_num
está o seu 5 e n_number_num
é o seu MAX
. O n_Rand(x)
retorna números inteiros aleatórios em [0, x]
(inclusive). Isso pode ser um pouco mais rápido ao selecionar muitos itens (por exemplo, não 5, mas 500) usando a pesquisa binária para encontrar o ponto de inserção. Para fazer isso, precisamos nos certificar de que atendemos aos requisitos.
Faremos uma pesquisa binária com a comparação n + j < rand_num[j]
que é a mesma que
n < rand_num[j] - j
. Precisamos mostrar que rand_num[j] - j
ainda é uma seqüência classificada para uma seqüência classificada rand_num[j]
. Felizmente, isso é facilmente mostrado, pois a distância mais baixa entre dois elementos do original rand_num
é um (os números gerados são únicos, portanto, sempre há uma diferença de pelo menos 1). Ao mesmo tempo, se subtrairmos os índices j
de todos os elementos
rand_num[j]
, as diferenças no índice serão exatamente 1. Portanto, no "pior" caso, obtemos uma sequência constante - mas nunca decrescente. A pesquisa binária pode, portanto, ser usada, produzindo o O(n log(n))
algoritmo:
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
}
};
E finalmente:
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
}
Eu testei isso em três benchmarks. Primeiro, 3 números foram escolhidos de 7 itens, e um histograma dos itens escolhidos foi acumulado em 10.000 execuções:
4265 4229 4351 4267 4267 4364 4257
Isso mostra que cada um dos 7 itens foi escolhido aproximadamente o mesmo número de vezes, e não há viés aparente causado pelo algoritmo. Todas as sequências também foram verificadas quanto à exatidão (unicidade de conteúdo).
O segundo benchmark envolveu a escolha de 7 números de 5000 itens. O tempo de várias versões do algoritmo foi acumulado em 10.000.000 execuções. Os resultados são indicados nos comentários no código como b1
. A versão simples do algoritmo é um pouco mais rápida.
O terceiro benchmark envolveu a escolha de 700 números entre 5000 itens. O tempo de várias versões do algoritmo foi novamente acumulado, desta vez mais de 10.000 execuções. Os resultados são indicados nos comentários no código como b2
. A versão de pesquisa binária do algoritmo é agora mais de duas vezes mais rápida do que a simples.
O segundo método começa a ser mais rápido para escolher mais do que cerca de 75 itens em minha máquina (observe que a complexidade de qualquer um dos algoritmos não depende do número de itens MAX
).
Vale ressaltar que os algoritmos acima geram os números aleatórios em ordem crescente. Mas seria simples adicionar outra matriz na qual os números seriam salvos na ordem em que foram gerados e retornar isso (a um custo adicional insignificante O(n)
). Não é necessário embaralhar a saída: isso seria muito mais lento.
Observe que os fontes estão em C ++, não tenho Java na minha máquina, mas o conceito deve estar claro.
EDITAR :
Por diversão, também implementei a abordagem que gera uma lista com todos os índices
0 .. MAX
, os escolhe aleatoriamente e os remove da lista para garantir a exclusividade. Como escolhi bastante alto MAX
(5000), o desempenho é catastrófico:
// 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
Também implementei a abordagem com a set
(uma coleção C ++), que na verdade vem em segundo lugar no benchmark b2
, sendo apenas cerca de 50% mais lenta do que a abordagem com a pesquisa binária. Isso é compreensível, pois o set
utiliza uma árvore binária, onde o custo de inserção é semelhante ao da busca binária. A única diferença é a chance de obter itens duplicados, o que retarda o progresso.
// 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
O código-fonte completo está aqui .