Seleziona N elementi casuali da un Elenco <T> in C #


158

Ho bisogno di un algoritmo rapido per selezionare 5 elementi casuali da un elenco generico. Ad esempio, vorrei ottenere 5 elementi casuali da a List<string>.


12
Per casuale, intendi inclusivo o esclusivo? IOW, lo stesso elemento può essere raccolto più di una volta? (veramente casuale) O una volta scelto un elemento, non dovrebbe essere più selezionabile dal pool disponibile?
Pretzel,

Risposte:


127

Scorrere e per ogni elemento rendere la probabilità di selezione = (numero necessario) / (numero lasciato)

Quindi, se avessi 40 oggetti, il primo avrebbe una probabilità 5/40 di essere selezionato. Se lo è, il prossimo ha una probabilità di 4/39, altrimenti ha una possibilità di 5/39. Quando arriverai alla fine avrai i tuoi 5 oggetti e spesso li avrai tutti prima.


33
Sento che questo è leggermente sbagliato. Sembra che il back-end dell'elenco venga scelto più spesso del front-end poiché il back-end vedrà probabilità molto più grandi. Ad esempio, se i primi 35 numeri non vengono scelti, gli ultimi 5 numeri devono essere scelti. Il primo numero vedrà solo una possibilità 5/40, ma quell'ultimo numero vedrà 1/1 più spesso di 5/40 volte. Dovrai randomizzare l'elenco prima di implementare questo algoritmo.
Ankur Goel,

23
ok, ho eseguito questo algoritmo 10 milioni di volte su un elenco di 40 elementi, ognuno con uno scatto 5/40 (.125) alla selezione, e poi ho eseguito quella simulazione più volte. Si scopre che questo non è distribuito uniformemente. Gli elementi da 16 a 22 vengono sottoselezionati (16 = .123, 17 = .124), mentre l'elemento 34 viene selezionato in eccesso (34 = .129). Anche gli elementi 39 e 40 sono sottoselezionati ma non di così tanto (39 = .1247, 40 = .1246)
Ankur Goel

22
@Ankur: non credo sia statisticamente significativo. Credo che ci sia una prova induttiva che ciò fornirà una distribuzione uniforme.
ricorsivo il

9
Ho ripetuto la stessa prova 100 milioni di volte e nella mia prova l'articolo meno scelto è stato scelto meno dello 0,106% in meno dell'elemento scelto più frequentemente.
ricorsivo il

5
@recursive: la prova è quasi banale. Sappiamo come selezionare K voci da K per qualsiasi K e come selezionare 0 voci da N per qualsiasi N. Supponiamo di conoscere un metodo per selezionare uniformemente K o K-1 voci da N-1> = K; quindi possiamo selezionare K elementi da N selezionando il primo oggetto con probabilità K / N e quindi utilizzando il metodo noto per selezionare gli elementi K o K-1 ancora necessari tra i restanti N-1.
Ilmari Karonen,

216

Utilizzando linq:

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

2
+1 Ma se due elementi ottengono lo stesso numero da rnd.Next () o simile, il primo verrà selezionato e il secondo no (se non sono necessari altri elementi). Tuttavia, è abbastanza casuale a seconda dell'uso.
Lasse Espeholt,

8
Penso che l'ordine sia O (n log (n)), quindi sceglierei questa soluzione se la semplicità del codice è la preoccupazione principale (cioè con piccoli elenchi).
Guido,

2
Ma questo non enumera e ordina l'intero elenco? A meno che, per "veloce", OP significasse "facile", non "performante" ...
drzaus,

2
Funzionerà solo se OrderBy () chiama il selettore chiave solo una volta per ogni elemento. Se lo chiama ogni volta che vuole eseguire un confronto tra due elementi, otterrà un valore diverso ogni volta, il che rovinerà l'ordinamento. La [documentazione] ( msdn.microsoft.com/en-us/library/vstudio/… ) non dice quale sia.
Oliver Bock

2
Fai attenzione se YourListha molti articoli ma vuoi selezionarne solo alcuni. In questo caso non è un modo efficace di farlo.
Callum Watkins,

39
public static List<T> GetRandomElements<T>(this IEnumerable<T> list, int elementsCount)
{
    return list.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();
}

27

Questo è in realtà un problema più difficile di quanto sembri, principalmente perché molte soluzioni matematicamente corrette non ti permetteranno effettivamente di cogliere tutte le possibilità (più su questo sotto).

Innanzitutto, ecco alcuni generatori di numeri casuali facili da implementare, corretti se hai un vero numero casuale:

(0) La risposta di Kyle, che è O (n).

(1) Genera un elenco di n coppie [(0, rand), (1, rand), (2, rand), ...], ordinale in base alla seconda coordinata e usa il primo k (per te, k = 5) indici per ottenere il tuo sottoinsieme casuale. Penso che sia facile da implementare, sebbene sia O (n log n) tempo.

(2) Init una lista vuota s = [] che diventerà gli indici di k elementi casuali. Scegli un numero r in {0, 1, 2, ..., n-1} a caso, r = rand% n, e aggiungi questo a s. Quindi prendi r = rand% (n-1) e mantieni in s; aggiungere a r gli elementi # meno di in s per evitare collisioni. Quindi prendi r = rand% (n-2) e fai la stessa cosa, ecc. Finché non hai k elementi distinti in s. Questo ha il tempo di esecuzione peggiore O (k ^ 2). Quindi per k << n, questo può essere più veloce. Se mantieni l'ordinamento e traccia gli intervalli contigui che ha, puoi implementarlo in O (k log k), ma è più lavoro.

