Виберіть N випадкових елементів зі списку <T> у C #


158

Мені потрібен швидкий алгоритм, щоб вибрати 5 випадкових елементів із загального списку. Наприклад, я хотів би отримати 5 випадкових елементів з а List<string>.


11
Випадково ви маєте на увазі інклюзивне чи ексклюзивне? IOW, чи можна вибрати один і той же елемент не один раз? (по-справжньому випадковим чином) Або після того, як елемент вибраний, його більше не можна буде брати з наявного пулу?
Кренцель

Відповіді:


127

Ітерація через і для кожного елемента зробить ймовірність вибору = (необхідне число) / (кількість залишилось)

Тож якби у вас було 40 предметів, перший мав би шанс обрати 5/40. Якщо це так, наступний має шанс 4/39, в іншому випадку - 5/39 шанс. До того часу, як ви досягнете кінця, у вас буде 5 ваших предметів, і часто ви будете мати всі їх до цього.


33
Я вважаю, що це тонко неправильно. Схоже, що задній кінець списку буде вибиратися частіше, ніж передній, оскільки задній кінець побачить набагато більші ймовірності. Наприклад, якщо перші 35 номерів не вибираються, останні 5 номерів повинні бути вибрані. Перше число колись побачить лише шанс 5/40, але останнє число побачить 1/1 частіше, ніж 5/40 разів. Перш ніж реалізувати цей алгоритм, вам доведеться спочатку рандомізувати список.
Ankur Goel

23
Ок, я запустив цей алгоритм 10 мільйонів разів за списком з 40 елементів, кожен з яких 5/40 (.125) вистрілив при виборі, а потім кілька разів запустив це моделювання. Виявляється, це розподілено не рівномірно. Елементи 16 через 22 отримують недобір (16 = .123, 17 = .124), тоді як елемент 34 перебирається (34 = .129). Елементи 39 і 40 також недообираються, але не на стільки (39 = .1247, 40 = .1246)
Анкур Гоел

21
@Ankur: Я не вірю, що це статистично важливо. Я вважаю, що є спонукальний доказ того, що це забезпечить рівномірний розподіл.
рекурсивна

9
Я повторював ту саму пробу 100 мільйонів разів, і в моєму випробуванні найменш обраний предмет був обраний менше ніж на 0,106% рідше, ніж найчастіше вибраний предмет.
рекурсивна

5
@recursive: Доказ майже банальний. Ми знаємо, як вибрати K елементів із K для будь-якого K і як вибрати 0 елементів з N для будь-якого N. Припустимо, ми знаємо метод рівномірного вибору елементів K або K-1 з N-1> = K; то ми можемо вибрати K елементів із N, вибравши перший елемент з вірогідністю K / N, а потім, використовуючи відомий метод, для вибору ще потрібних елементів K або K-1 з решти N-1.
Ільмарі Каронен

216

Використання linq:

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

2
+1 Але якщо два елементи отримують однакове число від rnd.Next () або подібне, тоді перший буде обраний, а другий, можливо, не буде (якщо більше елементів не потрібно). Це, однак, досить випадково, залежно від використання.
Lasse Espeholt

7
Я думаю, що порядок за допомогою O (n log (n)), тому я вибрав би це рішення, якщо простота коду є основною проблемою (тобто з невеликими списками).
Гвідо

2
Але це не перераховує і не сортує весь список? Якщо тільки "швидкий", OP означав "легкий", а не "
виконавець

2
Це буде працювати лише в тому випадку, якщо OrderBy () лише один раз викликає селектор ключів для кожного елемента. Якщо він викликає його коли завгодно, щоб здійснити порівняння між двома елементами, то він щоразу отримуватиме інше значення, що накрутить сортування. [Документація] ( msdn.microsoft.com/en-us/library/vstudio/… ) не говорить про те, що це робить.
Олівер Бок

2
Слідкуйте, чи YourListбагато предметів, але ви хочете вибрати лише кілька. У цьому випадку це не ефективний спосіб зробити це.
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

