Метод расширения IEnumerable, необходимый для перемешивания

Мне нужен метод расширения, который будет перемешивать IEnumerable<T>. Также может потребоваться int, чтобы указать размер возвращаемого IEnumerable. Лучше сохранить неизменность IEnumerable. Мое текущее решение для IList-

public static IList<T> Shuffle<T>(this IList<T> list, int size)
{
    Random rnd = new Random();
    var res = new T[size];

    res[0] = list[0];
    for (int i = 1; i < size; i++)
    {
        int j = rnd.Next(i);
        res[i] = res[j];
        res[j] = list[i];
    }
    return res;
}

public static IList<T> Shuffle<T>(this IList<T> list)
{ return list.Shuffle(list.Count); }

person Gulshan    schedule 27.04.2011    source источник
comment
обратите внимание, что для появления < > они обычно должны быть отформатированы как код: либо в строке с обратными кавычками (как я добавил), либо (как вы это сделали) с отступом из четырех пробелов   -  person AakashM    schedule 27.04.2011


Ответы (3)


Вы можете использовать перемешивание Фишера-Йетса-Дюрстенфельда. Нет необходимости явно передавать аргумент размера самому методу, вы можете просто привязать вызов к _ 1_, если вам не нужна вся последовательность:

var shuffled = originalSequence.Shuffle().Take(5);

// ...

public static class EnumerableExtensions
{
    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
    {
        return source.Shuffle(new Random());
    }

    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (rng == null) throw new ArgumentNullException(nameof(rng));

        return source.ShuffleIterator(rng);
    }

    private static IEnumerable<T> ShuffleIterator<T>(
        this IEnumerable<T> source, Random rng)
    {
        var buffer = source.ToList();
        for (int i = 0; i < buffer.Count; i++)
        {
            int j = rng.Next(i, buffer.Count);
            yield return buffer[j];

            buffer[j] = buffer[i];
        }
    }
}
person LukeH    schedule 27.04.2011
comment
Цель второго метода - просто выбросить исключение? - person Gulshan; 28.04.2011
comment
Да, это так, что проверка аргументов выполняется охотно, а не откладывается. Если бы второй и третий методы были объединены вместе, то никакая проверка аргументов не происходила бы, пока вы не начнете повторять последовательность. - person LukeH; 28.04.2011
comment
Люк, когда вы вызываете source.ToList () в своем основном методе, не означает ли это, что IEnumerable выполняется (возможно, если это перечислимый Linq, то вы нарушаете их отложенное выполнение? Лучше попросить IList! - person nawfal; 25.11.2012
comment
@nawfal: Да, метод должен буферизовать содержимое источника IEnumerable<>, чтобы он мог выполнить перемешивание. Затем он лениво возвращает свой вывод. Я не уверен, что вы имеете в виду, говоря о IList, и как это могло бы помочь. - person LukeH; 26.11.2012
comment
@LukeH Предположим, что source в вашем случае var d = p.Select(...).OrderBy(...), когда вы делаете ToList изнутри метода, он выполняет вышеуказанный запрос. Если вместо параметра вы выбираете IList, выполнение (путем выполнения ToList или ToArray для d) должно выполняться со стороны вызывающей стороны, чтобы не возникло путаницы в отношении побочных эффектов метода Shuffle. Я надеюсь это ясно - person nawfal; 26.11.2012
comment
@nawfal: Понятно. Это противоречит философии LINQ об отсутствии побочных эффектов. Взять IEnumerable<> последовательность и вернуть новую, перетасованную последовательность предпочтительнее, imo, чем взятие IList<> и ее изменение. - person LukeH; 26.11.2012
comment
@nawfal: Для этого достаточно просто написать метод, отличный от LINQy: что-то вроде public static void ShuffleInPlace<T>(this IList<T> source) - person LukeH; 26.11.2012
comment
@LukeH Я не говорю, что перемешивание должно происходить на одном и том же перечислимом. Фактически он должен вернуть новую перетасованную последовательность. но поскольку метод перемешивания выполняет возможный запрос, который является IEnumerable source, он противоречит отложенному выполнению Linq. Так что, возможно, вы не сможете извлечь максимальную пользу из ленивой оценки Linq. Я просто говорю, что если этот метод Shuffle когда-либо используется с Linq, он ведет себя не так, как все типичные методы Linq. Если вы объявляете его как IList, вы даете вызывающей стороне информацию о том, будет ли выражение выполняться или нет. - person nawfal; 26.11.2012
comment
Я не говорю, что приведенный выше метод плох, просто он может быть не так хорош для использования вместе с Linq. Просто думаю. Я сделал это отдельным вопросом здесь только для того, чтобы получить голоса :) - person nawfal; 26.11.2012
comment
@nawfal: многие встроенные методы LINQ создают внутреннюю буферизацию для всей последовательности, а затем лениво дают результаты: например, GroupBy, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse и т. д. все должны буферизовать свою исходную последовательность; Except, GroupJoin, Intersect, Join и т. Д. Все буферизуют свою вторичную входную последовательность. Это не проблема, imo, хотя было бы неплохо четко задокументировать, должен ли метод внутренне буферизовать всю последовательность. - person LukeH; 26.11.2012
comment
Нет необходимости в существовании второго метода - person Dan Hunex; 05.06.2018