@Kyle - hai ragione, ripensandoci sono d'accordo con la tua risposta. All'inizio l'ho letto in fretta e ho erroneamente pensato che stavi indicando di scegliere in sequenza ogni elemento con probabilità fissa k / n, il che sarebbe stato sbagliato - ma il tuo approccio adattivo mi sembra corretto. Mi dispiace per quello.

Ok, e ora per il kicker: asintoticamente (per k fisso, crescente), ci sono n ^ k / k! scelte del sottoinsieme di k elementi tra n elementi [questa è un'approssimazione di (n scegli k)]. Se n è grande e k non è molto piccolo, questi numeri sono enormi. La durata del ciclo migliore che si può sperare in qualsiasi generatore di numeri casuali standard a 32 bit è 2 ^ 32 = 256 ^ 4. Quindi, se abbiamo un elenco di 1000 elementi e vogliamo scegliere 5 a caso, non è possibile che un generatore di numeri casuali standard colpirà tutte le possibilità. Tuttavia, fintanto che stai bene con una scelta che funziona bene per set più piccoli e "sembra" sempre casuale, allora questi algoritmi dovrebbero essere ok.

Addendum : dopo aver scritto questo, mi sono reso conto che è difficile implementare correttamente l'idea (2), quindi volevo chiarire questa risposta. Per ottenere il tempo O (k log k), è necessaria una struttura simile a un array che supporti le ricerche e gli inserimenti O (log m): un albero binario bilanciato può farlo. Utilizzando una struttura del genere per creare un array chiamato s, ecco alcuni pseudopython:

# Returns a container s with k distinct random numbers from {0, 1, ..., n-1}
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
    s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
  return s

Suggerisco di esaminare alcuni casi esemplificativi per vedere come questo implementa efficacemente la spiegazione inglese sopra.


2
per (1) puoi mescolare un elenco più velocemente di quanto lo sia l'ordinamento, per (2) dovrai distorcere la tua distribuzione usando%
jk.

Dato l'obiezione lei ha sollevato circa la lunghezza del ciclo di un RNG, c'è un modo siamo in grado di costruire un algoritmo che sceglierà tutti i set con uguale probabilità?
Giona,

Per (1), per migliorare O (n log (n)) è possibile utilizzare l'ordinamento di selezione per trovare i k elementi più piccoli. Che verrà eseguito in O (n * k).
Jared,

@Jonah: penso di si. Supponiamo di poter combinare più generatori di numeri casuali indipendenti per crearne uno più grande ( crypto.stackexchange.com/a/27431 ). Quindi hai solo bisogno di un intervallo sufficientemente ampio per gestire la dimensione dell'elenco in questione.
Jared,

16

Penso che la risposta selezionata sia corretta e piuttosto dolce. L'ho implementato in modo diverso, poiché volevo anche il risultato in ordine casuale.

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

        Dictionary<double, SomeType> randomSortTable = new Dictionary<double,SomeType>();

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

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

ECCEZIONALE! Mi ha davvero aiutato!
Armstrongest,

1
Hai qualche motivo per non usare il nuovo Random () basato su Environment.TickCount vs. DateTime.Now.Millisecond?
Lasse Espeholt,

No, non ero a conoscenza dell'esistenza del valore predefinito.
Frank Schwieterman,

2
Va bene con un anno di ritardo ma ... Questo non va alla risposta piuttosto breve di @ ersin, e non fallirà se ottieni un numero casuale ripetuto (dove Ersin avrà una propensione verso il primo elemento di una coppia ripetuta)
Andii

1
Random random = new Random(DateTime.Now.Millisecond);su ogni chiamata è sicuramente sbagliato. La creazione di una nuova istanza di Randomogni volta riduce la casualità effettiva. Utilizzare static readonlyun'istanza di esso, preferibilmente costruita con il costruttore predefinito.
jpmc26,

12

Mi sono appena imbattuto in questo problema e qualche altra ricerca su Google mi ha portato al problema di mescolare casualmente un elenco: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle

Per mescolare completamente casualmente la tua lista (sul posto) fai questo:

Per mescolare una matrice a di n elementi (indici 0..n-1):

  for i from n  1 downto 1 do
       j  random integer with 0  j  i
       exchange a[j] and a[i]

Se hai solo bisogno dei primi 5 elementi, quindi invece di far partire i da n-1 a 1, devi solo eseguirlo su n-5 (cioè: n-5)

Diciamo che hai bisogno di k articoli,

Questo diventa:

  for (i = n  1; i >= n-k; i--)
  {
       j = random integer with 0  j  i
       exchange a[j] and a[i]
  }

Ogni elemento selezionato viene scambiato verso la fine dell'array, quindi gli elementi k selezionati sono gli ultimi k elementi dell'array.

Questo richiede tempo O (k), dove k è il numero di elementi selezionati casualmente che ti servono.

Inoltre, se non si desidera modificare l'elenco iniziale, è possibile annotare tutti gli swap in un elenco temporaneo, invertire tale elenco e applicarli nuovamente, eseguendo in tal modo il gruppo inverso di swap e restituendo l'elenco iniziale senza modificare il tempo di esecuzione O (k).

Infine, per il vero stickler, se (n == k), dovresti fermarti a 1, non nk, poiché l'intero scelto casualmente sarà sempre 0.


L'ho implementato usando C # nel mio post sul blog: vijayt.com/post/random-select-using-fisher-yates-algorithm . Spero che aiuti qualcuno a cercare il modo C #.
Vijayst,


8

Da Dragons in the Algorithm , un'interpretazione in C #:

int k = 10; // items to select
var items = new List<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 });
var selected = new List<int>();
double needed = k;
double available = items.Count;
var rand = new Random();
while (selected.Count < k) {
   if( rand.NextDouble() < needed / available ) {
      selected.Add(items[(int)available-1])
      needed--;
   }
   available--;
}