Це насправді важча проблема, ніж це здається, головним чином тому, що багато математично правильних рішень не зможуть насправді дозволити вам вразити всі можливості (докладніше про це нижче).

По-перше, ось кілька простих у виконанні, правильних, якщо у вас є, справді випадкових генераторів чисел:

(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

Я пропоную ознайомитися з кількома зразками випадків, щоб побачити, як це ефективно реалізує вищевказане англійське пояснення.


2
для (1) ви можете перетасувати список швидше, ніж сортування, для (2) ви будете змінювати розподіл, використовуючи%
jk.

З огляду на заперечення ви піднято питання про тривалість циклу ГСЧА, чи є спосіб , яким ми можемо побудувати алгоритм , який буде вибирати все набори з однаковою ймовірністю?
Йона

Для (1), для поліпшення O (n log (n)), ви можете використовувати сортування вибору, щоб знайти k найменших елементів. Це буде працювати в O (n * k).
Джаред

@Jonah: Я так думаю. Припустимо, ми можемо об'єднати кілька незалежних генераторів випадкових чисел для створення більшого ( crypto.stackexchange.com/a/27431 ). Тоді вам просто потрібен достатньо великий діапазон, щоб вирішити розмір списку, про який йдеться.
Джаред

16

Я вважаю, що обрана відповідь правильна і досить мила. Я реалізував це інакше, хоча я також хотів результату у випадковому порядку.

    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);
    }

ДУЖЕ! Дійсно мені допомогли!
Армстронджест

1
Чи є у вас якісь причини не використовувати новий Random (), заснований на Environment.TickCount проти DateTime.Now.Millisecond?
Lasse Espeholt

Ні, просто не знали, що за замовчуванням існує.
Френк Швітерман

Удосконалення randomSortTable: randomSortTable = someTypes.ToDictionary (x => random.NextDouble (), y => y); Зберігає цикл передбачення.
Келтекс

2
Гаразд, запізнився на рік, але ... Чи не вдається це скоріше відповісти на @ ersin, і чи не вийде, якщо ви отримаєте повторне випадкове число (де Ерсін матиме упередженість до першого пункту повторної пари)
Andiih

12

Я щойно зіткнувся з цією проблемою, і кілька інших пошуків 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.


Я реалізував це за допомогою C # у своєму дописі в блозі: vijayt.com/post/random-select-using-fisher-yates-algorithm . Сподіваюсь, це допомагає комусь шукати шлях C #.
vijayst

9

Ви можете використовувати це, але замовлення відбудеться на стороні клієнта

 .AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);

Домовились. Це може бути не найкращим чи найбільш випадковим, але для переважної більшості людей це буде досить добре.
Річібан


8

Із Драконів в алгоритмі інтерпретація в 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--;
}

Цей алгоритм вибере унікальні показники списку елементів.


Отримайте лише достатню кількість у списку, але не отримайте випадковим чином.
винний

2
Ця реалізація порушена, оскільки використовуються varрезультати в neededі availableобидва є цілими числами, що робить needed/availableзавжди 0.
Ніко

1
Це здається реалізацією прийнятої відповіді.
DCShannon

6

Вибір 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, але я не думаю, що це велика справа, враховуючи, що останній, швидше за все, ніколи не вузьке місце, його досить швидко ..

Редагувати: Якщо вам також потрібно впорядкувати порядок повернених предметів, тоді нічого, що може перемогти підхід Дакіма Фішера-Йейтса, є коротким, милим і простим ..


6

Думав про коментар @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);
        }
    }
}

2
Гарна ідея, з проблемами. (1) Якщо ваш більший список величезний (наприклад, читайте з бази даних), ви усвідомлюєте весь список, який може перевищувати пам'ять. (2) Якщо K близький до N, то ви багато будете шукати незатребуваний індекс у своєму циклі, внаслідок чого коду потрібно непередбачувана кількість часу. Ці проблеми вирішуються.
Пол Черноч

