2017-03-16 31 views
5

只是为了好玩我建在C#中的快速排序实现使用LINQ:快速排序LINQ的性能优势通过T []对的IEnumerable <T>

public static IEnumerable<T> quicksort<T>(IEnumerable<T> input) where T : IComparable<T>{ 
    if (input.Count() <= 1) return input; 
    var pivot = input.FirstOrDefault(); 
    var lesser = quicksort(input.Skip(1).Where(i => i.CompareTo(pivot) <= 0)); 
    var greater = quicksort(input.Where(i => i.CompareTo(pivot) > 0)); 
    return lesser.Append(pivot).Concat(greater); 
} 

它在大约13秒10000个排序随机整数。

将其更改为使用int []而不是List结果的性能会提高约700倍!对相同的10000个随机整数只需要21ms。

public static T[] quicksortArray<T>(T[] input) where T : IComparable<T>{ 
    if (input.Count() <= 1) return input; 
    var pivot = input.FirstOrDefault(); 
    var lesser = quicksortArray(input.Skip(1).Where(i => i.CompareTo(pivot) <= 0).ToArray()); 
    var greater = quicksortArray(input.Where(i => i.CompareTo(pivot) > 0).ToArray()); 
    return lesser.Append(pivot).Concat(greater).ToArray(); 
} 

只看我会认为这将有更坏 性能的代码。我认为.ToArray()会在内存中创建一个额外的数组,并在那里复制所有整数。 我认为传递数组与传递列表应该大致相同。

那么这个巨大的性能差异从哪里来?

+1

https://ideone.com/E5ASv7,https://ideone.com/DAUfmB – Ryan

+0

将'quicksortArray'与'Array进行比较后,您可能会感到有些惊讶。排序'也 – Slai

+0

@Slai我知道这是不高效的,不会使用它的任何事情:-) – marv51

回答

6

这就是为什么你应该小心迭代IEnumerable多次。

您第一次打电话给quicksort时,您通过List。它再次调用quicksort两次,在每种情况下,您传递的IEnumerable代表一个查询,它将跳过第一个项目,然后对每个项目执行比较。然后,您将该查询传递给两个更多quicksort的实例,但使查询不仅会跳过第一个项目并比较其后的每个项目,但会跳过该查询结果的第一个项目,然后比较每个项目之后的东西。这意味着当你最终达到一个值时,你有一个代表log(n)跳过的查询,并将序列中的每个项目与值log(n)次进行比较。

在你你不传递秒执行查询quicksort,你正在评估这些查询自己的价值观和传递来操作,然后可以使用这些值的两倍,而不是不断地执行一个非常复杂的查询多次。

+0

这是一大堆跳过。 –

+1

谢谢,在第一个示例中添加一个显式的.toList()修复了这种性能差异。我是在(错误的)假设下.toList()将隐式地在第一次使用查询结果时完成。 – marv51

+0

@ marv51当然值得注意的是,您不断复制数据的担忧是值得的。基本上你的解决方案将会是O(n^2 * log(n)),而不是一个好的解决方案,它会是O(n * log(n)),这只是你第一个样本的代码是O((2^n)* log(n))这只是*疯狂*坏,使复制的仍然不好的版本看起来相当惊人。你不应该*使用任何一种。 – Servy

4

关于linq查询的事情是懒惰的,他们不会被评估,直到你调用像ToArrayToList这样的方法。在例如你的第一个代码,这个查询:

input.Skip(1).Where(i => i.CompareTo(pivot)) 

将至少两次每次调用quicksort,一旦Count(),一次用于FirstOrDefault时间进行评估。这意味着过滤操作将重复为每个呼叫一遍又一遍。另一方面,如果您使用ToArray,由于您已经在数组中过滤了元素,因此Where将不会每次都执行,只有在您拨打ToArray后才会执行,就是这样。这是代码之间的差异,基于此你可以为其他部分做数学运算。

+2

问题不是它做了两次工作;这会使代码慢两倍。问题在于,它每工作一次,就会增加一倍*这是工作量的2^log(n)倍。这是一个很多*更多的工作。 – Servy

+0

@Servy:“每次调用'quicksort'时两次”似乎很准确地覆盖了这一点。 – Ryan

+0

@Servy是的,这是正确的。我并不是说它会使工作翻倍,但忘记提及在你的答案中对每个电话的查询结合起来,所以我就这个问题竖起了大拇指 –