在实现这个通用merge sort时,作为一种Code 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
修改
这是我的愚蠢考验让我。
我测试了这样:
var sortedInts = mergeSortInt.Sort(ints);
ints.Sort();
if (Enumerable.SequenceEqual(ints, sortedInts)) Console.WriteLine("ints sorts ok");
只需将第一行更改为
var sortedInts = mergeSortInt.Sort(ints).ToList();
删除问题(以及延迟评估)。
编辑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()
之前看到sortedInts.ToList()
返回
[0]: 2
[1]: 5
[2]: 8
但在ints.Sort()
之后返回
[0]: 2
[1]: 5
[2]: 5
这里到底发生了什么?
答案 0 :(得分:8)
您的功能是正确的 - 如果您检查Merge
的结果,您会看到结果已排序(example)。
那么问题出在哪里?正如您所怀疑的那样,您正在测试它是错误的 - 当您在原始列表上调用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>
,懒惰地连接到一个懒惰的指针,它应该是只是最后一个元素(其他元素之前被合并)。
这是你的函数的调用图 - 红色表示一个懒惰的集合,黑色表示一个实数:
当您更改列表时,新列表基本上是完整的,但最后一个元素是惰性的,并指向原始列表中最大元素的位置。
结果大部分都很好,但最后一个元素仍然指向原始列表:
最后一个例子:考虑您正在更改原始列表中的所有元素。如您所见,已排序集合中的大多数元素保持不变,但最后一个是惰性并指向新值:
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
答案 1 :(得分:6)
无法重现 - 我刚试过这个,它的效果绝对正常。显然,它以各种方式效率相当低,但删除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
问题可能是您测试代码的方式?
答案 2 :(得分:2)
我在列表和没有列表的情况下运行它并且它有效 无论如何,合并排序的优势之一是它能够使用O(1)空间复杂度就地排序,这种实现不会受益。
答案 3 :(得分:0)
问题是你排左右边而不是右边合并到一个序列。这并不意味着你得到一个完全排序的序列。
首先你需要合并,而不是必须排序:
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));
}