Questo algoritmo selezionerà le indicazioni univoche dell'elenco degli articoli.


Ottieni solo abbastanza elementi nell'elenco, ma non ottieni casualmente.
culithay

2
Questa implementazione è rotto perché l'utilizzo di varrisultati in neededed availableessendo entrambi numeri interi, il che rende needed/availablesempre 0.
Niko

1
Questa sembra essere un'implementazione della risposta accettata.
DCShannon,

6

Selezionando N articoli casuali da un gruppo non dovrebbe avere nulla a che fare con l' ordine ! La casualità riguarda l'imprevedibilità e non lo spostamento di posizioni in un gruppo. Tutte le risposte che trattano in qualche modo un ordinamento sono destinate ad essere meno efficienti di quelle che non lo fanno. Poiché l'efficienza è la chiave qui, posterò qualcosa che non cambia troppo l'ordine degli articoli.

1) se necessario veri valori casuali, il che significa che non ci sono restrizioni su quali elementi scegliere (cioè, una volta che l'elemento scelto può essere riselezionato):

public static List<T> GetTrueRandom<T>(this IList<T> source, int count, 
                                       bool throwArgumentOutOfRangeException = true)
{
    if (throwArgumentOutOfRangeException && count > source.Count)
        throw new ArgumentOutOfRangeException();

    var randoms = new List<T>(count);
    randoms.AddRandomly(source, count);
    return randoms;
}

Se disattivi il flag di eccezione, puoi scegliere elementi casuali un numero qualsiasi di volte.

Se hai {1, 2, 3, 4}, può dare {1, 4, 4}, {1, 4, 3} ecc per 3 elementi o anche {1, 4, 3, 2, 4} per 5 articoli!

Questo dovrebbe essere piuttosto veloce, in quanto non ha nulla da controllare.

2) Se hai bisogno di singoli membri del gruppo senza ripetizioni, farei affidamento su un dizionario (come molti hanno già sottolineato).

public static List<T> GetDistinctRandom<T>(this IList<T> source, int count)
{
    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    if (count == source.Count)
        return new List<T>(source);

    var sourceDict = source.ToIndexedDictionary();

    if (count > source.Count / 2)
    {
        while (sourceDict.Count > count)
            sourceDict.Remove(source.GetRandomIndex());

        return sourceDict.Select(kvp => kvp.Value).ToList();
    }

    var randomDict = new Dictionary<int, T>(count);
    while (randomDict.Count < count)
    {
        int key = source.GetRandomIndex();
        if (!randomDict.ContainsKey(key))
            randomDict.Add(key, sourceDict[key]);
    }

    return randomDict.Select(kvp => kvp.Value).ToList();
}

Il codice è un po 'più lungo di altri approcci del dizionario qui perché non sto solo aggiungendo, ma anche rimuovendo dalla lista, quindi sono un po' due loop. Puoi vedere qui che non ho riordinato nulla quando countdiventa uguale a source.Count. Questo perché credo che la casualità dovrebbe essere nel set restituito nel suo insieme . Voglio dire, se volete 5 oggetti casuali da 1, 2, 3, 4, 5, non dovrebbe importa se il suo 1, 3, 4, 2, 5o 1, 2, 3, 4, 5, ma se avete bisogno di 4 articoli della stessa serie, allora dovrebbe cedere in maniera imprevedibile 1, 2, 3, 4, 1, 3, 5, 2, 2, 3, 5, 4ecc In secondo luogo, quando il conteggio di oggetti casuali per essere restituito è più della metà del gruppo originale, quindi è più facile da rimuoveresource.Count - countelementi dal gruppo che aggiungere invece dicount elementi. Per motivi di prestazioni ho usatosourcesourceDict per ottenere quindi un indice casuale nel metodo di rimozione.

Quindi se hai {1, 2, 3, 4}, questo può finire in {1, 2, 3}, {3, 4, 1} ecc per 3 elementi.

3) Se hai bisogno di valori casuali veramente distinti dal tuo gruppo prendendo in considerazione i duplicati nel gruppo originale, allora puoi usare lo stesso approccio di cui sopra, ma HashSetsarà più leggero di un dizionario.

