Существует ли разница в производительности между этими двумя алгоритмами для перетасовки IEnumerable?

Эти два вопроса дают похожие альгорифты для перетаскивания IEnumerable:

Вот два метода бок о бок:

public static IEnumerable<T> Shuffle1<T> (this IEnumerable<T> source)
{
    Random random = new Random ();
    T [] copy = source.ToArray ();

    for (int i = copy.Length - 1; i >= 0; i--) {
        int index = random.Next (i + 1);
        yield return copy [index];
        copy [index] = copy [i];
    }
}


public static IEnumerable<T> Shuffle2<T> (this IEnumerable<T> source)
{
    Random random = new Random ();
    List<T> copy = source.ToList ();

    while (copy.Count > 0) {
        int index = random.Next (copy.Count);
        yield return copy [index];
        copy.RemoveAt (index);
    }
}

Они в основном идентичны, за исключением того, что используется List, и один использует массив. Понятно, что второй мне кажется более понятным. Но есть ли существенное преимущество в производительности от использования массива? Даже если время Big-O одинаковое, если оно в несколько раз быстрее, это может сделать заметную разницу.

Ответ 1

Вторая версия, вероятно, будет немного медленнее из-за RemoveAt. Списки - это действительно массивы, которые растут, когда вы добавляете к ним элементы, и поэтому вставка и удаление в середине медленны (на самом деле MSDN заявляет, что RemoveAt имеет сложность O (n)).

В любом случае лучше всего просто использовать профилировщик для сравнения обоих методов.

Ответ 2

Первый алгоритм - O (n), поскольку он имеет цикл, который выполняет обход O (1) на каждой итерации. Второй алгоритм O (n ^ 2), поскольку он выполняет операцию O (n) RemoveAt на каждой итерации. Кроме того, индексы в списках медленнее, чем индексы на массивах, поскольку первый - это вызов метода, тогда как последний является инструкцией IL.

Таким образом, из двух первый, скорее всего, будет быстрее. Тем не менее, если вы после исполнения, зачем беспокоиться о результатах? Он уже конвертируется в массив, поэтому просто перетасовывайте его и возвращайте массив напрямую (или завернутый в ReadOnlyCollection<T>, если вы беспокоитесь о том, что люди меняют его), что, вероятно, еще быстрее.

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

Ответ 3

Первый не компилируется, хотя очевидно, что вы пытаетесь перечислить перечислимое, а затем реализовать Fisher-Yates; что, вероятно, правильный подход, и это не должно быть непонятным для тех, кто когда-либо перетасовал массив раньше. Второе использование RemoveAt плохо по причинам, указанным другими комментаторами.

РЕДАКТИРОВАТЬ: Ваша верхняя реализация теперь выглядит правильно, и это хороший способ сделать это.