【问题标题】:C# List remove from end, really O(n)?C# List 从末尾删除,真的是 O(n)?
【发布时间】:2011-03-22 18:45:06
【问题描述】:

我读过几篇文章说 List.RemoveAt() 在 O(n) 时间内。

如果我这样做:

var myList = new List<int>();

/* Add many ints to the list here. */

// Remove item at end of list:
myList.RemoveAt(myList.Count - 1); // Does this line run in O(n) time?

从列表末尾移除应该是 O(1),因为它只需要减少列表计数。

我是否需要编写自己的类来获得这种行为,或者删除 C# 列表末尾的项目是否已经在 O(1) 时间内执行?

【问题讨论】:

  • 我知道我可以对此进行分析并查看或使用 .NET 反射器。创建这个问题的目的是为我认为重要的问题创建一个可搜索的结果。我在自己的搜索中没有找到答案。

标签: c# big-o


【解决方案1】:

一般List&lt;T&gt;::RemoveAt 为 O(N),因为需要在索引后将元素向上移动到数组中的一个插槽。但是对于从列表末尾删除的特定情况,不需要移位,因此它是 O(1)

【讨论】:

  • 会不会是 O(N) 但N 这里是list.Count - indexRemoved
  • 你在哪里读到的?他们的文档会提出其他建议,无论如何,程序似乎都是 O(n) - 也就是他们没有提出特殊情况..
  • @KevinDepue。这并不是他们正在制作的特例,它本身就是一个特例。类似于遍历一个列表,它总是 O(N),但是当列表只有 1 个元素时,它也是 O(1),因为在这种情况下 N 为 1。没有理由将其视为特例,但无论如何它仍然是特例。
  • @Jared - 导航到 arr[0] 也是 o(1) 吗? arr[arr.length-1] 呢?
  • 欲了解更多信息,请参阅MS' reference source(@JaredPar 和 @Blorgbeard 很重要)
【解决方案2】:

删除最后一项实际上是O(1) 操作,因为只有在这种情况下List 不会移动数组中的下一项。这是Reflector的代码:

this._size--;
if (index < this._size) // this statement is false if index equals last index in List
{
    Array.Copy(this._items, index + 1, this._items, index, this._size - index);
}
this._items[this._size] = default(T);

【讨论】:

  • Array.Copy 只是复制数据,而不是分配数据。
【解决方案3】:

这应该会给你一个想法

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

【讨论】:

    【解决方案4】:

    渐近地说,O(N) 是方法本身的最坏情况时间复杂度,其中 N 是计数。它的表现不会比这更差。

    实际上,它的顺序为 O(N-I)(忽略恒定时间开销),其中 I 是索引。这是可以推断的,因为超出给定索引 I 的所有项目都需要分别移动到列表中它们之前的位置。

    为了直观地看到这一点,如果 N 是 100 并且索引是 99(最后一个元素),那么没有需要“移动”的元素,只是删除了最后一个元素(或者只是在不改变大小的情况下减少了计数数据结构)。

    同样,当 N 为 100,索引为 0(第一个元素)时,必须进行 99 次移位。

    运行以下代码,自己看看:

    int size = 1000000;
    var list1 = new List<int>();
    var list2 = new List<int>();
    
    for (int i = 0; i < size; i++)
    {
        list1.Add(i);
        list2.Add(i);
    }
    
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < size; i++)
    {
        list1.RemoveAt(size-1);
        list1.Add(0);
    }
    sw.Stop();
    Console.WriteLine("Time elapsed: {0}", sw.ElapsedMilliseconds);
    
    sw = Stopwatch.StartNew();
    for (int i = 0; i < size; i++)
    {
        list2.RemoveAt(0);
        list2.Add(0);
    }
    sw.Stop();
    
    Console.WriteLine("Time elapsed: {0}", sw.ElapsedMilliseconds);
    

    【讨论】:

      【解决方案5】:

      在我看来,如果这实际上与您的应用程序相关,那么您可以在比提出问题所需的时间更短的时间内测量它。现在你至少有两个相互矛盾的答案,所以无论如何你都必须测试它。

      我要说明的一点是,除非 MSDN 文档说对于列表末尾的项目, removeAt 是 O(1),否则你不能真正指望它以这种方式工作,它可能会改变在任何给定的 .NET 更新中。就此而言,据您所知,不同类型的行为可能会有所不同。

      如果 List 是要使用的“自然”数据结构,则使用它。如果从列表中删除项目最终成为您分析的热点,那么也许是时候实现您自己的类了。

      【讨论】:

      • 如果您费心阅读我添加到问题中的评论,您会看到我解决了分析或使用反射器的问题。至于你关于 List 类的属性在未来的修订中发生变化的第二点,我认为这是一个有效的观点,但我也认为提出关于当前事态的问题仍然是合理的。特别是因为很长一段时间内不太可能发生变化。毕竟,编程行业变化相对较快,大多数特定于实现的问题只在有限的时间内相关。
      • 如果今天从末尾移除是 O(1),那么它将保持这种状态的可能性为 99.999999%。我不会建议他开始实现他自己的数据结构,因为这种情况不太可能发生变化。关于“自己测量”部分,它不会更快。像 JaredPar 这样值得信赖的人可以在几分钟甚至几秒钟内给出一个很好的答案;-)
      猜你喜欢
      • 2010-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-28
      • 1970-01-01
      • 2012-04-18
      相关资源
      最近更新 更多