203 votes

Sélectionner N éléments aléatoires à partir d'une List<T> en C#

J'ai besoin d'un algorithme rapide pour sélectionner 5 éléments aléatoires à partir d'une liste générique. Par exemple, j'aimerais obtenir 5 éléments aléatoires d'une List.

15 votes

Par « aléatoire », entendez-vous inclusif ou exclusif? En d'autres termes, un même élément peut-il être choisi plus d'une fois? (vraiment aléatoire) Ou une fois qu'un élément est choisi, doit-il être retiré du pool disponible et ne plus être choisi?

0 votes

??? vous venez de mélanger et de prendre les premiers N .. pourquoi y a-t-il tant de discussion ici?

279voto

ersin Points 1243

Utilisation de Linq :

VotreListe.OrderBy(x => rnd.Next()).Take(5)

4 votes

+1 Mais si deux éléments obtiennent le même nombre de rnd.Next() ou similaire, le premier sera sélectionné et le second ne le sera peut-être pas (s'il n'y a pas besoin de plus d'éléments). C'est assez aléatoire en fonction de l'utilisation, cependant.

9 votes

Je pense que l'ordre par est O(n log(n)), donc je choisirais cette solution si la simplicité du code est la principale préoccupation (c'est-à-dire avec de petites listes).

4 votes

Mais est-ce que cela énumère et trie toute la liste? À moins que, par "rapide", l'OP ait voulu dire "facile", et non "performant"...

147voto

Kyle Cronin Points 35834

Parcourir et pour chaque élément, la probabilité de sélection = (nombre nécessaire)/(nombre restant)

Donc, si vous aviez 40 éléments, le premier aurait une chance de 5/40 d'être sélectionné. S'il l'est, le suivant a une chance de 4/39, sinon il a une chance de 5/39. À la fin, vous aurez vos 5 éléments, et souvent vous les aurez tous avant cela.

Cette technique est appelée échantillonnage de sélection, un cas particulier de Échantillonnage du réservoir. Elle est similaire en performance au mélange des données d'entrée, mais bien sûr permet à l'échantillon d'être généré sans modifier les données originales.

0 votes

Pouvez-vous donner un exemple d'implémentation de ceci? Cela semble sympa, mais comment le faire ne me semble pas évident.

54voto

vag Points 71
public static List GetRandomElements(this IEnumerable liste, int elementsCount)
{
    return liste.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();
}

0 votes

Il est là... Merci!

2 votes

Utiliser Guid.NewGuid() ne garantit pas l'aléatoire, juste l'unicité.

30voto

Tyler Points 16516

C'est en fait un problème plus difficile que cela ne semble, principalement parce que de nombreuses solutions mathématiquement correctes échoueront à vous permettre d'atteindre toutes les possibilités (plus d'informations ci-dessous).

Tout d'abord, voici quelques solutions faciles à implémenter, correctes si vous disposez d'un générateur de nombres vraiment aléatoire :

(0) La réponse de Kyle, qui est O(n).

(1) Générez une liste de n paires [(0, rand), (1, rand), (2, rand), ...], triez-les selon la deuxième coordonnée, et utilisez les k premiers (pour vous, k=5) indices pour obtenir votre sous-ensemble aléatoire. Je pense que c'est facile à implémenter, bien que cela prenne du temps en O(n log n).

(2) Initialisez une liste vide s = [] qui contiendra les indices de k éléments aléatoires. Choisissez un nombre r dans {0, 1, 2, ..., n-1} de manière aléatoire, r = rand % n, et ajoutez-le à s. Ensuite, prenez r = rand % (n-1) et insérez-le dans s ; ajoutez à r le nombre d'éléments inférieurs à lui dans s pour éviter les collisions. Puis prenez r = rand % (n-2), et faites la même chose, etc. jusqu'à obtenir k éléments distincts dans s. Cela a un temps d'exécution dans le pire des cas de O(k^2). Donc pour k << n, cela peut être plus rapide. Si vous gardez s trié et suivez les intervalles contigus qu'il contient, vous pouvez l'implémenter en O(k log k), mais cela demande plus de travail.

@Kyle - tu as raison, poussé par l'erreur, je suis d'accord avec ta réponse. Je l'ai rapidement lue au début, et j'ai pensé à tort que tu indiquais de choisir séquentiellement chaque élément avec une probabilité fixe k/n, ce qui aurait été faux - mais ton approche adaptative me semble correcte. Désolé pour ça.

D'accord, et maintenant, pour la surprise : asymptotiquement (pour k fixé, n croissant), il y a n^k/k! choix de sous-ensemble de k éléments parmi n éléments [c'est une approximation de (n choose k)]. Si n est grand et que k n'est pas très petit, alors ces nombres sont énormes. La meilleure longueur de cycle que vous pouvez espérer dans n'importe quel générateur de nombres aléatoires standard sur 32 bits est 2^32 = 256^4. Donc si nous avons une liste de 1000 éléments et que nous voulons en choisir 5 au hasard, il n'y a aucun moyen pour un générateur de nombres aléatoires standard d'atteindre toutes les possibilités. Cependant, tant que vous acceptez un choix qui fonctionne bien pour des ensembles plus petits, et qui semble toujours aléatoire, alors ces algorithmes devraient être bons.

Addendum : Après avoir écrit cela, j'ai réalisé qu'il est difficile de mettre en œuvre l'idée (2) correctement, donc je voulais clarifier cette réponse. Pour obtenir un temps en O(k log k), vous avez besoin d'une structure de type tableau qui prend en charge des recherches et des insertions en O(log m) - un arbre binaire équilibré peut le faire. En utilisant une telle structure pour construire un tableau appelé s, voici un pseudocode en python :

# Retourne un conteneur s avec k nombres aléatoires distincts de {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # Peut être 0, doit être < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # C'est la recherche.
    s.InsertInOrder(q ? r + q : r + len(s))   # Insère juste avant q.
  return s

Je suggère de parcourir quelques cas d'exemple pour voir comment cela implémente efficacement l'explication ci-dessus en anglais.

3 votes

Pour le (1) vous pouvez mélanger une liste plus rapidement que la trier, pour (2) vous allez biaiser votre distribution en utilisant %

0 votes

Étant donné l'objection que vous avez soulevée concernant la longueur du cycle d'un rng, y a-t-il un moyen que nous pouvons construire un algorithme qui choisira tous les ensembles avec une probabilité égale?

0 votes

Pour (1), pour améliorer le O(n log(n)), vous pourriez utiliser le tri par sélection pour trouver les k plus petits éléments. Cela s'exécutera en O(n*k).

16voto

Frank Schwieterman Points 13519

Je pense que la réponse sélectionnée est correcte et assez astucieuse. Cependant, je l'ai implémentée différemment, car je voulais aussi que le résultat soit dans un ordre aléatoire.

    static IEnumerable PickSomeInRandomOrder(
        IEnumerable someTypes,
        int maxCount)
    {
        Random random = new Random(DateTime.Now.Millisecond);

        Dictionary randomSortTable = new Dictionary();

        foreach(SomeType someType in someTypes)
            randomSortTable[random.NextDouble()] = someType;

        return randomSortTable.OrderBy(KVP => KVP.Key).Take(maxCount).Select(KVP => KVP.Value);
    }

0 votes

FORMIDABLE! M'a vraiment aidé!

1 votes

Avez-vous une raison de ne pas utiliser new Random() qui est basé sur Environment.TickCount vs DateTime.Now.Millisecond?

0 votes

Non, je n'étais simplement pas au courant que la valeur par défaut existait.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X