【问题标题】:Why complexity of removing last element from List is O(1) and O(n) for the arrays? [duplicate]为什么从 List 中删除最后一个元素的复杂性对于数组来说是 O(1) 和 O(n)? [复制]
【发布时间】:2021-08-24 18:14:57
【问题描述】:

这个问题可能听起来很原始,但它是在(最后)关于从列表中删除元素的(最佳情况)复杂性的(热烈)讨论的结果。

在发布此问题之前,我考虑了以下几点:

  • 删除List<> 末尾元素的复杂度为 O(1)
  • 从静态数组中删除任何元素(甚至是最后一个元素)的复杂性为 O(n),因为它需要一个新数组。

现在请注意上述两个参数以及我们在 List<> 实现的背景中还有一个数组这一事实。

那么为什么List<> 操作会导致 O(1) 并且当两者都涉及重新创建数组时,数组也会导致 O(n)?或者我在这里错过了什么?谢谢!

更新:澄清一下,这个问题的主要焦点不是为什么最后一个元素删除的复杂性是 O(1),而是为什么它是 O(n) 的数组与 O(1) 的 List<>。标题已更新以反映更改。感谢答案(解决了它)和其他 cmets..`

【问题讨论】:

  • 当您通过索引从List<T> 中删除一个项目时,您需要将列表中出现在它后面的所有项目向前复制一个空格,以缩小您刚刚留下的差距。如果您删除最后一个元素,那是免费的;如果删除第一个元素,则必须复制所有剩余元素。一般来说,从列表中删除任意元素的成本与列表中元素的数量成比例,使其成为O(n)。这个matches the docs
  • @canton7 对。在数组的情况下它有什么不同?
  • @FailedScientist 查看我的完整答案
  • @FailedScientist 没有从数组中“删除”。您可以将剩余的 n-1 个元素复制到大小为 n-1 的数组中,这需要 O(n) 时间,或者您可以将索引处的元素设置为 null(或其他一些终端值),一个 O( 1) 操作。如果您按照List<> 对其后备数组所做的操作,并将剩余的元素向前洗牌,那么您最终会在数组末尾获得空间,并使用 O(n) 算法。
  • 复本用一句话说明,@canton7详细说明:去掉最后一项不做数组复制source.dot.net/#System.Private.CoreLib/List.cs,3d46113cc199059a

标签: c# algorithm


【解决方案1】:

首先,您的问题的基础不正确:按索引从List<T> 中删除项目是O(n),而不是O(1)。让我解释一下……

当您通过索引从List<T> 中删除一个项目时(即使用List<T>.RemoveAt),您需要将列表中出现在它后面的所有项目向前复制一个空格,以缩小您刚刚的差距左边。您可以在the source 中非常清楚地看到这一点:

public void RemoveAt(int index)
{
    if ((uint)index >= (uint)_size)
    {
        ThrowHelper.ThrowArgumentOutOfRange_IndexException();
    }
    _size--;
    if (index < _size)
    {
        Array.Copy(_items, index + 1, _items, index, _size - index);
    }
    if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
    {
        _items[_size] = default!;
    }
    _version++;
}

如果你删除最后一个元素,那是免费的;如果删除第一个元素,则必须复制所有剩余元素。

一般来说,从列表中删除任意元素的成本与列表中元素的数量成比例,使其成为线性的。确实,引用the docs

此方法是一个 O(n) 运算,其中n 是 (Count - index)。


除此之外,从列表中删除元素和从数组中删除元素之间的成本差异很小。

List&lt;T&gt; 由数组支持:底层数组可以大于List&lt;T&gt;.Count 返回的值。随着越来越多的项目被添加到列表中,在分配新的后备数组并复制所有旧元素之前,后备数组逐渐填满。

这意味着您可以拥有一个大于List&lt;T&gt; 中元素数量的后备数组。如果我们从List&lt;T&gt; 中删除一个元素,我们不需要分配一个全新的后备数组。

然而,数组的大小总是必须精确的。如果您有一个包含 3 个元素的数组,则该数组的 Length 必须正好为 3。如果您想将其减少到 2 的 Length,您需要分配一个具有该长度的全新数组。

因此,数组大小的任何更改都意味着您需要分配一个正确大小的新数组,并复制您关心的所有内容。

【讨论】:

  • 谢谢。所以即使我按索引去掉aList&lt;&gt;的最后一个元素,也是O(n)操作?
  • 这取决于你如何定义n。正如我所解释的,最后一个元素的具体情况是 O(1),按索引删除元素的一般成本是 O(n)其中 n 是Count - Index
  • @FailedScientist 算法O(n),在List&lt;&gt;大小为n的第n个元素上执行该算法不需要n操作。
  • @PrestonGuillot 是的,倒数第三段清除了 List 实现中数组与内部数组本身的混淆
【解决方案2】:

在实现类似列表的数据结构中很常见,将底层内存分配为数组,但大小可包含许多列表元素,跟踪最后使用的数组条目的位置。当插入新条目时需要更多空间时,分配通常会增加 50% 左右(控制起始大小和增长策略是一个实现细节)。

因此,从列表尾部删除条目通常只涉及减少用于跟踪最后一个元素位置的变量 - 一个 O(1) 操作,因为没有(取消)分配或移动其他列表元素必填。

然而请注意,Big O Notation 用于根据算法的限制(最坏情况)行为/要求对算法进行分类。分配给列表的底层内存仍然必须进行管理,尽管增长和收缩数组以及移动条目的执行频率应该低于纯数组,但它们是 O(N) 操作。虽然您的问题发表的陈述可能通常是正确的,但它假设了不需要的特定实现细节(特别是删除永远不会导致底层数组缩小)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 2019-07-27
    • 2020-06-25
    • 1970-01-01
    • 2021-01-01
    • 2021-08-24
    • 2016-06-07
    相关资源
    最近更新 更多