public static List<T> GetTrueDistinctRandom<T>(this IList<T> source, int count, 
                                               bool throwArgumentOutOfRangeException = true)
{
    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    var set = new HashSet<T>(source);

    if (throwArgumentOutOfRangeException && count > set.Count)
        throw new ArgumentOutOfRangeException();

    List<T> list = hash.ToList();

    if (count >= set.Count)
        return list;

    if (count > set.Count / 2)
    {
        while (set.Count > count)
            set.Remove(list.GetRandom());

        return set.ToList();
    }

    var randoms = new HashSet<T>();
    randoms.AddRandomly(list, count);
    return randoms.ToList();
}

La randomsvariabile viene creata HashSetper evitare l'aggiunta di duplicati nei casi più rari e più rari in cui Random.Nextpuò produrre lo stesso valore, soprattutto quando l'elenco di input è piccolo.

Quindi {1, 2, 2, 4} => 3 elementi casuali => {1, 2, 4} e mai {1, 2, 2}

{1, 2, 2, 4} => 4 elementi casuali => eccezione !! o {1, 2, 4} a seconda del flag impostato.

Alcuni dei metodi di estensione che ho usato:

static Random rnd = new Random();
public static int GetRandomIndex<T>(this ICollection<T> source)
{
    return rnd.Next(source.Count);
}

public static T GetRandom<T>(this IList<T> source)
{
    return source[source.GetRandomIndex()];
}

static void AddRandomly<T>(this ICollection<T> toCol, IList<T> fromList, int count)
{
    while (toCol.Count < count)
        toCol.Add(fromList.GetRandom());
}

public static Dictionary<int, T> ToIndexedDictionary<T>(this IEnumerable<T> lst)
{
    return lst.ToIndexedDictionary(t => t);
}

public static Dictionary<int, T> ToIndexedDictionary<S, T>(this IEnumerable<S> lst, 
                                                           Func<S, T> valueSelector)
{
    int index = -1;
    return lst.ToDictionary(t => ++index, valueSelector);
}

Se si tratta di prestazioni con decine di migliaia di elementi nell'elenco che devono essere ripetuti 10000 volte, allora potresti voler avere una classe casuale più veloce di System.Random, ma non penso che sia un grosso problema considerando che quest'ultimo molto probabilmente non è mai un collo di bottiglia, è abbastanza veloce ..

Modifica: se è necessario riorganizzare anche l'ordine degli articoli restituiti, non c'è nulla che possa battere l' approccio Fisher-Yates di dhakim : breve, dolce e semplice ..


6

Stavo pensando al commento di @JohnShedletsky sulla risposta accettata per quanto riguarda (parafrasi):

dovresti essere in grado di farlo in O (subset.Length), piuttosto che O (originalList.Length)

Fondamentalmente, dovresti essere in grado di generare subsetindici casuali e poi strapparli dall'elenco originale.

Il metodo

public static class EnumerableExtensions {

    public static Random randomizer = new Random(); // you'd ideally be able to replace this with whatever makes you comfortable

    public static IEnumerable<T> GetRandom<T>(this IEnumerable<T> list, int numItems) {
        return (list as T[] ?? list.ToArray()).GetRandom(numItems);

        // because ReSharper whined about duplicate enumeration...
        /*
        items.Add(list.ElementAt(randomizer.Next(list.Count()))) ) numItems--;
        */
    }

    // just because the parentheses were getting confusing
    public static IEnumerable<T> GetRandom<T>(this T[] list, int numItems) {
        var items = new HashSet<T>(); // don't want to add the same item twice; otherwise use a list
        while (numItems > 0 )
            // if we successfully added it, move on
            if( items.Add(list[randomizer.Next(list.Length)]) ) numItems--;

        return items;
    }

    // and because it's really fun; note -- you may get repetition
    public static IEnumerable<T> PluckRandomly<T>(this IEnumerable<T> list) {
        while( true )
            yield return list.ElementAt(randomizer.Next(list.Count()));
    }

}

Se volessi essere ancora più efficiente, probabilmente useresti uno HashSetdegli indici , non gli elementi dell'elenco reale (nel caso in cui tu abbia tipi complessi o confronti costosi);

Il test unitario

E per assicurarsi che non ci siano collisioni, ecc.

[TestClass]
public class RandomizingTests : UnitTestBase {
    [TestMethod]
    public void GetRandomFromList() {
        this.testGetRandomFromList((list, num) => list.GetRandom(num));
    }

    [TestMethod]
    public void PluckRandomly() {
        this.testGetRandomFromList((list, num) => list.PluckRandomly().Take(num), requireDistinct:false);
    }

    private void testGetRandomFromList(Func<IEnumerable<int>, int, IEnumerable<int>> methodToGetRandomItems, int numToTake = 10, int repetitions = 100000, bool requireDistinct = true) {
        var items = Enumerable.Range(0, 100);
        IEnumerable<int> randomItems = null;

        while( repetitions-- > 0 ) {
            randomItems = methodToGetRandomItems(items, numToTake);
            Assert.AreEqual(numToTake, randomItems.Count(),
                            "Did not get expected number of items {0}; failed at {1} repetition--", numToTake, repetitions);
            if(requireDistinct) Assert.AreEqual(numToTake, randomItems.Distinct().Count(),
                            "Collisions (non-unique values) found, failed at {0} repetition--", repetitions);
            Assert.IsTrue(randomItems.All(o => items.Contains(o)),
                        "Some unknown values found; failed at {0} repetition--", repetitions);
        }
    }
}

