【问题标题】:Why is List(T).Clear O(N)?为什么是 List(T).Clear O(N)?
【发布时间】:2011-01-25 21:15:34
【问题描述】:

根据the MSDN documentation on the List<T>.Clear method:

这个方法是一个 O(n) 操作, 其中 n 是计数。

为什么是 O(n)?我问是因为我假设清除List<T> 可以简单地通过在内部分配一个新的T[] 数组来完成。没有其他类可以引用这个数组,所以我看不出这种方法的危害。

现在,也许这是一个愚蠢的问题...分配T[] 数组本身是O(n) 吗?出于某种原因,我不会这么认为;但也许是(我现在缺乏 C.S. 学位吗?)。如果是这样,我想这可以解释它,因为根据上面引用的相同文档,列表的容量保持不变,这意味着需要构造一个大小相等的数组。

(再说一次,这似乎不是正确的解释,因为文档应该说“n 是 Capacity”——不是 @ 987654327@*).

我只是怀疑这个方法不是分配一个新数组,而是将当前数组的所有元素清零;我很想知道为什么会这样。

*Hans Passant 在对LukeH's answer 的评论中指出文档是正确的。 Clear 只清零已在List<T> 中设置的元素;它不需要将所有元素“重新归零”。

【问题讨论】:

    标签: .net list performance big-o


    【解决方案1】:

    据我所知,当前的实现只是在其内部的T[] 数组上调用Array.Clear,而Array.Clear 是一个O(n) 过程。 (正如 Hans 在 cmets 中指出的那样,MSDN 文档是正确的,在这种情况下 n 是列表的 Count,而不是 Capacity。)

    但是,即使它只是在内部分配了一个新的 T[] 数组,这仍然是一个 O(n) 过程,因为分配一个大小为 n 的数组涉及将所有 n 元素初始化为零/空/默认状态。

    当然,没有什么可以阻止某种内部诡计,其中数组可以用长度 0 或 42 或其他任何值初始化,然后根据需要再次即时自动扩展,分摊总体 O(n) 成本.

    【讨论】:

    • 哈,对了。我完全愚蠢地忽略了这个事实(我确实知道......或者无论如何,我认为我知道)!
    • @LukeH, Dan Tao:但它不必分配一个新的长度为n的数组;它可以只分配一个空数组。
    • @Luke:但是 wait:那么由于 Capacity 保持不变(根据文档),不应该是 O(n) 其中 n 是 容量?
    • 它将 this._size 传递给 length 参数的 Array.Clear()。
    • @Dan:准确度不是 O(n) 的本意。 _size 确实清除了它,但即使是 Clear(Capacity) 仍然是 O(Count)。 O(n*C) == O(n) 即使 C = 10^10
    【解决方案2】:

    因为List<T>.Clear() 的实现在列表的backign 数组上调用Array.Clear(),并且根据documentation,该方法将该数组的所有元素设置为null。

    我猜想清除现有数组而不是创建新数组的原因是 .NET 团队确定清除现有数组而不是分配新数组更有效。分配一个新数组也需要时间/内存,因此它是一种权衡,试图针对常见的使用场景进行优化。

    【讨论】:

    • 您能否就List<T>.Clear 的实现调用Array.Clear 提供任何想法?节省内存?
    • 似乎只有一半的答案。为什么它调用 Array.Clear() ?即在某些情况下这是有用的吗?
    • @Henk @Dan - 它不需要任何新的分配(假设您打算在调用 Clear 时重新使用该列表),它会释放当前对象以进行垃圾回收
    • @Marc:但无论如何这些对象都会被释放以进行垃圾收集。正如 OP 指出的那样,不会有其他对旧数组的引用,所以它可以被 GC 处理得很好。因此,除非有更多内容,否则归结为执行 O(n) 操作以避免分配。
    • 我不得不给它一些时间 - 正如@LukeH 指出的那样,无论如何你都必须将新的归零,而 Clear() 用于你想要保持容量的时候。并且内部数组可能已经升级到 gen 1+,然后保留它比新的便宜。在第 0 代中,成本将大致相同。
    【解决方案3】:

    List<T>.Clear() 调用 Array.Clear() 如下所示:

    public virtual void Clear()
    {
        if (this._size > 0)
        {
            Array.Clear(this._items, 0, this._size);
            this._size = 0;
        }
        this._version++;
    }
    

    反过来,Array.Clear() 会调用一个外部函数,该函数会将数组的元素归零,即 O(n):

    [MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    public static extern void Clear(Array array, int index, int length);
    

    【讨论】:

    • Array.Clear 不初始化新数组。它将元素归零。
    【解决方案4】:

    我从另一个方面提出了这个问题,这里没有回答。我在想它应该是 O(1),因为不需要分配,只需更改 Count 标志/指针,因此将每个元素(不再可访问)重置为默认值看起来很浪费,我想知道为什么会这样做。

    答案当然是这适用于 ValueTypes/structs,但不适用于类。后者应该被取消引用,即使是从 List 的私有数组中,否则它们将没有资格进行垃圾回收。

    注意 n 是 Count,而不是 Capacity,因为 List 始终保持着一个约定,即无论哪些元素不可访问,也没有引用任何值。因此 Count 和 Capacity 之间的元素,要么从未设置过,要么在移除时已重新设置。

    但是对于结构来说,这种重置是浪费的,在这种情况下,Clear() 可能是 O(1)。因此,标准库中有空间(现在微软正在提高 C# 的性能)用于 IList,其中 T:struct 具有 List 的所有功能以及 O(1) Clear() 实现。

    关于分配新私有数组的 OP 建议:首先,您已经有了该选项,如果这是您想要的,只需新建 List 本身。其次,正如 LukeH 所说,分配是 O(n),因为它意味着初始化/重置。第三,将遍历已分配列表/数组的 O(n) 与新分配(和取消分配)所需的时间进行比较是非常棘手的。

    【讨论】:

      【解决方案5】:

      List<T> 列表包含一些对象。分配一个新的List<T> 并不会摆脱旧列表中的对象,因此它不是clear 操作。

      clear 操作对列表进行一次线性遍历,将所有元素一一清除。由于有n 元素,这需要O(n) 时间。

      分配T[] 取决于是否指定了大小。如果指定了大小,则必须为每个元素留出内存,或者至少为该元素留出每个指针。因此这需要O(n)。但是,如果我们只是为T[] 初始化一个指针,那将花费O(1) 时间。

      附: CS学位并不自动意味着您知道(或记住)这些东西……可悲,但是,真的。没有 CS 学位完全没有坏处。

      【讨论】:

      • “分配一个新的 List 并没有摆脱对象......” - 是的。通过删除旧列表,元素的引用会丢失。
      • 如果没有其他对这些对象的引用,那么它们将被 GC'd。如果有 引用,那就另当别论了。但无论哪种方式分配一个新数组都会达到与将元素清零相同的效果。
      猜你喜欢
      • 2012-04-08
      • 1970-01-01
      • 1970-01-01
      • 2011-12-10
      • 1970-01-01
      • 2016-06-07
      • 2020-07-09
      • 2020-10-19
      • 1970-01-01
      相关资源
      最近更新 更多