web-dev-qa-db-fra.com

L'utilisation de Random et OrderBy est-elle un bon algorithme de lecture aléatoire?

J'ai lu n article sur divers algorithmes de lecture aléatoire sur Coding Horror . J'ai vu que quelque part les gens ont fait cela pour mélanger une liste:

var r = new Random();
var shuffled = ordered.OrderBy(x => r.Next());

Est-ce un bon algorithme de lecture aléatoire? Comment ça marche exactement? Est-ce une façon acceptable de procéder?

160
Svish

Ce n'est pas une façon de mélanger que j'aime, principalement au motif que c'est O (n log n) sans raison valable quand il est facile d'implémenter un O(n) shuffle. Le code dans la question "fonctionne" en donnant fondamentalement un nombre aléatoire (espérons-le unique!) à chaque élément, puis en ordonnant les éléments en fonction de ce nombre.

Je préfère la variante de Durstenfield du shuffle de Fisher-Yates qui permute les éléments.

L'implémentation d'une simple méthode d'extension Shuffle consisterait essentiellement à appeler ToList ou ToArray sur l'entrée, puis à utiliser une implémentation existante de Fisher-Yates. (Passez le Random comme paramètre pour rendre la vie généralement plus agréable.) Il y a beaucoup d'implémentations autour ... J'ai probablement une réponse quelque part.

La bonne chose à propos d'une telle méthode d'extension est qu'il serait alors très clair pour le lecteur ce que vous essayez réellement de faire.

EDIT: Voici une implémentation simple (pas de vérification d'erreur!):

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;
    }
}

EDIT: Les commentaires sur les performances ci-dessous m'ont rappelé que nous pouvons réellement retourner les éléments lorsque nous les mélangeons:

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];
    }
}

Cela ne fera désormais que le travail nécessaire.

Notez que dans les deux cas, vous devez faire attention à l'instance de Random que vous utilisez comme:

  • La création de deux instances de Random à peu près en même temps produira la même séquence de nombres aléatoires (lorsqu'elle est utilisée de la même manière)
  • Random n'est pas compatible avec les threads.

J'ai n article sur Random qui va plus en détail sur ces problèmes et fournit des solutions.

200
Jon Skeet

Ceci est basé sur la réponse de Jon Skeet réponse .

Dans cette réponse, le tableau est mélangé, puis renvoyé à l'aide de yield. Le résultat net est que le tableau est conservé en mémoire pendant la durée de foreach, ainsi que les objets nécessaires à l'itération, et pourtant le coût est tout au début - le rendement est essentiellement une boucle vide.

Cet algorithme est beaucoup utilisé dans les jeux, où les trois premiers éléments sont sélectionnés, et les autres ne seront nécessaires que plus tard, voire pas du tout. Ma suggestion est de yield les numéros dès qu'ils sont échangés. Cela réduira le coût de démarrage, tout en maintenant le coût d'itération à O(1) (essentiellement 5 opérations par itération). Le coût total resterait le même, mais le brassage lui-même serait plus rapide. Dans les cas où cela est appelé en tant que collection.Shuffle().ToArray() cela ne fera théoriquement aucune différence, mais dans les cas d'utilisation susmentionnés, il accélérera le démarrage. De plus, cela rendrait l'algorithme utile pour les cas où vous n'avez besoin que par exemple, si vous avez besoin de retirer trois cartes d'un jeu de 52, vous pouvez appeler deck.Shuffle().Take(3) et seulement trois échanges auront lieu (bien que le tableau entier doive être copié en premier ).

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);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
        // we don't actually perform the swap, we can forget about the
        // swapped element because we already returned it.
    }

    // there is one item remaining that was not returned - we return it now
    yield return elements[0]; 
}
70
configurator

À partir de cette citation de Skeet:

Ce n'est pas une manière de mélanger que j'aime, principalement au motif que c'est O (n log n) sans raison valable quand il est facile d'implémenter un O(n) shuffle. Le code dans la question "fonctionne" en donnant fondamentalement un nombre aléatoire ( si tout va bien unique! ) à chaque élément, puis en ordonnant les éléments en fonction de ce nombre.

Je vais continuer à expliquer la raison de la si tout va bien unique!

Maintenant, à partir de Enumerable.OrderBy :

Cette méthode effectue un tri stable; c'est-à-dire que si les clés de deux éléments sont égales, l'ordre des éléments est préservé

