2010-12-10 69 views
4

Ces deux questions donnent algorthims similaires pour brouiller un IEnumerable:Y a-t-il une différence de performance entre ces deux algorithmes pour mélanger un IEnumerable?

Voici les deux méthodes côte à côte:

public static IEnumerable<T> Shuffle1<T> (this IEnumerable<T> source) 
{ 
    Random random = new Random(); 
    T [] copy = source.ToArray(); 

    for (int i = copy.Length - 1; i >= 0; i--) { 
     int index = random.Next (i + 1); 
     yield return copy [index]; 
     copy [index] = copy [i]; 
    } 
} 


public static IEnumerable<T> Shuffle2<T> (this IEnumerable<T> source) 
{ 
    Random random = new Random(); 
    List<T> copy = source.ToList(); 

    while (copy.Count > 0) { 
     int index = random.Next (copy.Count); 
     yield return copy [index]; 
     copy.RemoveAt (index); 
    } 
} 

Ils sont fondamentalement identique, sauf un utilise un List, et on utilise un tableau. Conceptuellement, le second me semble plus clair. Mais y a-t-il un avantage substantiel de performance à gagner en utilisant un tableau? Même si le temps Big-O est le même, s'il est plusieurs fois plus rapide, cela pourrait faire une différence notable.

+5

Vous avez écrit le code dans les deux sens. Si vous voulez savoir lequel est le plus rapide * exécutez le code * et vous le saurez. C'est un mystère pour moi pourquoi tant de gens postent "lequel est plus rapide?" des questions. Il doit sûrement être à la fois plus rapide et plus précis de simplement exécuter le code dans les deux sens en étant assis à votre bureau plutôt que de demander Internet et d'attendre que quelqu'un d'autre le fasse fonctionner pour vous, ou devinez. –

+0

@Matthew toujours poster votre propre indice de référence, puis demandez à ABT sa raison de mieux. – nawfal

Répondre

7

La deuxième version sera probablement un peu plus lente à cause de RemoveAt. Les listes sont en réalité des tableaux qui se développent lorsque vous ajoutez des éléments, et ainsi, l'insertion et la suppression au milieu sont lentes (en fait, MSDN indique que RemoveAt a une complexité O (n)).

Quoi qu'il en soit, le mieux serait simplement d'utiliser un profileur pour comparer les deux méthodes.

+2

+1 répondu: J'ajouterais que 'List .RemoveAt' est O (n) où n est Count - index signifiant qu'il faut plus de temps pour enlever du début que de la fin. Il doit déplacer chaque valeur après l'index. Il n'y a pas besoin de profiler cela, le second ne peut être que plus lent. Le premier est un "Shuffle de Fisher-Yates" et est hautement reconnaissable pour quiconque possède des connaissances. – Tergiver

+2

Exécution à la fois dans Linqpad qui vous donne un compte de minuterie, en utilisant une liste de 300k entiers, le premier a pris en moyenne 0.1 sec, le second a pris 22.5 sec (Windows XP sur 3 GHz Pentium D avec 2 Go de RAM). Je devrais ajouter le timing inclus l'initialisation de ma liste et j'ai utilisé une boucle foreach qui a pris chaque élément de la shuffle et l'a placé dans un HashSet – pstrjds

2

Le premier algorithme est O (n) car il a une boucle qui effectue un échange O (1) à chaque itération. Le deuxième algorithme est O (n^2) car il effectue une opération O (n) RemoveAt à chaque itération. De plus, les indexeurs sur les listes sont plus lents que les index sur les tableaux car le premier est un appel de méthode alors que le second est une instruction IL.

Donc, sur les deux le premier est susceptible d'être plus rapide. Cela dit, si vous êtes après la performance, pourquoi s'embêter à donner les résultats? Il est déjà en train de convertir en un tableau donc il suffit de mélanger et de renvoyer le tableau directement (ou de le mettre dans un ReadOnlyCollection<T> si vous êtes inquiet au sujet des modifications), ce qui est probablement encore plus rapide. Sur une note de côté, les deux méthodes ont des bogues que le comportement de Random lorsqu'il est utilisé par plusieurs threads est indéfini, donc ils devraient probablement utiliser un thread-safe random number generator.

+1

Céder est bon si vous ne voulez pas tous les éléments. Dites que vous voulez les 3 premiers éléments aléatoires d'une collection de 1000. Ce sera beaucoup plus rapide que de mélanger tous les 1000 éléments, puis en choisissant le premier 3. –

+2

@Matthew - vrai, mais si la performance est une préoccupation alors je voudrais faire deux méthodes; un qui est rapide à un shuffle complet (Shuffle) et un optimisé pour prendre un échantillon aléatoire (Sample ou TakeRandom). Je pense aussi que c'est probablement un peu plus auto-documenté car les gens ne s'attendent pas forcément à ce qu'une méthode aléatoire soit paresseuse. –

+0

FYI, c'est très certainement O (N^2). Comme la liste se rétrécit, 'RemoveAt' devra fonctionner sur une moyenne de N/2 éléments par itération, donc l'exécution de' RemoveAt' est toujours directement proportionnelle à N. – mquander

2

Le premier ne compile pas, bien qu'il soit apparent que vous essayez de réifier l'énumérable, puis d'implémenter Fisher-Yates; C'est probablement la bonne approche, et il ne devrait pas être clair pour quiconque a déjà battu un tableau avant. La seconde utilisant RemoveAt est mauvaise pour les raisons indiquées par d'autres commentateurs.

EDIT: Votre version supérieure semble être correcte maintenant, et c'est un bon moyen de le faire.

+1

Oups, correction de l'erreur de compilation. J'ai changé les noms des variables à partir des deux questions auxquelles je me suis connecté pour qu'elles se correspondent aussi étroitement que possible, mais j'en ai raté une. –