LINQ方法的运行时复杂性(Big-O)有什么保证?

时间:2010-05-09 22:29:06

标签: c# .net linq algorithm complexity-theory

我最近开始使用LINQ了,我还没有真正看到任何LINQ方法的运行时复杂性。显然,这里有很多因素在起作用,所以让我们将讨论局限于普通的IEnumerable LINQ-to-Objects提供者。此外,假设任何作为选择器/ mutator /等传入的Func是廉价的O(1)操作。

很明显,所有单遍操作(SelectWhereCountTake/SkipAny/All等等都是O (n),因为他们只需要走一次序列;虽然这甚至会受到懒惰的影响。

对于更复杂的行动来说,事情变得更加模糊;类似于集合的运算符(UnionDistinctExcept等)默认使用GetHashCode(afaik),因此假设它们正在使用似乎是合理的内部的哈希表,通常也可以使这些操作成为O(n)。那些使用IEqualityComparer的版本呢?

OrderBy需要排序,所以我们很可能会看O(n log n)。如果它已经排序了怎么办?如果我说OrderBy().ThenBy()并为两者提供相同的密钥怎么样?

我可以使用排序或散列来查看GroupBy(和Join)。这是什么?

Contains将是List上的O(n),但是HashSet上的O(1) - LINQ会检查基础容器以查看它是否可以加快速度?

真正的问题 - 到目前为止,我一直坚信运营是高效的。但是,我可以依靠吗?例如,STL容器清楚地指定了每个操作的复杂性。 .NET库规范中的LINQ性能是否有类似的保证?

更多问题(回应评论):
没有真正想过开销,但我没想到会有很多简单的Linq-to-Objects。 CodingHorror帖子讨论的是Linq-to-SQL,在那里我可以理解解析查询并使SQL增加成本 - 对象提供者也有类似的成本吗?如果是这样,如果您使用声明性或功能性语法,它会有所不同吗?

5 个答案:

答案 0 :(得分:104)

保证非常非常少,但有一些优化:

  • 使用索引访问的扩展方法(例如ElementAtSkipLastLastOrDefault将检查基础类型是否实现IList<T>,以便您获得O(1)访问而不是O(N)。

  • Count方法检查ICollection实现,以便此操作为O(1)而不是O(N)。

  • DistinctGroupBy Join,我也相信集合汇总方法(UnionIntersectExcept )使用散列,因此它们应该接近O(N)而不是O(N²)。

  • Contains检查ICollection实现,如果基础集合也是O(1),可能为O(1),例如HashSet<T>,但这取决于实际的数据结构,并不能保证。散列集覆盖Contains方法,这就是它们为O(1)的原因。

  • OrderBy方法使用稳定的快速排序,因此它们的平均值为O(N log N)。

我认为这涵盖了大多数(如果不是全部)内置扩展方法。实际保证很少; Linq本身将尝试利用高效的数据结构,但编写可能效率低下的代码并不是免费的。

答案 1 :(得分:8)

你可以真正依赖的是,Enumerable方法是针对一般情况编写的,不会使用朴素算法。可能有第三方的东西(博客等)描述了实际使用的算法,但从STL算法的意义上来说,这些并不是官方的或保证的。

为了说明,这里是来自System.Core的Enumerable.Count的反映源代码(由ILSpy提供):

// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    checked
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        ICollection<TSource> collection = source as ICollection<TSource>;
        if (collection != null)
        {
            return collection.Count;
        }
        ICollection collection2 = source as ICollection;
        if (collection2 != null)
        {
            return collection2.Count;
        }
        int num = 0;
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                num++;
            }
        }
        return num;
    }
}

正如您所看到的,它需要付出一些努力来避免简单地枚举每个元素的天真解决方案。

答案 2 :(得分:7)

我很早就知道如果枚举是.Count().Count会返回IList

但我总是对Set操作的运行时复杂性感到有点厌倦:.Intersect().Except().Union()

这是.Intersect()的反编译BCL(.NET 4.0 / 4.5)实现(我的评论):

private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)                    // O(M)
    set.Add(source);                                    // O(1)

  foreach (TSource source in first)                     // O(N)
  {
    if (set.Remove(source))                             // O(1)
      yield return source;
  }
}

结论:

  • 表现为O(M + N)
  • 集合已设置时, (它可能不一定是直截了当的,因为使用的IEqualityComparer<T>也需要匹配。)

为了完整性,以下是.Union().Except()的实现。

剧透警报:他们也有 O(N + M)的复杂性。

private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
  foreach (TSource source in second)
  {
    if (set.Add(source))
      yield return source;
  }
}


private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)
    set.Add(source);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
}

答案 3 :(得分:3)

我刚刚打破了反射器,他们会在调用Contains时检查基础类型。

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> is2 = source as ICollection<TSource>;
    if (is2 != null)
    {
        return is2.Contains(value);
    }
    return source.Contains<TSource>(value, null);
}

答案 4 :(得分:3)

正确的答案是“它取决于”。它取决于底层IEnumerable的类型。我知道对于某些集合(如实现ICollection或IList的集合),使用了特殊的代码路径,但实际的实现并不能保证做任何特殊的事情。例如,我知道ElementAt()具有可索引集合的特殊情况,类似于Count()。但总的来说,你应该假设O(n)性能最差的情况。

一般来说,我认为你不会找到你想要的那种性能保证,但是如果你遇到linq运算符的特定性能问题,你总是可以为你的特定集合重新实现它。此外,还有许多博客和可扩展性项目将Linq扩展到Objects以添加这些性能保证。检查Indexed LINQ扩展并添加到运算符集以获得更多性能优势。