【问题标题】:Any() Time Complexity [duplicate]Any() 时间复杂度 [重复]
【发布时间】:2021-09-20 05:15:57
【问题描述】:

我相信这个问题的答案在这里得到了很好的解释:LINQ Ring: Any() vs Contains() for Huge Collections

但我的问题是针对当前实现的

IEnumerable<T> msgs = null;

/// ...
/// some method calls which returns a long list of messages
/// The return type of the method is IEnumerable<T>
/// List<T> ret = new List<T>();
/// ...
/// return ret
/// ...
if (msgs.Any())
    object= msgs.Last();

msgs 是一个在内存中的集合(IEnumerable)表示。 Any() 在这里如何工作?这个 Any() 方法调用没有条件,不就是 O(1) 吗?还是它仍然查看每个元素?

【问题讨论】:

  • 如果IEnumerableICollection,它会查看计数,如果不是,它将调用IEnumerator MoveNext 并返回结果。这里没有魔法
  • 这里的底层类型是什么?除非您提供具体信息,否则我们无法提供任何具体信息。是List&lt;T&gt; 还是更复杂的数据库查询?
  • The msgs is an in memory collection (IEnumerable) said 具体类型是什么?请分享minimal reproducible example
  • @TheGeneral Any 从不查看 Count 属性。 github.com/microsoft/referencesource/blob/master/System.Core/… Count() 扩展方法尝试并查找 ICollection&lt;T&gt;.CountICollection.Count,但不是 Any()。
  • 如果您执行Console.WriteLine(msgs.GetType()),您会在控制台中看到什么打印?

标签: c# linq


【解决方案1】:

我假设IEnumerable&lt;BaseJournalMessage&gt; msgs 不是数组或列表之类的集合,否则AnyLast 将没有问题(但you have performance issues)。所以这似乎是一个昂贵的 LINQ 查询,它被执行了两次,一次是在 Any,另一次是在 Last

Any需要枚举序列,看是否至少有一个。 Last 需要完全枚举它才能得到最后一个。您可以通过这种方式提高效率:

BaseJournalMessage last = msgs.LastOrDefault();
if (last != null)
    time = last.JournalTime;

解释一下。考虑 msg 是一个数组:

IEnumerable<BaseJournalMessage> msgs = new BaseJournalMessage[0];

这里Any 简单而高效,因为它只需要检查数组中的枚举数是否有一个元素,与其他集合相同。复杂度为 O(1)。

现在考虑这是一个复杂的查询,就像您的情况一样。这里Any 的复杂度显然不是 O(1)。:

 IEnumerable<BaseJournalMessage> msgs = hugeMessageList
    .Where(msg => ComplexMethod(msg) && OtherComplexCondition(msg))
    .OrderBy(msg => msg.SomeProperty);

这不是一个集合,因为您没有附加 ToList/ToArray/ToHashSet。相反,它是一个延迟执行的 LINQ 查询。您将在每次枚举时执行它。这可能是 foreach-loop、AnyLast 调用或枚举它的任何其他方法。有时始终获取当前状态很有用,但通常如果您必须多次访问它,您应该将查询具体化为一个集合。所以追加ToList 一切都很好。

如果您想知道它是否正在执行查询,请查看每个 LINQ 方法中的术语“延迟执行”(例如 WhereSelectOrderBy)。您可以根据需要链接任意数量的延迟执行方法,而无需实际执行查询。但是,如果一个方法包含“强制立即查询评估”(例如 ToList),则查询将被执行(因此请避免在查询中间使用这些方法)。

【讨论】:

  • 不是我的反对意见,而是例如IEnumerable&lt;BaseJournalMessage&gt; msgs = new List&lt;BaseJournalMessage&gt;(); 将在内存中。我认为没有理由假设可枚举由 IQueryable() 支持。
  • @EricJ.:我没说是IQueryable。由于我忘记在我的第一个版本中选择 JournalTime 属性,因此我也对其进行了编辑,也许这就是否决票的原因。如果我说查询它与数据库调用无关。我只是想说这是一个昂贵的 Linq 查询。我已经在答案中解释过了
  • 那么 LastOrDefault 会消除性能问题吗?但似乎 LastOrDefault 也是 O(N),对吗?此外,我刚刚更新了问题中的一些代码 cmets:我的问题中的 msgs 是一个 IEnumerable,但是有一个方法调用返回分配给 msgs 的 List
  • @WeilunYuan 是的,如果源不是集合,LastOrDefault 也是 O(n) 是正确的,否则该方法会尝试将其强制转换为 IList,如果成功仅使用索引器,那么它就是O(1)。但是使用 Any AND Last 仍然更有效,即 2 * O(n)
  • 如果 source(msg) 实际上是一个 List ,那么无论是 Ay 还是 Last 都没有性能问题。然后您正在查看代码的错误部分。也许返回列表的方法是问题所在。
【解决方案2】:

Any() 在这里是如何工作的?有 这个 Any() 方法调用没有条件,不就是 O(1) 吗? 还是它仍然查看每个元素?

对于 LinQ-To-Object,在System.Linq.Enumerable 静态类中实现,Any() 的实现只是获取IEnumerator 并调用MoveNext()。如果结果是true,则Any() 返回true 本身。否则返回false。它永远不会进一步迭代。

所以它是一个纯 O(1) 算法。 编辑:我必须纠正自己:时间取决于可枚举的“任何”迭代。我对大 O 表示法以及“O(1)”和“O(n)”的含义有误解。

这是source code(现在可以在 GitHub 上找到源代码):

    public static bool Any<TSource>(this IEnumerable<TSource> source) {
        if (source == null) throw Error.ArgumentNull("source");
        using (IEnumerator<TSource> e = source.GetEnumerator()) {
            if (e.MoveNext()) return true;
        }
        return false;
    }

【讨论】:

  • So it is a pure O(1) algorithm. 过于简单化,因为它不是固定成本,因为它根据调用它的可枚举而变化。如果它是非常复杂的投影的结果,它可能会非常昂贵。
  • 是的,即使使用 Linq-to-objects,e.MoveNext 也可能需要一天时间才能执行。你不能说它是 O(1),因为你对来源一无所知。
  • @TimSchmelter 确定我可以看到它是 O(1)。因为它显然是。来源在 GitHub。如果 O(1) 中的第 1 次迭代需要一天,它仍然是 O(1)。
  • @lidqy 这可能是技术上准确的,这是我的观点。但在讨论 Big O 时,人们感兴趣的是手术的预期成本。当你说它是 O(1) 时,它有可能被解释为不正确的东西。说“Any 的成本是访问第一个元素的成本”会更简单、更清楚。干净,清晰,不会被误解。
  • @lidqy 不,O(1) 并不意味着它需要进行一次操作。这意味着无论来源是什么,它都需要一个恒定的时间。不是这种情况。源可能是一个复杂的 Linq-to-objects 查询,显然不是 O(1),Any 需要执行它。正确的答案是,你不能在不知道来源的情况下对 Any 的复杂性说任何话。这同样适用于其他方法,如 First
猜你喜欢
  • 2016-02-13
  • 2012-08-14
  • 2018-10-23
  • 2017-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-06
相关资源
最近更新 更多