C# 为什么从列表中删除最后一个元素的复杂性对于数组是O(1)和O(n)?

C# 为什么从列表中删除最后一个元素的复杂性对于数组是O(1)和O(n)?,c#,algorithm,C#,Algorithm,这个问题听起来很原始,但它是关于从列表中删除元素(最后)的(最佳情况)复杂性的(生动的)讨论的结果 在发布此问题之前,我考虑了以下几点: 删除列表末尾的元素的复杂性为O(1) 从静态数组中删除任何元素(即使是最后一个)的复杂性为O(n),因为它需要一个新数组 现在请注意以上两个参数,以及我们在List实现的背景中也有一个数组这一事实 那么为什么List操作会导致O(1),而当这两个操作都涉及到重新创建一个数组时,数组会导致O(n)呢?还是我在这里遗漏了什么?谢谢 更新:只是澄清一下,这个问题

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

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

  • 删除
    列表末尾的元素的复杂性为O(1)
  • 从静态数组中删除任何元素(即使是最后一个)的复杂性为O(n),因为它需要一个新数组
现在请注意以上两个参数,以及我们在
List
实现的背景中也有一个数组这一事实

那么为什么
List
操作会导致O(1),而当这两个操作都涉及到重新创建一个数组时,数组会导致O(n)呢?还是我在这里遗漏了什么?谢谢


更新:只是澄清一下,这个问题的主要焦点不是为什么最后一个元素移除的复杂性是O(1),而是为什么数组的复杂性是O(n),而
列表的复杂性是O(1)。标题已更新以反映更改。多亏了答案(它解决了这个问题)和其他注释。`

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

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


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

首先,您的问题的基础是不正确的:按索引从
列表中删除项是
O(n)
,非
O(1)
。让我解释一下


当您按索引(即使用
List.RemoveAt
)从
列表中删除某个项目时,您需要将其后面显示的所有项目在列表中向前复制一个空格,以缩小刚才留下的间隙。你可以很清楚地看到:

public void RemoveAt(int索引)
{
如果((uint)索引>=(uint)\u大小)
{
ThrowHelper.throwargumentotofrange_IndexException();
}
_大小--;
如果(索引大小)
{
复制(_项,索引+1,_项,索引,_大小-索引);
}
if(RuntimeHelpers.IsReferenceOrContainsReferences())
{
_项目[_size]=默认值!;
}
_版本++;
}
如果删除最后一个元素,则不需要任何成本;如果删除第一个元素,则必须复制所有剩余元素

因此,一般来说,从列表中删除任意元素的成本会随着列表中元素的数量而增加,从而使其成为线性的。事实上,可以这样说:

此方法是一个O(n)操作,其中
n
是(
Count
-
index


这样一来,从列表中移除一个元素和从数组中移除一个元素在成本上就有了一点区别

List
由数组支持:基础数组可以大于
List.Count
返回的值。随着更多项目添加到列表中,在分配新的备份数组和复制所有旧元素之前,备份数组逐渐填满

这意味着您可以拥有一个大于
列表中元素数的后备数组。如果我们从
列表中删除一个元素
,我们就不需要分配一个全新的备份数组

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


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

当您按索引从
列表中删除某个项目时,您需要将该项目后出现的所有项目在列表中向前复制一个空格,以缩小刚才留下的差距。如果删除最后一个元素,则不需要任何成本;如果删除第一个元素,则必须复制所有剩余元素。一般来说,从列表中删除任意元素的成本随列表中元素的数量而增加,使其
O(n)
。这是广州7号,对。“在数组的情况下,它有什么不同?”FailedScientist看到了我的全部answer@FailedScientist数组中没有“删除”。您可以将剩余的n-1个元素复制到大小为n-1的数组中,这需要O(n)个时间,也可以将索引处的元素设置为
null
(或其他一些终端值),执行O(1)操作。如果你像
List
那样使用它的备份数组,并将剩余的元素向前移动,那么你最终会得到数组末尾的空格和一个O(n)算法。副本在一句话中解释说,@canton7详细说明:删除最后一项不会做数组副本谢谢。因此,即使我按索引删除
列表的最后一个元素,它也将是O(n)op
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++;
}