Список <T> и IEnumerable разность

При реализации этого общего merge sort, как своего рода Код Kata, я наткнулся на разницу между IEnumerable и List, мне нужна помощь, чтобы понять.

Здесь MergeSort

public class MergeSort<T>
{
    public IEnumerable<T> Sort(IEnumerable<T> arr)
    {
        if (arr.Count() <= 1) return arr;

        int middle = arr.Count() / 2;
        var left = arr.Take(middle).ToList();
        var right = arr.Skip(middle).ToList();
        return Merge(Sort(left), Sort(right));
    }

    private static IEnumerable<T> Merge(IEnumerable<T> left, IEnumerable<T> right)
    {
        var arrSorted = new List<T>();

        while (left.Count() > 0 && right.Count() > 0)
        {
            if (Comparer<T>.Default.Compare(left.First(), right.First()) < 0)
            {
                arrSorted.Add(left.First());
                left=left.Skip(1);
            }
            else
            {
                arrSorted.Add(right.First());  
                right=right.Skip(1);  
            }
        }

        return arrSorted.Concat(left).Concat(right);
    }
}

Если я удаляю .ToList() в переменных left и right, он не может правильно сортироваться. Вы понимаете, почему?

Пример

var ints = new List<int> { 5, 8, 2, 1, 7 };
var mergeSortInt = new MergeSort<int>();
var sortedInts = mergeSortInt.Sort(ints);

С .ToList()

    [0]: 1
    [1]: 2
    [2]: 5
    [3]: 7
    [4]: 8

Без .ToList()

    [0]: 1
    [1]: 2
    [2]: 5
    [3]: 7
    [4]: 2

Edit

Это был мой тупой тест, который заставил меня.

Я протестировал его так:

var sortedInts = mergeSortInt.Sort(ints);
ints.Sort();
if (Enumerable.SequenceEqual(ints, sortedInts)) Console.WriteLine("ints sorts ok");

просто изменив первую строку на

var sortedInts = mergeSortInt.Sort(ints).ToList();

устраняет проблему (и ленивую оценку).

EDIT 2010-12-29

Мне показалось, что я выясню, как ленивая оценка беспорядок здесь, но я просто не понимаю.

Удалите .ToList() в методе сортировки выше, как это показано

var left = arr.Take(middle);
var right = arr.Skip(middle);

попробуйте это

var ints = new List<int> { 5, 8, 2 };
var mergeSortInt = new MergeSort<int>();
var sortedInts = mergeSortInt.Sort(ints);
ints.Sort();
if (Enumerable.SequenceEqual(ints, sortedInts)) Console.WriteLine("ints sorts ok");

При отладке Вы можете видеть, что до ints.Sort() a sortedInts.ToList() возвращает

[0]: 2
[1]: 5
[2]: 8

но после ints.Sort() он возвращает

[0]: 2
[1]: 5
[2]: 5

Что на самом деле происходит здесь?

Ответы

Ответ 1

Ваша функция верна - если вы проверите результат Merge, вы увидите, что результат отсортирован (пример). < ш > Так где же проблема? Как вы подозревали, вы неправильно проверяете это - , когда вы вызываете Sort в свой первоначальный список, вы меняете все коллекции, которые происходят от него!
Вот фрагмент, который показывает, что вы сделали:

List<int> numbers = new List<int> {5, 4};
IEnumerable<int> first = numbers.Take(1);
Console.WriteLine(first.Single()); //prints 5
numbers.Sort();
Console.WriteLine(first.Single()); //prints 4!

Все созданные вами коллекции в основном такие же, как first - в некотором смысле, они ленивые указатели на позиции в ints. Очевидно, когда вы вызываете ToList, проблема устраняется.

Ваше дело более сложное. Ваш Sort является частью ленивым, точно так же, как вы предполагаете: сначала вы создаете список (arrSorted) и добавляете к нему целые числа. Эта часть не ленива, и по этой причине вы видите, что отсортированы первые несколько элементов. Затем вы добавляете остальные элементы, но Concat ленив. Теперь рекурсия включает в себя еще больше: в большинстве случаев большинство элементов на вашем IEnumerable нетерпеливы - вы создаете списки из левого и правого, которые также состоят из преимущественно нетерпеливого + ленивого хвоста. Вы получаете отсортированный List<int>, лениво континурованный ленивым указателем, который должен быть только последним элементом (ранее были объединены другие элементы).
Здесь график вызовов ваших функций - красный обозначил ленивую коллекцию, черный - действительное число:

alt text

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

Результат в основном хорош, но последний элемент по-прежнему указывает на исходный список:

alt text

Последний пример: подумайте, что вы меняете все элементы в исходном списке. Как вы можете видеть, большинство элементов в отсортированной коллекции остаются неизменными, но последнее является ленивым и указывает на новое значение:

var ints = new List<int> { 3,2,1 };
var mergeSortInt = new MergeSort<int>();
var sortedInts = mergeSortInt.Sort(ints);
// sortedInts is { 1, 2, 3 }
for(int i=0;i<ints.Count;i++) ints[i] = -i * 10;
// sortedInts is { 1, 2, 0 }

Здесь тот же пример на Ideone: http://ideone.com/FQVR7

Ответ 2

Невозможно воспроизвести - я только что пробовал это, и он работает абсолютно нормально. Очевидно, что это довольно неэффективно по-разному, но удаление вызовов ToList не привело к сбою.

Здесь мой тестовый код с вашим MergeSort кодом как есть, но без вызовов ToList():

using System;
using System.Collections.Generic;

public static class Extensions
{
    public static void Dump<T>(this IEnumerable<T> items, string name)
    {
        Console.WriteLine(name);
        foreach (T item in items)
        {
            Console.Write(item);
            Console.Write(" ");
        }
        Console.WriteLine();
    }
}

class Test
{    
    static void Main()
    {
        var ints = new List<int> { 5, 8, 2, 1, 7 };
        var mergeSortInt = new MergeSort<int>();
        var sortedInts = mergeSortInt.Sort(ints);
        sortedInts.Dump("Sorted");
    }
}

Вывод:

Sorted
1 2 5 7 8

Возможно, проблема заключалась в том, как вы тестировали свой код?

Ответ 3

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

Ответ 4

Проблема заключается в том, что вы сортируете левый правый, чем правую, и объединяетесь в одну последовательность. Это не означает, что вы получаете полностью отсортированную последовательность.

Сначала вы должны объединиться, и вам придется сортировать:

public IEnumerable<T> Sort(IEnumerable<T> arr)
{
    if (arr.Count() <= 1) return arr;

    int middle = arr.Count() / 2;
    var left = arr.Take(middle).ToList();
    var right = arr.Skip(middle).ToList();

    // first merge and than sort
    return Sort(Merge(left, right));
}