No es una forma de barajar lo que me gusta, sobre todo porque es O (n log n) sin ninguna buena razón cuando es fácil implementar una baraja O (n). El código en la pregunta "funciona" básicamente dando un número aleatorio (¡con suerte único!) A cada elemento, luego ordenando los elementos de acuerdo con ese número.
Prefiero la variante de Durstenfield del shuffle de Fisher-Yates que intercambia elementos.
La implementación de un Shuffle
método de extensión simple consistiría básicamente en llamar ToList
o ToArray
en la entrada y luego usar una implementación existente de Fisher-Yates. (Pase el Random
parámetro como para hacer la vida en general más agradable). Hay muchas implementaciones alrededor ... Probablemente tengo una en alguna respuesta.
Lo bueno de este método de extensión es que sería muy claro para el lector lo que realmente está tratando de hacer.
EDITAR: Aquí hay una implementación simple (¡sin verificación de errores!):
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
// Note i > 0 to avoid final pointless iteration
for (int i = elements.Length-1; i > 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
int swapIndex = rng.Next(i + 1);
T tmp = elements[i];
elements[i] = elements[swapIndex];
elements[swapIndex] = tmp;
}
// Lazily yield (avoiding aliasing issues etc)
foreach (T element in elements)
{
yield return element;
}
}
EDITAR: los comentarios sobre el rendimiento a continuación me recordaron que en realidad podemos devolver los elementos a medida que los barajamos:
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
{
T[] elements = source.ToArray();
for (int i = elements.Length - 1; i >= 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
// ... except we don't really need to swap it fully, as we can
// return it immediately, and afterwards it's irrelevant.
int swapIndex = rng.Next(i + 1);
yield return elements[swapIndex];
elements[swapIndex] = elements[i];
}
}
Esto ahora solo hará tanto trabajo como sea necesario.
Tenga en cuenta que en ambos casos, debe tener cuidado con la instancia Random
que usa como:
- La creación de dos instancias
Random
aproximadamente al mismo tiempo producirá la misma secuencia de números aleatorios (cuando se usa de la misma manera)
Random
No es seguro para subprocesos.
Tengo un artículo sobre elRandom
que se detalla más sobre estos temas y proporciona soluciones.
source.ToArray();
que debe tenerusing System.Linq;
el mismo archivo. Si no lo haces, obtienes este error:'System.Collections.Generic.IEnumerable<T>' does not contain a definition for 'ToArray' and no extension method 'ToArray' accepting a first argument of type 'System.Collections.Generic.IEnumerable<T>' could be found (are you missing a using directive or an assembly reference?)
Esto se basa en la respuesta de Jon Skeet .
En esa respuesta, la matriz se baraja, luego se devuelve usando
yield
. El resultado neto es que la matriz se mantiene en memoria durante la duración de foreach, así como los objetos necesarios para la iteración, y sin embargo, el costo es todo al principio: el rendimiento es básicamente un ciclo vacío.Este algoritmo se usa mucho en los juegos, donde se seleccionan los primeros tres elementos, y los demás solo se necesitarán más adelante si es que se necesitan. Mi sugerencia es a
yield
los números tan pronto como se intercambian. Esto reducirá el costo de inicio, mientras mantiene el costo de iteración en O (1) (básicamente 5 operaciones por iteración). El costo total se mantendría igual, pero la mezcla sería más rápida. En los casos en que esto se llama, yacollection.Shuffle().ToArray()
que en teoría no hará ninguna diferencia, pero en los casos de uso mencionados anteriormente, acelerará el arranque. Además, esto haría que el algoritmo sea útil para casos en los que solo necesita unos pocos elementos únicos. Por ejemplo, si necesita extraer tres cartas de un mazo de 52, puede llamardeck.Shuffle().Take(3)
y solo se realizarán tres intercambios (aunque primero se tendría que copiar toda la matriz).fuente
A partir de esta cita de Skeet:
¡Seguiré explicando un poco la razón de lo que es de esperar único!
Ahora, desde el Enumerable.OrderBy :
¡Esto es muy importante! ¿Qué sucede si dos elementos "reciben" el mismo número aleatorio? Sucede que permanecen en el mismo orden en que están en la matriz. Ahora, ¿cuál es la posibilidad de que esto suceda? Es difícil calcular exactamente, pero existe el problema de cumpleaños que es exactamente este problema.
Ahora, ¿es real? ¿Es verdad?
Como siempre, en caso de duda, escriba algunas líneas de programa: http://pastebin.com/5CDnUxPG
Este pequeño bloque de código baraja una serie de 3 elementos una cierta cantidad de veces usando el algoritmo de Fisher-Yates hecho hacia atrás, el algoritmo de Fisher-Yates hecho hacia adelante (en la página wiki hay dos algoritmos de pseudocódigo ... Producen equivalentes resultados, pero uno se realiza del primer al último elemento, mientras que el otro se realiza del último al primer elemento), el ingenuo algoritmo incorrecto de http://blog.codinghorror.com/the-danger-of-naivete/ y el uso de
.OrderBy(x => r.Next())
y el.OrderBy(x => r.Next(someValue))
.Ahora, Random.Next es
entonces es equivalente a
Para probar si este problema existe, podríamos agrandar la matriz (algo muy lento) o simplemente reducir el valor máximo del generador de números aleatorios (
int.MaxValue
no es un número "especial" ... Es simplemente un número muy grande). Al final, si el algoritmo no está sesgado por la estabilidad delOrderBy
, entonces cualquier rango de valores debería dar el mismo resultado.Luego, el programa prueba algunos valores, en el rango de 1 ... 4096. Mirando el resultado, está bastante claro que para valores bajos (<128), el algoritmo es muy sesgado (4-8%). Con 3 valores necesitas al menos
r.Next(1024)
. Si hace que la matriz sea más grande (4 o 5), inclusor.Next(1024)
no es suficiente. No soy un experto en barajar y en matemáticas, pero creo que por cada bit adicional de longitud de la matriz, necesitas 2 bits adicionales de valor máximo (porque la paradoja del cumpleaños está conectada al sqrt (valores numéricos)), entonces que si el valor máximo es 2 ^ 31, diré que debería poder ordenar matrices de hasta 2 ^ 12/2 ^ 13 bits (4096-8192 elementos)fuente
Probablemente está bien para la mayoría de los propósitos, y casi siempre genera una distribución verdaderamente aleatoria (excepto cuando Random.Next () produce dos enteros aleatorios idénticos).
Funciona asignando a cada elemento de la serie un número entero aleatorio, luego ordenando la secuencia por estos números enteros.
Es totalmente aceptable para el 99,9% de las aplicaciones (a menos que sea absolutamente necesario manejar el caso límite anterior). Además, la objeción de skeet a su tiempo de ejecución es válida, por lo que si está barajando una lista larga, es posible que no desee usarla.
fuente
Esto ha surgido muchas veces antes. Busque Fisher-Yates en StackOverflow.
Aquí hay una muestra de código C # que escribí para este algoritmo. Puede parametrizarlo en otro tipo, si lo prefiere.
fuente
Random
como una variable estática como esta:Random
no es seguro para subprocesos. Ver csharpindepth.com/Articles/Chapter12/Random.aspxRandom
es un dolor de usar, como se señaló en mi artículo.Parece un buen algoritmo de barajado, si no te preocupa demasiado el rendimiento. El único problema que señalaría es que su comportamiento no es controlable, por lo que puede ser difícil probarlo.
Una opción posible es que se pase una semilla como parámetro al generador de números aleatorios (o al generador aleatorio como parámetro), para que pueda tener más control y probarlo más fácilmente.
fuente
Encontré que la respuesta de Jon Skeet es completamente satisfactoria, pero el robo-escáner de mi cliente informará que cualquier instancia
Random
es una falla de seguridad. Así que lo cambié porSystem.Security.Cryptography.RNGCryptoServiceProvider
. Como beneficio adicional, corrige el problema de seguridad de subprocesos que se mencionó. Por otro lado,RNGCryptoServiceProvider
se ha medido 300 veces más lento que el usoRandom
.Uso:
Método:
fuente
Buscando un algoritmo? Puedes usar mi
ShuffleList
clase:Luego, úsalo así:
¿Como funciona?
Tomemos un lista ordenada inicial de los 5 primeros números enteros:
{ 0, 1, 2, 3, 4 }
.El método comienza contando el nubmer de elementos y lo llama
count
. Luego, alcount
disminuir en cada paso, toma un número aleatorio entre0
ycount
y lo mueve al final de la lista.En el siguiente ejemplo paso a paso, los elementos que se pueden mover están en cursiva , el elemento seleccionado está en negrita :
0 1 2 3 4
0 1 2 3 4
0 1 2 4 3
0 1 2 4 3
1 2 4 3 0
1 2 4 3 0
1 2 3 0 4
1 2 3 0 4
2 3 0 4 1
2 3 0 4 1
3 0 4 1 2
fuente
Este algoritmo se baraja generando un nuevo valor aleatorio para cada valor en una lista, luego ordenando la lista por esos valores aleatorios. Piense en ello como agregar una nueva columna a una tabla en memoria, luego llenarla con GUID y luego ordenar por esa columna. A mí me parece una forma eficiente (¡especialmente con el azúcar lambda!)
fuente
Ligeramente no relacionado, pero aquí hay un método interesante (que a pesar de que es realmente excesivo, REALMENTE se ha implementado) para una generación verdaderamente aleatoria de tiradas de dados.
Dice-O-Matic
La razón por la que estoy publicando esto aquí, es porque él hace algunos puntos interesantes sobre cómo reaccionaron sus usuarios a la idea de usar algoritmos para barajar, sobre dados reales. Por supuesto, en el mundo real, tal solución es solo para los extremos realmente extremos del espectro donde la aleatoriedad tiene un impacto tan grande y tal vez el impacto afecta el dinero;).
fuente
Diría que muchas respuestas aquí, como "Este algoritmo se baraja al generar un nuevo valor aleatorio para cada valor en una lista, y luego ordenar la lista por esos valores aleatorios" podría estar muy mal.
Creo que esto NO asigna un valor aleatorio a cada elemento de la colección de origen. En cambio, puede haber un algoritmo de ordenación que se ejecute como Quicksort que llamaría a una función de comparación aproximadamente n log n veces. ¡Algún tipo de algoritmo realmente espera que esta función de comparación sea estable y siempre devuelva el mismo resultado!
¡No podría ser que IEnumerableSorter llame a una función de comparación para cada paso del algoritmo, por ejemplo, quicksort y cada vez llame a la función
x => r.Next()
para ambos parámetros sin almacenarlos en caché!En ese caso, realmente podría estropear el algoritmo de clasificación y hacerlo mucho peor que las expectativas en las que se basa el algoritmo. Por supuesto, eventualmente se estabilizará y devolverá algo.
Podría verificarlo más tarde colocando la salida de depuración dentro de una nueva función "Siguiente" para ver qué sucede. En Reflector no pude averiguar de inmediato cómo funciona.
fuente
Tiempo de inicio para ejecutarse en código con borrar todos los hilos y almacenar en caché cada nueva prueba,
Primer código fallido. Se ejecuta en LINQPad. Si sigues para probar este código.
list.OrderBy (x => r.Next ()) usa 38.6528 ms
list.OrderBy (x => Guid.NewGuid ()) usa 36.7634 ms (se recomienda desde MSDN).
después de la segunda vez ambos usan al mismo tiempo.
EDITAR: CÓDIGO DE PRUEBA en Intel Core i7 [email protected], Ram 8 GB DDR3 @ 1600, HDD SATA 5200 rpm con [Datos: www.dropbox.com/s/pbtmh5s9lw285kp/data]
Descripción del resultado: https://www.dropbox.com/s/9dw9wl259dfs04g/ResultDescription.PNG
Estadísticas del resultado: https://www.dropbox.com/s/ewq5ybtsvesme4d/ResultStat.PNG
Conclusión:
Suponga que LINQ OrderBy (r.Next ()) y OrderBy (Guid.NewGuid ()) no son peores que el Método aleatorio definido por el usuario en la primera solución.
Respuesta: son contradicción.
fuente