1
Моє рішення проблеми молотіння полягає в наступному: якщо K <N / 2, зробіть це по-своєму. Якщо K> = N / 2, виберіть індекси, які НЕ слід зберігати, а не ті, які слід зберігати. Ще є щось молотання, але набагато менше.
Пол Черноч

Також зауважив, що це змінює порядок перерахування предметів, що може бути прийнятним в одних ситуаціях, а не в інших.
Пол Черноч

В середньому, для K = N / 2 (найгірший випадок, коли запропоновано поліпшення поліпшення), алгоритм (обмолочений покращений), схоже, приймає ~ 0,693 * N ітерацій. Тепер зробіть порівняння швидкості. Це краще, ніж прийнята відповідь? Для яких розмірів вибірки?
mbomb007

6

Я поєднав декілька вищезазначених відповідей, щоб створити метод розширення з оцінкою "Лінь". Моє тестування показало, що підхід Кайла (Порядок (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означає, що логіка ніколи не використовується.
NetMage

4

Поширюючись на відповідь @ 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

3

Це найкраще, що я міг придумати на першому розрізі:

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 - загальний підрахунок списку, а потім просто витягнення цих елементів у списку, здавалося, є найкращим способом, але використання словника для забезпечення унікальності - це те, що я все ще переглядаю.

Також зауважте, що я використав список рядків, замініть за потребою.


1
Працювали на першому пострілі!
сангам

3

Просте рішення, яке я використовую (напевно, не годиться для великих списків): Скопіюйте список у тимчасовий список, потім у циклі випадковим чином виберіть пункт «Елемент зі списку темпів» та помістіть його у список вибраних елементів, видаливши його з тимчасового списку (щоб він не міг бути переібрано).

Приклад:

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++;
 }

Видалення з середини списку так часто буде дорогим. Ви можете розглянути можливість використання пов'язаного списку для алгоритму, що вимагає стільки видалень. Або, що аналогічно, замініть вилучений елемент нульовим значенням, але тоді ви трохи обточите, коли ви виберете вже вилучені елементи і доведеться вибирати знову.
Пол Черноч

3

Тут ви маєте одну реалізацію, засновану на Фішера-Йейтса Шефле , складність алгоритму якого 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;
    }
}

2

На основі відповіді Кайла, ось моя реалізація 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

Цей метод може бути еквівалентним методу Кайла.

Скажіть, ваш список має розмір 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--;
    }
} 

Працює як шарм :)

-Алекс Гілберт


1
Це виглядає для мене рівнозначно. За порівнянні з аналогічним stackoverflow.com/a/48141/2449863
DCShannon

1

чому б не щось подібне:

 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

Мета: Виберіть 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;
    }
}

0

Нещодавно я зробив це у своєму проекті, використовуючи ідею, подібну до точки 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

0

Ось мій підхід (повний текст тут 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;
}

0

Це не настільки елегантно чи ефективно, як прийняте рішення, але це швидко записати. Спочатку перестановіть масив випадковим чином, потім виберіть перші K-елементи. У пітоні,

import numpy

N = 20
K = 5

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

print idx[:K]

0

Я б використав метод розширення.

    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;
        }
    }

Пам'ять: ~ count
Складність: O (кількість 2 )


0

Коли 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

0

Використання LINQ з великими списками (коли дорого доторкнутися до кожного елемента) І якщо ви можете жити з можливістю копій:

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

Для мого використання у мене був список 100 000 елементів, і через їх витягнення з БД я приблизно вдвічі (або краще) час порівняно з rnd у всьому списку.

Наявність великого списку значно зменшить шанси для дублікатів.


Це рішення може мати повторні елементи !! Випадковий у списку дірок може не бути.
AxelWass

Хм. Правда. Де я його використовую, це не має значення. Відредагував відповідь, щоб це відобразити.
Wolf5

-1

Це вирішить вашу проблему

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]);
                }

Хоча це може відповісти на питання, ви повинні відредагувати свою відповідь, щоб включити пояснення, як цей блок коду відповідає на питання. Це допомагає надати контекст і робить вашу відповідь набагато кориснішою для майбутніх читачів.
Hoppeduppeanut
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.