Выберите N случайных элементов из списка <T>в C # - PullRequest
141 голосов
/ 07 сентября 2008

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

Ответы [ 27 ]

185 голосов
/ 22 февраля 2010

Использование linq:

YourList.OrderBy(x => rnd.Next()).Take(5)
122 голосов
/ 07 сентября 2008

Итерируйте и для каждого элемента сделайте вероятность выбора = (необходимое число) / (оставшееся число)

Так что, если у вас было 40 предметов, первый имел бы шанс 5/40 быть выбранным. Если это так, у следующего есть шанс 4/39, в противном случае он имеет шанс 5/39. К тому времени, как вы дойдете до конца, у вас будет 5 предметов, и часто у вас будет все до этого.

29 голосов
/ 29 сентября 2012
public static List<T> GetRandomElements<T>(this IEnumerable<T> list, int elementsCount)
{
    return list.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();
}
26 голосов
/ 07 сентября 2008

Это на самом деле более сложная проблема, чем кажется, в основном потому, что многие математически правильные решения не позволят вам использовать все возможности (подробнее об этом ниже).

Во-первых, вот несколько простых в реализации, корректных, если вы действительно имеете случайный генератор случайных чисел:

(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 различных элементов в s. Это имеет наихудшее время выполнения O (k ^ 2). Так что для k << n это может быть быстрее. Если вы сохраняете сортировку и отслеживаете, какие у нее есть непрерывные интервалы, вы можете реализовать ее в O (k log k), но это больше работы. </p>

@ Кайл - ты прав, если подумать, я согласен с твоим ответом. Сначала я поспешно прочитал его и по ошибке подумал, что вы указали на последовательный выбор каждого элемента с фиксированной вероятностью 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

Я предлагаю пройтись по нескольким примерам примеров, чтобы увидеть, как это эффективно реализует приведенное выше объяснение на английском языке.

16 голосов
/ 16 января 2009

Я думаю, что выбранный ответ является правильным и довольно сладким. Я реализовал это по-другому, так как я также хотел получить результат в случайном порядке.

    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);
    }
10 голосов
/ 16 января 2012

Я только что столкнулся с этой проблемой, и некоторые другие поиски в 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, а не на n-k, так как случайно выбранное целое число всегда будет равно 0.

8 голосов
/ 23 марта 2012

Вы можете использовать это, но заказ будет происходить на стороне клиента

 .AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);
8 голосов
/ 07 сентября 2008

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

Этот алгоритм будет выбирать уникальные признаки списка предметов.

7 голосов
/ 22 июня 2013

Думал о комментарии @JohnShedletsky к принятому ответу относительно (перефразировать):

Вы должны быть в состоянии это в O (subset.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 из индексов , а не фактические элементы списка (в случае, если у вас сложные типы или дорогостоящие сравнения); 1018 *

Юнит тест

И чтобы убедиться, что у нас нет столкновений и т. Д.

[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);
        }
    }
}
5 голосов
/ 24 ноября 2012

Выбор N случайных предметов из группы не должен иметь ничего общего с order ! Случайность связана с непредсказуемостью, а не с перетасовкой позиций в группе. Все ответы, связанные с каким-то порядком, обязательно будут менее эффективными, чем те, которые этого не делают. Поскольку эффективность здесь является ключевым фактором, я опубликую то, что не сильно меняет порядок элементов.

1) Если вам нужно true случайные значения, что означает, что нет ограничений на выбор элементов (т. Е. Один раз выбранный элемент может быть повторно выбран):

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 элементов.

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

Если все дело в производительности с десятками тысяч элементов в списке, которые нужно повторять 10000 раз, то вы можете захотеть иметь более быстрый случайный класс , чем System.Random, но я не думаю, что это большое дело, учитывая, что последнее, скорее всего, никогда не является узким местом, оно достаточно быстрое ...

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

...