在检查LINQ的能力时,我编写了简单的QuickSort实现和
很高兴最终快速排序功能适合一行。
但是我注意到这个“一行”功能的性能与我原来的“直接”版本有很大不同。
这是在循环中调用快速排序函数10次的代码:
var r = new Random(DateTime.Now.Millisecond);
Stopwatch watch = new Stopwatch();
for (int i = 0; i < 10; i++)
{
watch.Reset();
var randomA = new int[100].Select(x => r.Next(100)).ToList();
watch.Start();
var sorted = QuickSort<int>(randomA);
watch.Stop();
Console.WriteLine("Duration: {0} ms", watch.ElapsedMilliseconds);
}
Console.ReadLine();
QuickSort的两个实现功能与输出结果:
简易版:
IEnumerable<T> QuickSort<T>(IEnumerable<T> a) where T : IComparable<T>
{
if (a.Count() <= 1) return a;
var pivot = a.First();
IEnumerable<T> lesser = a.Skip(1).Where(x => x.CompareTo(pivot) < 0);
IEnumerable<T> bigger = a.Skip(1).Where(x => x.CompareTo(pivot) >= 0);
return QuickSort(lesser).Concat(new T[] { pivot }).Concat(QuickSort(bigger));
}
输出
持续时间:22毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:3毫秒
持续时间:2毫秒
持续时间:2毫秒
持续时间:3毫秒
持续时间:3毫秒
One Line实施:
IEnumerable<T> QuickSort<T>(IEnumerable<T> a) where T : IComparable<T>
{
return a.Count() <= 1 ? a :
QuickSort(a.Skip(1).Where(i => i.CompareTo(a.First()) < 0)).
Concat(new T[] { a.First() }).
Concat(QuickSort(a.Skip(1).Where(i => i.CompareTo(a.First()) >= 0)));
}
输出
持续时间:24154毫秒
持续时间:407毫秒
持续时间:2281毫秒
持续时间:2420毫秒
持续时间:919毫秒
持续时间:48777毫秒
持续时间:4615毫秒
持续时间:3115毫秒
持续时间:1311毫秒
持续时间:1631毫秒
为什么性能会有这么大的差异?
答案 0 :(得分:5)
原因是你每次都在重新计算a.First()
- 如果你再次考虑这个因素:
public static IEnumerable<T> QuickSort<T>(IEnumerable<T> a)
where T : IComparable<T>
{
if (a.Count() <= 1) return a;
var pivot = a.First();
return QuickSort(a.Skip(1).Where(i => i.CompareTo(pivot) < 0))
.Concat(new T[] { pivot })
.Concat(QuickSort(a.Skip(1).Where(i => i.CompareTo(pivot) >= 0)));
}
然后表现相同。
为了澄清一个实验 - 使用一个拦截器(一个平凡的,只是为了这个实验,不要在家里使用它,这是错误的)First()
的实现我们可以看到实际上多少次{{ 1}}被称为。
First()
这表明在“一个班轮”中public static class TestHelper
{
public static int firstCalledCounter = 0;
public static TSource First<TSource>(this IEnumerable<TSource> source)
{
firstCalledCounter++;
Console.WriteLine("First called " + firstCalledCounter);
IList<TSource> list = source as IList<TSource>;
if (list != null)
{
if (list.Count > 0)
{
return list[0];
}
else return default(TSource);
}
else return default(TSource);
}
}
被称为 236376 次,而在我们将a.First()
分解出去的版本中,它被称为 98 < / strong>次。这解释了性能差异。
答案 1 :(得分:2)
这两个例子的主要罪犯都是这样的(注意这不回答问题,因为改变这个操作会对这两个例子产生影响):
if (a.Count() <= 1) return a;
在这里,您只是检查是否有一个项目,但是,您正在呼叫Count
。如果IEnumerable<T>
实现确实实现了ICollection<T>
,它将每次都通过列表进行枚举。
由于您以递归方式调用此方法,因此在you don't really have to时最终会遍历序列中的所有项目。
相反,你应该这样做:
if (!a.Skip(1).Any()) return a;
这样,您最多只迭代两个项目(跳过第一个项目,检查是否存在第二个项目),以确定序列中是否至多有一个项目。
请注意,对于较大的N
值,您将遇到与Count
相同的问题;这是一个优化,因为N
的值太小了。