2
Bella idea, con problemi. (1) Se la tua lista più grande è enorme (leggi da un database, per esempio) allora realizzi l'intera lista, che può superare la memoria. (2) Se K è vicino a N, allora sbaglierai molto alla ricerca di un indice non reclamato nel tuo loop, facendo sì che il codice richieda un periodo di tempo imprevedibile. Questi problemi sono risolvibili.
Paul Chernoch,

1
La mia soluzione al problema del thrashing è questa: se K <N / 2, fallo a modo tuo. Se K> = N / 2, scegli gli indici che NON dovrebbero essere mantenuti, invece di quelli che dovrebbero essere mantenuti. C'è ancora del thrashing, ma molto meno.
Paul Chernoch,

Si è inoltre notato che ciò altera l'ordine degli elementi enumerati, il che può essere accettabile in alcune situazioni, ma non in altre.
Paul Chernoch,

In media, per K = N / 2 (il caso peggiore per il miglioramento suggerito da Paul), l'algoritmo (migliorato dal thrashing) sembra prendere ~ 0,693 * N iterazioni. Ora fai un confronto di velocità. È meglio della risposta accettata? Per quali dimensioni del campione?
mbomb007,

6

Ho combinato alcune delle risposte di cui sopra per creare un metodo di estensione valutato pigramente. I miei test hanno dimostrato che l'approccio di Kyle (Ordine (N)) è molte volte più lento dell'uso di un set da parte di Drzaus per proporre gli indici casuali da scegliere (Ordine (K)). Il primo esegue molte più chiamate al generatore di numeri casuali, oltre a ripetere più volte gli elementi.

Gli obiettivi della mia implementazione erano:

1) Non realizzare l'elenco completo se viene fornito un IEnumerable che non è un IList. Se mi viene data una sequenza di un milione di elementi, non voglio esaurire la memoria. Usa l'approccio di Kyle per una soluzione online.

2) Se posso dire che è un IList, usa l'approccio di drzaus, con una svolta. Se K è più della metà di N, rischio di battere mentre scelgo più e più volte indici casuali e devo saltarli. Quindi compongo un elenco degli indici da NON conservare.

3) Garantisco che gli articoli saranno restituiti nello stesso ordine in cui sono stati rilevati. L'algoritmo di Kyle non ha richiesto alterazioni. L'algoritmo di drzaus richiedeva che non emettessi elementi nell'ordine in cui sono stati scelti gli indici casuali. Raccolgo tutti gli indici in un SortedSet, quindi emetto gli elementi in ordine di indice ordinato.

4) Se K è grande rispetto a N e invertisco il senso dell'insieme, allora elenco tutti gli elementi e verifico se l'indice non è nell'insieme. Ciò significa che perdo il tempo di esecuzione dell'Ordine (K), ma poiché K è vicino a N in questi casi, non perdo molto.

Ecco il codice:

    /// <summary>
    /// Takes k elements from the next n elements at random, preserving their order.
    /// 
    /// If there are fewer than n elements in items, this may return fewer than k elements.
    /// </summary>
    /// <typeparam name="TElem">Type of element in the items collection.</typeparam>
    /// <param name="items">Items to be randomly selected.</param>
    /// <param name="k">Number of items to pick.</param>
    /// <param name="n">Total number of items to choose from.
    /// If the items collection contains more than this number, the extra members will be skipped.
    /// If the items collection contains fewer than this number, it is possible that fewer than k items will be returned.</param>
    /// <returns>Enumerable over the retained items.
    /// 
    /// See http://stackoverflow.com/questions/48087/select-a-random-n-elements-from-listt-in-c-sharp for the commentary.
    /// </returns>
    public static IEnumerable<TElem> TakeRandom<TElem>(this IEnumerable<TElem> items, int k, int n)
    {
        var r = new FastRandom();
        var itemsList = items as IList<TElem>;

        if (k >= n || (itemsList != null && k >= itemsList.Count))
            foreach (var item in items) yield return item;
        else
        {  
            // If we have a list, we can infer more information and choose a better algorithm.
            // When using an IList, this is about 7 times faster (on one benchmark)!
            if (itemsList != null && k < n/2)
            {
                // Since we have a List, we can use an algorithm suitable for Lists.
                // If there are fewer than n elements, reduce n.
                n = Math.Min(n, itemsList.Count);

                // This algorithm picks K index-values randomly and directly chooses those items to be selected.
                // If k is more than half of n, then we will spend a fair amount of time thrashing, picking
                // indices that we have already picked and having to try again.   
                var invertSet = k >= n/2;  
                var positions = invertSet ? (ISet<int>) new HashSet<int>() : (ISet<int>) new SortedSet<int>();

                var numbersNeeded = invertSet ? n - k : k;
                while (numbersNeeded > 0)
                    if (positions.Add(r.Next(0, n))) numbersNeeded--;

                if (invertSet)
                {
                    // positions contains all the indices of elements to Skip.
                    for (var itemIndex = 0; itemIndex < n; itemIndex++)
                    {
                        if (!positions.Contains(itemIndex))
                            yield return itemsList[itemIndex];
                    }
                }
                else
                {
                    // positions contains all the indices of elements to Take.
                    foreach (var itemIndex in positions)
                        yield return itemsList[itemIndex];              
                }
            }
            else
            {
                // Since we do not have a list, we will use an online algorithm.
                // This permits is to skip the rest as soon as we have enough items.
                var found = 0;
                var scanned = 0;
                foreach (var item in items)
                {
                    var rand = r.Next(0,n-scanned);
                    if (rand < k - found)
                    {
                        yield return item;
                        found++;
                    }
                    scanned++;
                    if (found >= k || scanned >= n)
                        break;
                }
            }
        }  
    } 