С любовью к LINQ:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> list, int size)
{
    var r = new Random();
    var shuffledList = 
        list.
            Select(x => new { Number = r.Next(), Item = x }).
            OrderBy(x => x.Number).
            Select(x => x.Item).
            Take(size); // Assume first @size items is fine

    return shuffledList.ToList();
}
person Anton Gogolev    schedule 27.04.2011
comment
Разве это не то же самое, что OrderBy(x => r.Next())? - person Gulshan; 27.04.2011
comment
@Gulshan: Да, это так. Сортировка по случайному числу не считается очень хорошим алгоритмом перетасовки. Это работает, но не так уж случайно, как могло бы быть. - person Gabe; 27.04.2011
comment
Нет, это не то же самое. OrderBy(x => r.Next()) потенциально может поместить сортировку в бесконечный цикл. Это не может. - person Jim Mischel; 28.04.2011
comment
@Jim: Метод OrderBy на самом деле делает что-то подобное внутренне в любом случае - генерирует ключ для каждого элемента один раз с использованием проекции, сохраняет его, а затем использует этот сохраненный ключ для сортировки - поэтому нет опасности бесконечный цикл с текущей реализацией MS. (Хотя нет гарантии, что реализация будет одинаковой на разных платформах / версиях.) - person LukeH; 28.04.2011
comment
@LukeH: Можете дать мне ссылку на дополнительную информацию по этому поводу? То, что вы сказали, не имеет для меня смысла, особенно в контексте функции сравнения (это то, что r.Next используется здесь). Что мне не хватает? - person Jim Mischel; 28.04.2011
comment
@Jim: Поскольку это всего лишь деталь реализации, официальной документации нет, но есть хорошее обсуждение в блоге Джона Скита: msmvps.com/blogs/jon_skeet/archive/2011/02/23 / (см. четыре статьи, составляющие раздел «Заказ», и их комментарии). - person LukeH; 28.04.2011
comment
@LukeH: Спасибо. По дороге домой я понял, что думаю, что параметр был делегатом сравнения, а не делегатом выбора. Это была плохая неделя. - person Jim Mischel; 28.04.2011

Идея у Антона есть, но вы могли бы сделать ее двухстрочной:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{
    var r = new Random();
    return enumerable.OrderBy(x=>r.Next()).ToList();
}

К сожалению, его нельзя оценивать лениво, потому что r выйдет за рамки, когда он выполнится. Вы можете создать реализацию IEnumerable, которая инкапсулирует этот код и возвращает его, но это становится более сложным.

person KeithS    schedule 27.04.2011
comment
Слышал, это предвзято. - person Gulshan; 27.04.2011
comment
@Gulshan: Это не должно быть слишком плохо, хотя тот факт, что OrderBy является стабильной сортировкой, может вызвать проблемы (и возможно, что Random сам может иметь некоторую врожденную предвзятость). Главный потенциальный недостаток использования OrderBy заключается в том, что он имеет временную сложность O (n lg n); возможно выполнить перемешивание за O (n) и без смещения. - person LukeH; 27.04.2011
comment
Это действительно, действительно плохая идея. Что-то подобное может вызвать бесконечный цикл в методе сортировки. - person Jim Mischel; 28.04.2011
comment
@Jim: текущая реализация OrderBy в CLR выполняет проекцию только один раз для каждого элемента, сохраняет ключ, а затем использует этот сохраненный ключ для упорядочивания, поэтому в настоящее время нет опасности бесконечного цикла. (Конечно, это зависит от детали реализации, которая теоретически может измениться.) - person LukeH; 28.04.2011
comment
@LukeH: Верно. Это не может вызвать бесконечный цикл. Я думал о делегате сравнения, а не о селекторе. - person Jim Mischel; 28.04.2011
comment
Почему вы ToList() -ing, когда возвращаете только IEnumerable? Это может вызвать побочные эффекты! - person nawfal; 25.11.2012
comment
Потому что, если вы вернете только итератор OrderBy, r выйдет из области видимости и будет уничтожен до того, как он когда-либо будет использован. Основной побочный эффект - дополнительная память; это не влияет на начальное перечислимое. - person KeithS; 26.11.2012
comment
Что с этим r выйдет за рамки? А как насчет захвата переменных? Опустить .ToList() в этом фрагменте кода не должно быть проблемой ... - person Dennis; 10.04.2014
comment
-1 Это управляемый язык, r размещен в куче и никогда не выйдет за пределы области видимости, GC гарантирует, что r не будет собирать мусор до тех пор, пока на него больше не будет ссылаться. - person Lukazoid; 30.10.2014