C'est très important! Que se passe-t-il si deux éléments "reçoivent" le même nombre aléatoire? Il arrive qu'ils restent dans le même ordre que dans le tableau. Maintenant, quelle est la possibilité que cela se produise? Il est difficile de calculer exactement, mais il y a problème d'anniversaire qui est exactement ce problème.

Maintenant, est-ce réel? Est-ce vrai?

Comme toujours, en cas de doute, écrivez quelques lignes de programme: http://Pastebin.com/5CDnUxPG

Ce petit bloc de code mélange un tableau de 3 éléments un certain nombre de fois en utilisant l'algorithme Fisher-Yates fait en arrière, l'algorithme Fisher-Yates fait en avant (dans la page wiki il y a deux pseudo-code algorithmes ... Ils produisent des résultats équivalents, mais l'un se fait du premier au dernier élément, tandis que l'autre se fait du dernier au premier élément), le mauvais algorithme naïf de http://blog.codinghorror.com/ the-danger-of-naivete / et en utilisant .OrderBy(x => r.Next()) et .OrderBy(x => r.Next(someValue)).

Maintenant, Random.Next est

Entier signé 32 bits supérieur ou égal à 0 et inférieur à MaxValue.

il est donc équivalent à

OrderBy(x => r.Next(int.MaxValue))

Pour tester si ce problème existe, nous pourrions agrandir le tableau (quelque chose de très lent) ou simplement réduire la valeur maximale du générateur de nombres aléatoires (int.MaxValue N'est pas un nombre "spécial" ... C'est simplement un très grand nombre). En fin de compte, si l'algorithme n'est pas biaisé par la stabilité de OrderBy, alors n'importe quelle plage de valeurs devrait donner le même résultat.

Le programme teste ensuite certaines valeurs, comprises entre 1 et 4096. En regardant le résultat, il est assez clair que pour des valeurs faibles (<128), l'algorithme est très biaisé (4-8%). Avec 3 valeurs, vous avez besoin d'au moins r.Next(1024). Si vous agrandissez le tableau (4 ou 5), alors même r.Next(1024) ne suffit pas. Je ne suis pas un expert en mélange et en mathématiques, mais je pense que pour chaque bit supplémentaire de longueur du tableau, vous avez besoin de 2 bits supplémentaires de valeur maximale (parce que le paradoxe d'anniversaire est connecté au sqrt (numvalues)), donc que si la valeur maximale est 2 ^ 31, je dirai que vous devriez pouvoir trier des tableaux jusqu'à 2 ^ 12/2 ^ 13 bits (4096-8192 éléments)

8
xanatos

Il est probablement correct dans la plupart des cas, et il génère presque toujours une distribution vraiment aléatoire (sauf lorsque Random.Next () produit deux entiers aléatoires identiques).

Il fonctionne en attribuant à chaque élément de la série un entier aléatoire, puis en ordonnant la séquence par ces entiers.

Il est totalement acceptable pour 99,9% des applications (sauf si vous devez absolument gérer le cas Edge ci-dessus). De plus, l'objection de skeet à son exécution est valide, donc si vous mélangez une longue liste, vous ne voudrez peut-être pas l'utiliser.

7
ripper234

On dirait un bon algorithme de brassage, si vous n'êtes pas trop inquiet sur les performances. Le seul problème que je soulignerais est que son comportement n'est pas contrôlable, vous pouvez donc avoir du mal à le tester.

Une option possible consiste à transmettre une valeur de départ en tant que paramètre au générateur de nombres aléatoires (ou au générateur aléatoire en tant que paramètre), afin que vous puissiez avoir plus de contrôle et le tester plus facilement.

4
Samuel Carrijo

Cela a été soulevé plusieurs fois auparavant. Recherchez Fisher-Yates sur StackOverflow.

Voici un exemple de code C # J'ai écrit pour cet algorithme. Vous pouvez le paramétrer sur un autre type, si vous préférez.

static public class FisherYates
{
        //      Based on Java code from wikipedia:
        //      http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
        static public void Shuffle(int[] deck)
        {
                Random r = new Random();
                for (int n = deck.Length - 1; n > 0; --n)
                {
                        int k = r.Next(n+1);
                        int temp = deck[n];
                        deck[n] = deck[k];
                        deck[k] = temp;
                }
        }
}
4
hughdbrown

J'ai trouvé la réponse de Jon Skeet entièrement satisfaisante, mais le robot-scanner de mon client signalera toute instance de Random comme une faille de sécurité. Je l'ai donc troqué pour System.Security.Cryptography.RNGCryptoServiceProvider. En prime, il corrige le problème de sécurité des threads mentionné. D'un autre côté, RNGCryptoServiceProvider a été mesuré 300 fois plus lentement que l'utilisation de Random.

Usage:

using (var rng = new RNGCryptoServiceProvider())
{
    var data = new byte[4];
    yourCollection = yourCollection.Shuffle(rng, data);
}

Méthode:

/// <summary>
/// Shuffles the elements of a sequence randomly.
/// </summary>
/// <param name="source">A sequence of values to shuffle.</param>
/// <param name="rng">An instance of a random number generator.</param>
/// <param name="data">A placeholder to generate random bytes into.</param>
/// <returns>A sequence whose elements are shuffled randomly.</returns>
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, RNGCryptoServiceProvider rng, byte[] data)
{
    var elements = source.ToArray();
    for (int i = elements.Length - 1; i >= 0; i--)
    {
        rng.GetBytes(data);
        var swapIndex = BitConverter.ToUInt32(data, 0) % (i + 1);
        yield return elements[swapIndex];
        elements[swapIndex] = elements[i];
    }
}
3
frattaro

