Generar todos los índices de una secuencia es generalmente una mala idea, ya que puede llevar mucho tiempo, especialmente si la proporción de los números a elegir MAX
es baja (la complejidad se vuelve dominada por O(MAX)
). Esto empeora si la relación de los números a elegir se MAX
acerca a uno, ya que entonces eliminar los índices elegidos de la secuencia de todos también se vuelve costoso (nos acercamosO(MAX^2/2)
). Pero para números pequeños, esto generalmente funciona bien y no es particularmente propenso a errores.
Filtrar los índices generados mediante el uso de una colección también es una mala idea, ya que se dedica algo de tiempo a insertar los índices en la secuencia, y el progreso no está garantizado ya que se puede extraer el mismo número aleatorio varias veces (pero para lo suficientemente grande MAX
es poco probable ). Esto podría estar cerca de la complejidad
O(k n log^2(n)/2)
, ignorando los duplicados y asumiendo que la colección usa un árbol para una búsqueda eficiente (pero con un costo constante significativo k
de asignar los nodos del árbol y posiblemente tener que reequilibrar ).
Otra opción es generar los valores aleatorios de forma única desde el principio, garantizando que se avanza. Eso significa que en la primera ronda, [0, MAX]
se genera un índice aleatorio en :
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
En la segunda ronda, solo [0, MAX - 1]
se genera (ya que un elemento ya estaba seleccionado):
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)
Luego, los valores de los índices deben ajustarse: si el segundo índice cae en la segunda mitad de la secuencia (después del primer índice), debe incrementarse para tener en cuenta la brecha. Podemos implementar esto como un bucle, lo que nos permite seleccionar un número arbitrario de elementos únicos.
Para secuencias cortas, este es un 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();
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);
}
}
¿Dónde n_select_num
está tu 5 y n_number_num
es tu MAX
. Los n_Rand(x)
rendimientos enteros aleatorios en [0, x]
(ambos inclusive). Esto se puede hacer un poco más rápido si se seleccionan muchos elementos (por ejemplo, no 5 sino 500) mediante la búsqueda binaria para encontrar el punto de inserción. Para hacer eso, debemos asegurarnos de cumplir con los requisitos.
Haremos una búsqueda binaria con la comparación n + j < rand_num[j]
que es igual a
n < rand_num[j] - j
. Necesitamos mostrar que rand_num[j] - j
sigue siendo una secuencia ordenada para una secuencia ordenada rand_num[j]
. Afortunadamente, esto se muestra fácilmente, ya que la distancia más baja entre dos elementos del original rand_num
es uno (los números generados son únicos, por lo que siempre hay una diferencia de al menos 1). Al mismo tiempo, si restamos los índices j
de todos los elementos
rand_num[j]
, las diferencias en el índice son exactamente 1. Entonces, en el "peor" caso, obtenemos una secuencia constante, pero nunca decreciente. Por lo tanto, se puede utilizar la búsqueda binaria, dando como resultado el 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;
}
};
Y 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();
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());
}
}
He probado esto en tres puntos de referencia. Primero, se eligieron 3 números de 7 elementos, y se acumuló un histograma de los elementos elegidos en más de 10,000 ejecuciones:
4265 4229 4351 4267 4267 4364 4257
Esto muestra que cada uno de los 7 elementos se eligió aproximadamente el mismo número de veces, y no existe un sesgo aparente causado por el algoritmo. También se verificó la exactitud de todas las secuencias (unicidad de los contenidos).
El segundo punto de referencia consistió en elegir 7 números de 5000 elementos. El tiempo de varias versiones del algoritmo se acumuló en 10,000,000 ejecuciones. Los resultados se indican en los comentarios del código como b1
. La versión simple del algoritmo es un poco más rápida.
El tercer punto de referencia consistió en elegir 700 números de 5000 elementos. El tiempo de varias versiones del algoritmo se acumuló nuevamente, esta vez más de 10,000 ejecuciones. Los resultados se indican en los comentarios del código como b2
. La versión de búsqueda binaria del algoritmo es ahora más de dos veces más rápida que la simple.
El segundo método comienza a ser más rápido para elegir más de cca 75 elementos en mi máquina (tenga en cuenta que la complejidad de cualquiera de los algoritmos no depende de la cantidad de elementos MAX
).
Vale la pena mencionar que los algoritmos anteriores generan los números aleatorios en orden ascendente. Pero sería sencillo agregar otra matriz en la que se guardarían los números en el orden en que se generaron y devolverla en su lugar (con un costo adicional insignificante O(n)
). No es necesario mezclar la salida: sería mucho más lento.
Tenga en cuenta que las fuentes están en C ++, no tengo Java en mi máquina, pero el concepto debería ser claro.
EDITAR :
Para divertirme, también he implementado el enfoque que genera una lista con todos los índices
0 .. MAX
, los elige al azar y los elimina de la lista para garantizar la singularidad. Como elegí bastante alto MAX
(5000), el rendimiento es catastrófico:
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);
}
También implementé el enfoque con una set
(una colección de C ++), que en realidad ocupa el segundo lugar en el punto de referencia b2
, siendo solo un 50% más lento que el enfoque con la búsqueda binaria. Eso es comprensible, ya que set
utiliza un árbol binario, donde el costo de inserción es similar al de la búsqueda binaria. La única diferencia es la posibilidad de obtener elementos duplicados, lo que ralentiza el progreso.
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());
El código fuente completo está aquí .