【问题标题】:Why isn't System...Stack<T> implemented as a Linked List?为什么 System...Stack<T> 不作为链表实现?
【发布时间】:2009-01-04 03:59:05
【问题描述】:

我在查看我的代码时发现了一些我编写的扩展方法,用于从 System.Collections.Generic.Stack 中删除项目。我很好奇,所以我查看了 Stack with Reflector 的源代码,我可以看到他们将它实现为数组而不是链表,我只是想知道为什么?使用链表就无需调整内部数组的大小...

这是我的扩展,欢迎任何批评或建议。谢谢。

public static Stack<T> Remove<T>(this Stack<T> stack, T item)
{
    Stack<T> newStack = new Stack<T>();

    EqualityComparer<T> eqc = EqualityComparer<T>.Default;

    foreach( T newItem in stack.Reverse() )
    {
        if( !eqc.Equals(newItem, item) )
        {
            newStack.Push(newItem);
        }
    }

    return newStack;
}
/// <summary>
/// Returns a new Stack{T} with one or more items removed, based on the given predicate.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="stack"></param>
/// <param name="fnRemove"></param>
/// <returns>The new stack.</returns>
/// <remarks>
/// We have to turn tricks in order to save the LIFO order of the pool
/// since there is no built-in remove method since they implement a Stack internally
/// as an array instead of a linked list. Maybe they have a good reason, I don't know...
/// 
/// So, to fix this I'm just using a LINQ extension method to enumerate in reverse.
/// </remarks>
public static Stack<T> RemoveWhere<T>(this Stack<T> stack, Predicate<T> fnRemove)
{
    Stack<T> newStack = new Stack<T>();

    foreach( T newItem in stack.Reverse() )
    {
        /// Check using the caller's method.
        if( fnRemove(newItem) )
        {
            /// It's not allowed in the new stack.
            continue;
        }
        newStack.Push(newItem);
    }

    return newStack;
}

【问题讨论】:

  • 一个更有趣的问题是为什么 List 也用数组来实现。插入和删除是 O(n)。
  • nobugs: get(int) and remove(int) are O(n) is in LinkedList as well.
  • 旁注:要消除continue 的使用,显然可以反转 if 语句。我没有反对继续,但你在这里不需要它。
  • @BenKnoble 谢谢!你当然是对的。在这个小例子中,它似乎不合逻辑,但总的来说,在很久以前阅读 Pragmatic Programmer 之后,我总是倾向于 early-exits 而不是 if-statements。总的来说,对我的代码的影响是更少的嵌套,更容易阅读代码和 cmets。所以这就是这里发生的事情 - 只是我的习惯。今天(在我晚年)我会少用括号。

标签: c# .net data-structures


【解决方案1】:

后进先出队列(堆栈)通常对数组最有效,因为您将项目推送到数组的末端并从数组的同一末端拉出项目。所以数组运行良好,没有链表的内存和分配开销。不需要创建和垃圾收集列表项包装对象,从而抵消了创建和调整数组大小的成本。

【讨论】:

  • 是的,但它是在 Java 中并且很久以前。
  • 顺便说一句,您不需要显式垃圾收集包装器对象。这将在某个未确定的时间自动发生。
  • @Software Monkey:是的,但充其量只有在您关闭应用程序时才会发生。
【解决方案2】:

考虑具有 1,000,000 个项目的 Stack&lt;byte&gt; 的大小。

  • 对于数组,大小 ~= 1,000,000 字节。通常更多是由于备用容量,但可能不会超过两倍。
  • 对于链表,每个条目都需要自己的对象、数据本身和至少一个引用(对于单链表,这可能是堆栈所需的全部内容)。填充到 4 个字节后,总大小可能约为 16,000,000 字节。哎哟。

再加上引用的局部性和减少的 GC 压力,我认为使用数组作为堆栈、队列和双端队列的底层数据结构非常有意义——最后两个将数组用作循环缓冲区,当然。唯一明显的缺点是浪费了“备用容量”,并且当容量耗尽时需要复制所有内容。

【讨论】:

  • 实际上,在 32 位系统上,链表的典型对象大小为 16(基础)+ 上一个/下一个链接(8)+ 有效负载参考(4),而对于 64 位系统,这些大小是两倍系统。所以现实更糟。我已经通过分析器测量了这些。
【解决方案3】:

当您运行基准测试时,由于局部性,您最终几乎总是会在 Array 中看到巨大的性能优势。 ArrayList 将所有内容放入一个数组中,该数组存储在一个大的连续内存块中。链表通过链接存储它们的数据,因此最终会分配大量内存并在内存中跳转。

【讨论】:

  • 这将是一个大问题。
  • 同意,但我确信有关该主题的研究会指导框架库开发人员在编写此类内容时,它说数组最终比链接列表更快地进行基准测试。跨度>
  • 那条笼统的说法“数组最终比链接列表更快地进行基准测试”是有缺陷的。在链表和数组的中间插入一个元素怎么样?我几乎不认为数组会更快!
  • @Mitch:是的,除了这是一个堆栈(而不是列表),它是一个后进先出队列;物品被推到末端并从末端拉出 - 堆栈(通常)不会插入中间。决定实现选择的是堆栈行为。
  • 是的...抱歉没有澄清。我们在这里谈论堆栈,我认为这是理所当然的。虽然,您知道,如果我们将其限制为将单个元素插入到 2 元素列表的中间,我认为数组可能能够让链表物有所值。
猜你喜欢
  • 2011-06-14
  • 2011-03-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-04
  • 2013-03-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多