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>
.
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>
.
Risposte:
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.
Utilizzando linq:
YourList.OrderBy(x => rnd.Next()).Take(5)
YourList
ha molti articoli ma vuoi selezionarne solo alcuni. In questo caso non è un modo efficace di farlo.
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.
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);
}
Random random = new Random(DateTime.Now.Millisecond);
su ogni chiamata è sicuramente sbagliato. La creazione di una nuova istanza di Random
ogni volta riduce la casualità effettiva. Utilizzare static readonly
un'istanza di esso, preferibilmente costruita con il costruttore predefinito.
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.
Puoi usarlo ma l'ordinazione avverrà sul lato client
.AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);
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.
var
risultati in needed
ed available
essendo entrambi numeri interi, il che rende needed/available
sempre 0.
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 count
diventa 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, 5
o 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, 4
ecc In secondo luogo, quando il conteggio di oggetti casuali per essere restituito è più della metà del gruppo originale, quindi è più facile da rimuoveresource.Count - count
elementi dal gruppo che aggiungere invece dicount
elementi. Per motivi di prestazioni ho usatosource
sourceDict
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 HashSet
sarà 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 randoms
variabile viene creata HashSet
per evitare l'aggiunta di duplicati nei casi più rari e più rari in cui Random.Next
può 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 ..
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 subset
indici casuali e poi strapparli dall'elenco originale.
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 HashSet
degli indici , non gli elementi dell'elenco reale (nel caso in cui tu abbia tipi complessi o confronti costosi);
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);
}
}
}
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));
}
}
if (itemsList != null && k < n/2)
quale mezzo dentro if
invertSet
è sempre il false
che significa che la logica non viene mai usata.
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
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.
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++;
}
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;
}
}
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 ;
}
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
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#
È molto più difficile di quanto si pensi. Vedi il grande articolo "Shuffling" di Jeff.
Ho scritto un articolo molto breve sull'argomento incluso il codice C #:
restituisce un sottoinsieme casuale di N elementi di un determinato array
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;
}
}
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
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;
}
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;
}
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 )
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
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.
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]);
}