Uso un generatore di numeri casuali specializzato, ma puoi solo usare Casuale di C # se vuoi. ( FastRandom stato scritto da Colin Green e fa parte di SharpNEAT. Ha un periodo di 2 ^ 128-1 che è migliore di molti RNG.)

Ecco i test unitari:

[TestClass]
public class TakeRandomTests
{
    /// <summary>
    /// Ensure that when randomly choosing items from an array, all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_Array_Uniformity()
    {
        const int numTrials = 2000000;
        const int expectedCount = numTrials/20;
        var timesChosen = new int[100];
        var century = new int[100];
        for (var i = 0; i < century.Length; i++)
            century[i] = i;

        for (var trial = 0; trial < numTrials; trial++)
        {
            foreach (var i in century.TakeRandom(5, 100))
                timesChosen[i]++;
        }
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount/100;
        AssertBetween(avg, expectedCount - 2, expectedCount + 2, "Average");
        //AssertBetween(min, expectedCount - allowedDifference, expectedCount, "Min");
        //AssertBetween(max, expectedCount, expectedCount + allowedDifference, "Max");

        var countInRange = timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: {0}", countInRange));
    }

    /// <summary>
    /// Ensure that when randomly choosing items from an IEnumerable that is not an IList, 
    /// all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_IEnumerable_Uniformity()
    {
        const int numTrials = 2000000;
        const int expectedCount = numTrials / 20;
        var timesChosen = new int[100];

        for (var trial = 0; trial < numTrials; trial++)
        {
            foreach (var i in Range(0,100).TakeRandom(5, 100))
                timesChosen[i]++;
        }
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount / 100;
        var countInRange =
            timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: {0}", countInRange));
    }

    private IEnumerable<int> Range(int low, int count)
    {
        for (var i = low; i < low + count; i++)
            yield return i;
    }

