Мені потрібен швидкий алгоритм, щоб вибрати 5 випадкових елементів із загального списку. Наприклад, я хотів би отримати 5 випадкових елементів з а List<string>
.
Мені потрібен швидкий алгоритм, щоб вибрати 5 випадкових елементів із загального списку. Наприклад, я хотів би отримати 5 випадкових елементів з а List<string>
.
Відповіді:
Ітерація через і для кожного елемента зробить ймовірність вибору = (необхідне число) / (кількість залишилось)
Тож якби у вас було 40 предметів, перший мав би шанс обрати 5/40. Якщо це так, наступний має шанс 4/39, в іншому випадку - 5/39 шанс. До того часу, як ви досягнете кінця, у вас буде 5 ваших предметів, і часто ви будете мати всі їх до цього.
Використання linq:
YourList.OrderBy(x => rnd.Next()).Take(5)
YourList
багато предметів, але ви хочете вибрати лише кілька. У цьому випадку це не ефективний спосіб зробити це.
Це насправді важча проблема, ніж це здається, головним чином тому, що багато математично правильних рішень не зможуть насправді дозволити вам вразити всі можливості (докладніше про це нижче).
По-перше, ось кілька простих у виконанні, правильних, якщо у вас є, справді випадкових генераторів чисел:
(0) Відповідь Кайла, яка є O (n).
(1) Створіть список з n пар [(0, rand), (1, rand), (2, rand), ...], сортуйте їх за другою координатою та використовуйте першу k (для вас, k = 5) індекси для отримання випадкового набору. Я думаю, що це легко здійснити, хоча це час O (n log n).
(2) Запустіть порожній список s = [], який зростатиме до показників k випадкових елементів. Виберіть число r у {0, 1, 2, ..., n-1} навмання, r = rand% n та додайте це до s. Далі візьміть r = rand% (n-1) і вставте в s; додайте в r елементів # менше, ніж в s, щоб уникнути зіткнень. Далі візьміть r = rand% (n-2) і зробіть те ж саме, і т.д., поки у вас не буде k різних елементів. Це найгірший час роботи O (k ^ 2). Тож для k << n це може бути швидше. Якщо ви будете сортувати і відслідковувати, які суміжні інтервали у нього є, ви можете реалізувати це в O (k log k), але це більше роботи.
@Kyle - ти маєш рацію, подумавши, я згоден з твоєю відповіддю. Спочатку я поспіхом прочитав це, і помилково подумав, що ти вказуєш на послідовний вибір кожного елемента з фіксованою ймовірністю k / n, що було б помилково - але твій адаптивний підхід мені здається правильним. Вибач за це.
Гаразд, і тепер для кікера: асимптотично (для фіксованого k, n зростаючого), є n ^ k / k! вибір підмножини k елементів із n елементів [це наближення (n вибирати k)]. Якщо n великий, а k не дуже малий, то ці числа величезні. Найкраща тривалість циклу, на яку можна сподіватися, у будь-якому стандартному 32-бітовому генераторі випадкових чисел - 2 ^ 32 = 256 ^ 4. Отже, якщо у нас є список з 1000 елементів, і ми хочемо вибрати 5 навмання, стандартний генератор випадкових чисел не зможе вразити всі можливості. Однак, поки ви все в порядку з вибором, який добре працює для менших наборів, і завжди "виглядає" випадковим, тоді ці алгоритми повинні бути нормальними.
Додаток : Після написання цього я зрозумів, що складно реалізувати ідею (2) правильно, тому хотів уточнити цю відповідь. Щоб отримати час O (k log k), вам потрібна структура, схожа на масив, яка підтримує O (log m) пошуку та вставки - збалансоване бінарне дерево може це зробити. Використовуючи таку структуру для складання масиву під назвою s, ось певний псевдопітон:
# 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
Я пропоную ознайомитися з кількома зразками випадків, щоб побачити, як це ефективно реалізує вищевказане англійське пояснення.
Я вважаю, що обрана відповідь правильна і досить мила. Я реалізував це інакше, хоча я також хотів результату у випадковому порядку.
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);
}
Я щойно зіткнувся з цією проблемою, і кілька інших пошуків Google привели мене до проблеми випадкового перетасування списку: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
Щоб повністю випадково перетасувати список (на місці), зробіть це:
Щоб перетасувати масив a з n елементів (індекси 0..n-1):
for i from n − 1 downto 1 do
j ← random integer with 0 ≤ j ≤ i
exchange a[j] and a[i]
Якщо вам потрібно лише перші 5 елементів, то замість того, щоб виконувати i весь шлях від n-1 до 1, потрібно лише запустити його до n-5 (тобто: n-5)
Скажімо, вам потрібно k елементів,
Це стає:
for (i = n − 1; i >= n-k; i--)
{
j = random integer with 0 ≤ j ≤ i
exchange a[j] and a[i]
}
Кожен обраний елемент підміняється до кінця масиву, тому вибрані k елементи є останніми k елементами масиву.
Для цього потрібен час O (k), де k - кількість випадково вибраних елементів, які вам потрібні.
Крім того, якщо ви не хочете змінювати свій початковий список, ви можете записати всі свої свопи у тимчасовий список, скасувати цей список і застосувати їх знову, таким чином виконавши зворотний набір свопів і повернувши вам свій початковий список, не змінюючи час роботи O (k).
Нарешті, для справжнього стикера, якщо (n == k), ви повинні зупинитися на 1, а не на nk, оскільки випадково вибране ціле число завжди буде 0.
Ви можете використовувати це, але замовлення відбудеться на стороні клієнта
.AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);
Із Драконів в алгоритмі інтерпретація в 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--;
}
Цей алгоритм вибере унікальні показники списку елементів.
var
результати в needed
і available
обидва є цілими числами, що робить needed/available
завжди 0.
Вибір N випадкових елементів з групи не повинен мати нічого спільного з замовленням ! Випадковість - це непередбачуваність, а не змішування позицій у групі. Усі відповіді, які стосуються деяких видів упорядкування, мають бути менш ефективними, ніж ті, які цього не роблять. Оскільки ефективність тут є ключовою, я опублікую щось, що не дуже змінить порядок позицій.
1) Якщо вам потрібні справжні випадкові значення, це означає, що немає обмежень щодо вибору елементів (тобто, коли обраний елемент можна буде вибрати повторно):
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;
}
Якщо ви встановите прапор виключення, ви можете вибирати випадкові елементи будь-яку кількість разів.
Якщо у вас {1, 2, 3, 4}, він може дати {1, 4, 4}, {1, 4, 3} і т. Д. Для 3 предметів або навіть {1, 4, 3, 2, 4} для 5 предметів!
Це повинно бути досить швидким, оскільки перевірити його нема чого.
2) Якщо вам потрібні окремі члени групи без повторення, я б покладався на словник (як уже багато вказували).
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();
}
Код трохи довший, ніж інші підходи до словника тут, тому що я не тільки додаю, але й видаляю зі списку, тому його щось два цикли. Тут ви бачите, що я взагалі нічого не упорядкував, коли count
став рівним source.Count
. Це тому, що я вважаю, що випадковість повинна бути у поверненому наборі в цілому . Я маю в виду , якщо ви хочете 5 випадкових елементів з 1, 2, 3, 4, 5
, він повинен не має значення , якщо його 1, 3, 4, 2, 5
або 1, 2, 3, 4, 5
, але якщо вам потрібно 4 елементів з того ж набору, то він повинен непередбачувано виходу в 1, 2, 3, 4
, 1, 3, 5, 2
,2, 3, 5, 4
та т.д. По друге, коли підрахунок випадкових предметів, що підлягають повернуто більше половини початкової групи, то її легше видалитиsource.Count - count
елементи з групи, ніж додавання count
елементів. З міркувань продуктивності я використовував source
замість того, sourceDict
щоб отримати випадковий індекс у методі видалення.
Отже, якщо у вас є {1, 2, 3, 4}, це може закінчитися через {1, 2, 3}, {3, 4, 1} тощо для трьох предметів.
3) Якщо вам потрібні по-справжньому відмінні випадкові значення від вашої групи, беручи до уваги дублікати в початковій групі, ви можете використовувати той же підхід, що і вище, але HashSet
заголовок буде легшим, ніж словник.
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();
}
randoms
Змінної прийнято , HashSet
щоб уникнути дублікатів додають в рідкісних рідкісних випадках , коли Random.Next
може дати таке ж значення, особливо коли список вхідних малий.
Отже {1, 2, 2, 4} => 3 випадкові предмети => {1, 2, 4} і ніколи {1, 2, 2}
{1, 2, 2, 4} => 4 випадкові предмети => виняток !! або {1, 2, 4} залежно від встановленого прапора.
Деякі з методів розширення, які я використав:
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);
}
Якщо все про продуктивність з десятками 1000-ти предметів у списку потрібно повторити 10000 разів, то, можливо, ви хочете мати швидший випадковий клас, ніж System.Random
, але я не думаю, що це велика справа, враховуючи, що останній, швидше за все, ніколи не вузьке місце, його досить швидко ..
Редагувати: Якщо вам також потрібно впорядкувати порядок повернених предметів, тоді нічого, що може перемогти підхід Дакіма Фішера-Йейтса, є коротким, милим і простим ..
Думав про коментар @JohnShedletsky щодо прийнятої відповіді щодо (перефразовування):
ви повинні мати змогу зробити це в O (підмножина.Length), а не O (originalList.Length)
В основному, ви повинні мати можливість генерувати subset
випадкові індекси та потім виривати їх із вихідного списку.
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()));
}
}
Якби ви хотіли бути ще ефективнішими, ви, ймовірно, використовували б один HashSet
з показників , а не фактичне елементи списку (в разі , якщо у вас є складні типи або дорогі порівняння);
І щоб переконатися, що у нас немає зіткнень тощо.
[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);
}
}
}
Я поєднав декілька вищезазначених відповідей, щоб створити метод розширення з оцінкою "Лінь". Моє тестування показало, що підхід Кайла (Порядок (N)) у багато разів повільніше, ніж використання набору drzaus для набору випадкових індексів для вибору (Order (K)). Перший виконує набагато більше дзвінків до генератора випадкових чисел, а також повторює більше разів по пунктах.
Цілями моєї реалізації були:
1) Не усвідомлюйте повного списку, якщо дано IEnumerable, який не є IList. Якщо мені подарують послідовність мільйонів предметів, я не хочу закінчуватися пам'яттю. Використовуйте підхід Кайла для он-лайн рішення.
2) Якщо я можу сказати, що це IList, використовуйте підхід drzaus із поворотом. Якщо K більше половини N, я ризикую обмолотити, оскільки я вибираю багато випадкових індексів знову і знову, і мені доведеться їх пропускати. Таким чином, я складаю список індексів, які НЕ слід зберігати.
3) Я гарантую, що предмети будуть повернені в тому ж порядку, в якому вони зустрічалися. Алгоритм Кайла не потребував змін. Алгоритм drzaus вимагав, щоб я не випромінював елементи в тому порядку, у якому обрані випадкові індекси. Я збираю всі індекси в SortedSet, а потім випускаю елементи в порядку відсортованого індексу.
4) Якщо K є великим порівняно з N, і я інвертую сенс множини, то я перераховую всі предмети і перевіряю, чи не є індекс у множині. Це означає, що я втрачаю час виконання порядку (K), але оскільки K в цих випадках близький до N, я не втрачаю багато.
Ось код:
/// <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;
}
}
}
}
Я використовую спеціалізований генератор випадкових чисел, але ви можете просто скористатися C # 's Random, якщо хочете. ( FastRandom був написаний Коліном Гріном і є частиною SharpNEAT. Він має період 2 ^ 128-1, що краще, ніж багато RNG.)
Ось одиничні тести:
[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)
що означає всередині, if
invertSet
це завжди false
означає, що логіка ніколи не використовується.
Поширюючись на відповідь @ ers, якщо вас турбують можливі різні реалізації OrderBy, це має бути безпечним:
// 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
Це найкраще, що я міг придумати на першому розрізі:
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;
}
Використання списку randoms в діапазоні 1 - загальний підрахунок списку, а потім просто витягнення цих елементів у списку, здавалося, є найкращим способом, але використання словника для забезпечення унікальності - це те, що я все ще переглядаю.
Також зауважте, що я використав список рядків, замініть за потребою.
Просте рішення, яке я використовую (напевно, не годиться для великих списків): Скопіюйте список у тимчасовий список, потім у циклі випадковим чином виберіть пункт «Елемент зі списку темпів» та помістіть його у список вибраних елементів, видаливши його з тимчасового списку (щоб він не міг бути переібрано).
Приклад:
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++;
}
Тут ви маєте одну реалізацію, засновану на Фішера-Йейтса Шефле , складність алгоритму якого O (n), де n - підмножина або розмір вибірки, а не розмір списку, як зазначив Джон Шедлецький.
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;
}
}
На основі відповіді Кайла, ось моя реалізація 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 ;
}
Цей метод може бути еквівалентним методу Кайла.
Скажіть, ваш список має розмір n, і ви хочете k елементів.
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--;
}
}
Працює як шарм :)
-Алекс Гілберт
чому б не щось подібне:
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#
Це набагато важче, ніж можна було б подумати. Дивіться чудову статтю "Перемішання" від Джеффа.
Я написав дуже коротку статтю на цю тему, включаючи код C #:
Повернення випадкового підмножини з N елементів заданого масиву
Мета: Виберіть N кількість елементів із джерела колекції без дублювання. Я створив розширення для будь-якої загальної колекції. Ось як я це зробив:
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;
}
}
Нещодавно я зробив це у своєму проекті, використовуючи ідею, подібну до точки 1 Тайлера .
Я завантажував купу запитань і вибирав навмання п’ять. Сортування здійснювалося за допомогою IComparer .
aВсі запитання завантажувались у список QuestionSorter, який потім був сортований за допомогою функції Сортування списку та перших k елементів, де вибрано.
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;
}
}
}
Використання:
List<QuestionSorter> unsortedQuestions = new List<QuestionSorter>();
// add the questions here
unsortedQuestions.Sort(unsortedQuestions as IComparer<QuestionSorter>);
// select the first k elements
Ось мій підхід (повний текст тут http://krkadev.blogspot.com/2010/08/random-numbers-without-repair.html ).
Він повинен працювати в O (K) замість O (N), де K - кількість шуканих елементів, а N - розмір списку, який можна вибрати:
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;
}
Я б використав метод розширення.
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;
}
}
Пам'ять: ~ count
Складність: O (кількість 2 )
Коли N дуже великий, звичайний метод, який випадковим чином переміщує N числа і вибирає, скажімо, перші k числа, може бути забороняючим через складність простору. Наступний алгоритм вимагає лише O (k) як для часових, так і для просторових складностей.
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
Використання LINQ з великими списками (коли дорого доторкнутися до кожного елемента) І якщо ви можете жити з можливістю копій:
new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))
Для мого використання у мене був список 100 000 елементів, і через їх витягнення з БД я приблизно вдвічі (або краще) час порівняно з rnd у всьому списку.
Наявність великого списку значно зменшить шанси для дублікатів.
Це вирішить вашу проблему
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]);
}