Je dirais que de nombreuses réponses comme "Cet algorithme mélange en générant une nouvelle valeur aléatoire pour chaque valeur d'une liste, puis en ordonnant la liste en fonction de ces valeurs aléatoires" pourrait être très erronée!

Je pense que cela N'ASSIGNE PAS une valeur aléatoire à chaque élément de la collection source. Au lieu de cela, il pourrait y avoir un algorithme de tri fonctionnant comme Quicksort qui appellerait une fonction de comparaison environ n log n fois. Une sorte d'algortihm s'attend vraiment à ce que cette fonction de comparaison soit stable et renvoie toujours le même résultat!

Ne pourrait-il pas être que IEnumerableSorter appelle une fonction de comparaison pour chaque étape de l'algorithme, par ex. quicksort et à chaque fois appelle la fonction x => r.Next() pour les deux paramètres sans les mettre en cache!

Dans ce cas, vous pourriez vraiment gâcher l'algorithme de tri et le rendre bien pire que les attentes sur lesquelles l'algorithme est construit. Bien sûr, il finira par devenir stable et retourner quelque chose.

Je pourrais le vérifier plus tard en plaçant la sortie de débogage dans une nouvelle fonction "Next" afin de voir ce qui se passe. Dans Reflector, je n'ai pas pu découvrir immédiatement comment cela fonctionne.

2
Christian

Légèrement sans rapport, mais voici une méthode intéressante (qui même si elle est vraiment excessive, a VRAIMENT été mise en œuvre) pour une génération vraiment aléatoire de lancers de dés!

Dés-O-Matic

La raison pour laquelle je poste ceci ici, c'est qu'il fait des remarques intéressantes sur la façon dont ses utilisateurs ont réagi à l'idée d'utiliser des algorithmes pour mélanger, par rapport aux dés réels. Bien sûr, dans le monde réel, une telle solution ne concerne que les extrémités vraiment extrêmes du spectre où le hasard a un impact si important et peut-être que l'impact affecte l'argent;).

2
Irfy

Vous cherchez un algorithme? Vous pouvez utiliser ma classe ShuffleList:

class ShuffleList<T> : List<T>
{
    public void Shuffle()
    {
        Random random = new Random();
        for (int count = Count; count > 0; count--)
        {
            int i = random.Next(count);
            Add(this[i]);
            RemoveAt(i);
        }
    }
}

Ensuite, utilisez-le comme ceci:

ShuffleList<int> list = new ShuffleList<int>();
// Add elements to your list.
list.Shuffle();

Comment ça marche?

Prenons une liste triée initiale des 5 premiers entiers: { 0, 1, 2, 3, 4 }.

La méthode commence par compter le nombre d'éléments et l'appelle count. Ensuite, avec count décroissant à chaque étape, il faut un nombre aléatoire entre 0 et count et le déplace à la fin de la liste.

Dans l'exemple étape par étape suivant, les éléments pouvant être déplacés sont italiques , l'élément sélectionné est bold:

0 1 2 3 4
0 1 2 4
0 1 2 4 3
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

2
SteeveDroz

Cet algorithme mélange en générant une nouvelle valeur aléatoire pour chaque valeur d'une liste, puis en ordonnant la liste en fonction de ces valeurs aléatoires. Considérez-le comme l'ajout d'une nouvelle colonne à une table en mémoire, puis le remplissage avec des GUID, puis le tri par cette colonne. Cela me semble un moyen efficace (surtout avec le sucre lambda!)

1
Dave Swersky