    private static void AssertBetween(int x, int low, int high, String message)
    {
        Assert.IsTrue(x > low, String.Format("Value {0} is less than lower limit of {1}. {2}", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value {0} is more than upper limit of {1}. {2}", x, high, message));
    }

    private static void AssertBetween(double x, double low, double high, String message)
    {
        Assert.IsTrue(x > low, String.Format("Value {0} is less than lower limit of {1}. {2}", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value {0} is more than upper limit of {1}. {2}", x, high, message));
    }
}

Non c'è un errore nel test? Hai if (itemsList != null && k < n/2)quale mezzo dentro if invertSetè sempre il falseche significa che la logica non viene mai usata.
NetMage,

4

Estendendo la risposta di @ ers, se uno è preoccupato per possibili diverse implementazioni di OrderBy, questo dovrebbe essere sicuro:

// Instead of this
YourList.OrderBy(x => rnd.Next()).Take(5)

// Temporarily transform 
YourList
    .Select(v => new {v, i = rnd.Next()}) // Associate a random index to each entry
    .OrderBy(x => x.i).Take(5) // Sort by (at this point fixed) random index 
    .Select(x => x.v); // Go back to enumerable of entry

3

Questo è il migliore che ho potuto inventare per un primo taglio:

public List<String> getRandomItemsFromList(int returnCount, List<String> list)
{
    List<String> returnList = new List<String>();
    Dictionary<int, int> randoms = new Dictionary<int, int>();

    while (randoms.Count != returnCount)
    {
        //generate new random between one and total list count
        int randomInt = new Random().Next(list.Count);

        // store this in dictionary to ensure uniqueness
        try
        {
            randoms.Add(randomInt, randomInt);
        }
        catch (ArgumentException aex)
        {
            Console.Write(aex.Message);
        } //we can assume this element exists in the dictonary already 

        //check for randoms length and then iterate through the original list 
        //adding items we select via random to the return list
        if (randoms.Count == returnCount)
        {
            foreach (int key in randoms.Keys)
                returnList.Add(list[randoms[key]]);

            break; //break out of _while_ loop
        }
    }

    return returnList;
}

Usare un elenco di randoms in un intervallo di 1 - il conteggio totale degli elenchi e quindi semplicemente estrarre quegli elementi nell'elenco sembrava essere il modo migliore, ma usare il Dizionario per garantire l'unicità è qualcosa su cui sto ancora riflettendo.

Nota anche che ho usato un elenco di stringhe, sostituirlo se necessario.


1
Ha lavorato al primo colpo!
sangam,

3

La semplice soluzione che uso (probabilmente non va bene per elenchi di grandi dimensioni): copia l'elenco in un elenco temporaneo, quindi in modo casuale seleziona l'elemento dall'elenco degli elementi temporanei e inseriscilo nell'elenco degli elementi selezionati rimuovendolo dall'elenco degli elementi temporanei (quindi non può essere riselezionata).

Esempio:

List<Object> temp = OriginalList.ToList();
List<Object> selectedItems = new List<Object>();
Random rnd = new Random();
Object o;
int i = 0;
while (i < NumberOfSelectedItems)
{
            o = temp[rnd.Next(temp.Count)];
            selectedItems.Add(o);
            temp.Remove(o);
            i++;
 }

La rimozione dal centro di un elenco così spesso sarà costosa. Puoi prendere in considerazione l'utilizzo di un elenco collegato per un algoritmo che richiede così tante rimozioni. O in modo equivalente, sostituisci l'elemento rimosso con un valore nullo, ma poi ti schiaccerai un po 'mentre raccogli gli elementi già rimossi e devi scegliere di nuovo.
Paul Chernoch,

3

Qui hai un'implementazione basata su Fisher-Yates Shuffle la cui complessità dell'algoritmo è O (n) dove n è la dimensione del sottoinsieme o del campione, anziché la dimensione dell'elenco, come ha sottolineato John Shedletsky.

public static IEnumerable<T> GetRandomSample<T>(this IList<T> list, int sampleSize)
{
    if (list == null) throw new ArgumentNullException("list");
    if (sampleSize > list.Count) throw new ArgumentException("sampleSize may not be greater than list count", "sampleSize");
    var indices = new Dictionary<int, int>(); int index;
    var rnd = new Random();

    for (int i = 0; i < sampleSize; i++)
    {
        int j = rnd.Next(i, list.Count);
        if (!indices.TryGetValue(j, out index)) index = j;

        yield return list[index];

        if (!indices.TryGetValue(i, out index)) index = i;
        indices[j] = index;
    }
}

2

Sulla base della risposta di Kyle, ecco la mia implementazione di c #.

/// <summary>
/// Picks random selection of available game ID's
/// </summary>
private static List<int> GetRandomGameIDs(int count)
{       
    var gameIDs = (int[])HttpContext.Current.Application["NonDeletedArcadeGameIDs"];
    var totalGameIDs = gameIDs.Count();
    if (count > totalGameIDs) count = totalGameIDs;

    var rnd = new Random();
    var leftToPick = count;
    var itemsLeft = totalGameIDs;
    var arrPickIndex = 0;
    var returnIDs = new List<int>();
    while (leftToPick > 0)
    {
        if (rnd.Next(0, itemsLeft) < leftToPick)
        {
            returnIDs .Add(gameIDs[arrPickIndex]);
            leftToPick--;
        }
        arrPickIndex++;
        itemsLeft--;
    }

    return returnIDs ;
}

2

Questo metodo può essere equivalente a quello di Kyle.

Supponi che la tua lista abbia dimensioni n e desideri k elementi.

Random rand = new Random();
for(int i = 0; k>0; ++i) 
{
    int r = rand.Next(0, n-i);
    if(r<k) 
    {
        //include element i
        k--;
    }
} 

Funziona come un fascino :)

-Alex Gilbert


1
Sembra equivalente a me. Confronto alla simile stackoverflow.com/a/48141/2449863
DCShannon

1

perché non qualcosa del genere:

 Dim ar As New ArrayList
    Dim numToGet As Integer = 5
    'hard code just to test
    ar.Add("12")
    ar.Add("11")
    ar.Add("10")
    ar.Add("15")
    ar.Add("16")
    ar.Add("17")

    Dim randomListOfProductIds As New ArrayList

    Dim toAdd As String = ""
    For i = 0 To numToGet - 1
        toAdd = ar(CInt((ar.Count - 1) * Rnd()))

        randomListOfProductIds.Add(toAdd)
        'remove from id list
        ar.Remove(toAdd)

    Next
'sorry i'm lazy and have to write vb at work :( and didn't feel like converting to c#


1

Obiettivo: selezionare N numero di elementi dall'origine raccolta senza duplicazione. Ho creato un'estensione per qualsiasi raccolta generica. Ecco come l'ho fatto:

public static class CollectionExtension
{
    public static IList<TSource> RandomizeCollection<TSource>(this IList<TSource> source, int maxItems)
    {
        int randomCount = source.Count > maxItems ? maxItems : source.Count;
        int?[] randomizedIndices = new int?[randomCount];
        Random random = new Random();

        for (int i = 0; i < randomizedIndices.Length; i++)
        {
            int randomResult = -1;
            while (randomizedIndices.Contains((randomResult = random.Next(0, source.Count))))
            {
                //0 -> since all list starts from index 0; source.Count -> maximum number of items that can be randomize
                //continue looping while the generated random number is already in the list of randomizedIndices
            }

            randomizedIndices[i] = randomResult;
        }

        IList<TSource> result = new List<TSource>();
        foreach (int index in randomizedIndices)
            result.Add(source.ElementAt(index));

        return result;
    }
}

0

Di recente l'ho fatto sul mio progetto usando un'idea simile al punto 1 di Tyler .
Stavo caricando un sacco di domande e ne selezionavo cinque a caso. L'ordinamento è stato realizzato utilizzando un IComparer .
aTutte le domande sono state caricate nell'elenco a QuestionSorter, che è stato quindi ordinato utilizzando la funzione di ordinamento dell'elenco e i primi k elementi dove selezionato.

    private class QuestionSorter : IComparable<QuestionSorter>
    {
        public double SortingKey
        {
            get;
            set;
        }

        public Question QuestionObject
        {
            get;
            set;
        }

        public QuestionSorter(Question q)
        {
            this.SortingKey = RandomNumberGenerator.RandomDouble;
            this.QuestionObject = q;
        }

        public int CompareTo(QuestionSorter other)
        {
            if (this.SortingKey < other.SortingKey)
            {
                return -1;
            }
            else if (this.SortingKey > other.SortingKey)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
    }

Uso:

    List<QuestionSorter> unsortedQuestions = new List<QuestionSorter>();

    // add the questions here

    unsortedQuestions.Sort(unsortedQuestions as IComparer<QuestionSorter>);

    // select the first k elements

0

Ecco il mio approccio (testo completo qui http://krkadev.blogspot.com/2010/08/random-numbers-without-repetition.html ).

Dovrebbe essere eseguito in O (K) invece di O (N), dove K è il numero di elementi desiderati e N è la dimensione dell'elenco tra cui scegliere:

public <T> List<T> take(List<T> source, int k) {
 int n = source.size();
 if (k > n) {
   throw new IllegalStateException(
     "Can not take " + k +
     " elements from a list with " + n +
     " elements");
 }
 List<T> result = new ArrayList<T>(k);
 Map<Integer,Integer> used = new HashMap<Integer,Integer>();
 int metric = 0;
 for (int i = 0; i < k; i++) {
   int off = random.nextInt(n - i);
   while (true) {
     metric++;
     Integer redirect = used.put(off, n - i - 1);
     if (redirect == null) {
       break;
     }
     off = redirect;
   }
   result.add(source.get(off));
 }
 assert metric <= 2*k;
 return result;
}

0

Non è elegante o efficiente come la soluzione accettata, ma è veloce da scrivere. Innanzitutto, permuta l'array in modo casuale, quindi seleziona i primi elementi K. In pitone,

import numpy

N = 20
K = 5

idx = np.arange(N)
numpy.random.shuffle(idx)

print idx[:K]

0

Vorrei usare un metodo di estensione.

    public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> elements, int countToTake)
    {
        var random = new Random();

        var internalList = elements.ToList();

        var selected = new List<T>();
        for (var i = 0; i < countToTake; ++i)
        {
            var next = random.Next(0, internalList.Count - selected.Count);
            selected.Add(internalList[next]);
            internalList[next] = internalList[internalList.Count - selected.Count];
        }
        return selected;
    }

0
public static IEnumerable<T> GetRandom<T>(this IList<T> list, int count, Random random)
    {
        // Probably you should throw exception if count > list.Count
        count = Math.Min(list.Count, count);

        var selectedIndices = new SortedSet<int>();

        // Random upper bound
        int randomMax = list.Count - 1;

        while (selectedIndices.Count < count)
        {
            int randomIndex = random.Next(0, randomMax);

            // skip over already selected indeces
            foreach (var selectedIndex in selectedIndices)
                if (selectedIndex <= randomIndex)
                    ++randomIndex;
                else
                    break;

            yield return list[randomIndex];

            selectedIndices.Add(randomIndex);
            --randomMax;
        }
    }

Memoria: ~ count
Complessità: O (count 2 )


0

Quando N è molto grande, il normale metodo che mescola casualmente i numeri N e seleziona, diciamo, i primi k numeri, può essere proibitivo a causa della complessità dello spazio. Il seguente algoritmo richiede solo O (k) sia per complessità temporali che spaziali.

http://arxiv.org/abs/1512.00501

def random_selection_indices(num_samples, N):
    modified_entries = {}
    seq = []
    for n in xrange(num_samples):
        i = N - n - 1
        j = random.randrange(i)

        # swap a[j] and a[i] 
        a_j = modified_entries[j] if j in modified_entries else j 
        a_i = modified_entries[i] if i in modified_entries else i

        if a_i != j:
            modified_entries[j] = a_i   
        elif j in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(j)

        if a_j != i:
            modified_entries[i] = a_j 
        elif i in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(i)
        seq.append(a_j)
    return seq

0

Utilizzando LINQ con elenchi di grandi dimensioni (quando è costoso toccare ciascun elemento) E se puoi vivere con la possibilità di duplicati:

new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))

Per il mio uso ho avuto un elenco di 100.000 elementi, e per il fatto che sono stati estratti da un DB I circa la metà (o meglio) del tempo rispetto a un secondo dell'intero elenco.

Avere un grande elenco ridurrà notevolmente le probabilità per i duplicati.


Questa soluzione può avere elementi ripetuti !! Il casuale nell'elenco delle buche potrebbe non esserlo.
AxelWass il

Hmm. Vero. Dove lo uso, non importa però. Modificato la risposta per riflettere ciò.
Lupo, 5

-1

Questo risolverà il tuo problema

var entries=new List<T>();
var selectedItems = new List<T>();


                for (var i = 0; i !=10; i++)
                {
                    var rdm = new Random().Next(entries.Count);
                        while (selectedItems.Contains(entries[rdm]))
                            rdm = new Random().Next(entries.Count);

                    selectedItems.Add(entries[rdm]);
                }

Sebbene ciò possa rispondere alla domanda, è necessario modificare la risposta per includere una spiegazione di come questo blocco di codice risponde alla domanda. Questo aiuta a fornire un contesto e rende la tua risposta molto più utile ai futuri lettori.
Hoppeduppeanut,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.