【问题标题】:Stack<> implementation in C#C#中的堆栈<>实现
【发布时间】:2010-07-09 17:25:19
【问题描述】:

我最近一直在实现递归目录搜索实现,并且我正在使用堆栈来跟踪路径元素。当我使用 string.Join() 加入路径元素时,我发现它们被颠倒了。调试方法的时候查看栈,发现栈内部数组中元素本身是倒置的,即最近Push()的元素在内部数组的开头,最近的Push() ed 元素位于内部数组的末尾。这似乎很落后,而且非常违反直觉。谁能告诉我为什么微软会以这种方式实现堆栈?

【问题讨论】:

  • 听起来你想要一个队列而不是堆栈。
  • 不,我正在遍历树深度优先,它需要是一个堆栈,但这与问题并不真正相关。
  • 真正的男人使用从最高地址开始向下增长的堆栈。

标签: c# stack


【解决方案1】:

我认为你错了。

Stack&lt;T&gt;.Push 并不是在其内部数组的开头插入一个项目(它没有)。相反,它从上到下进行枚举,因为这是人们直观地枚举堆栈的方式(想想一堆煎饼:你从顶部开始,然后向下工作)。

如果您从 Visual Studio 的调试器中查看集合的内容,我认为它会按照它们被枚举的顺序向您显示它们——而不是它们在内部存储的顺序*。

看看Reflector 中的Stack&lt;T&gt;.Push 方法,你会发现代码基本上就是你所期望的:

// (code to check array size)
this._array[this._size++] = item;
// (code to update internal version number)

因此堆栈在内部将新元素添加到其内部数组的末尾。让您感到困惑的是 Stack&lt;T&gt;.Enumerator 类,而不是 Stack&lt;T&gt; 类本身。

*我不知道这是否是真的,但对于Stack&lt;T&gt;来说是真的;原因见Hans Passant's excellent answer

【讨论】:

  • +1,作为对 OP 的回答,我很直观地首先看到了堆栈的顶部。
  • 当我在调试器中查看堆栈的内部数组时,我认为它会显示内部元素的实际顺序,而不是通过 IEnumerable 接口枚举它们的方式。感谢您澄清这一点。
  • 您也可以使用调试器查看存储元素的实际内部数组的内容。详情见我的回答:stackoverflow.com/questions/3215052/stack-implementation-in-c/…
【解决方案2】:

你让我去那里呆了一会儿,这确实看起来完全不符合低音。然而,还有其他事情正在发生。 Stack 类有一个调试器可视化工具,名为 System_StackDebugView。它是一个内部类,你必须使用 Reflector 才能看到它。

该可视化工具有一个 Items 属性,当您在调试器中展开节点时,您会看到该属性。该 Items 属性使用 Stack.ToArray()。看起来像这样:

public T[] ToArray()
{
    T[] localArray = new T[this._size];
    for (int i = 0; i < this._size; i++)
    {
        localArray[i] = this._array[(this._size - i) - 1];
    }
    return localArray;
}

是的,倒退。

【讨论】:

    【解决方案3】:

    您所描述的正确的实现,因为堆栈是 LIFO(后进先出)结构。想象它就像一堆盘子,最近放入堆栈的元素是第一个被移除的元素。您是否在其他地方遇到过 FIFO 堆栈?

    FIFO 将是一个队列。

    【讨论】:

    • wiki页面上的图片很好地解释了这一点,实际上tinyurl.com/lwtr2(对不起tinyURL,stackoverflow由于某种原因不喜欢wiki URL)
    • 我不知道为什么有人对你投了反对票。你的答案在教科书上是正确的。
    • 堆栈的底部通常在 ar[0] 中,顶部向数组的末尾增长。将最近的元素放在最前面需要在推入或弹出任何东西时移动每个元素。请记住,最初的问题是要求实施,而不是逻辑细节。
    • 我可能是错的,但我认为您误解了 Alex 的问题。我认为他要问的是为什么要在内部数组的开头插入最新的元素来实现堆栈。也就是说,我认为他被 Visual Studio 的调试器“欺骗”,认为这就是 MS 实现Stack&lt;T&gt; 的方式(当然不是,但你可以看到他为什么会对此感到困惑)。跨度>
    • @ziplin OP 对在 ar[0] 处显示堆栈顶部的调试器感到困惑。事实证明不是,但这并不重要。根据所提出的问题,这是一个低效且愚蠢的实现,而不是“正确的实现”。
    【解决方案4】:

    这里是栈的 push 和 pops 方法是如何实现的。请注意,它使用的是数组中的最后一个索引,而不是第一个。所以肯定还有其他问题导致你的倒退。

       public virtual void Push(object obj)
        {
            if (this._size == this._array.Length)
            {
                object[] destinationArray = new object[2 * this._array.Length];
                Array.Copy(this._array, 0, destinationArray, 0, this._size);
                this._array = destinationArray;
            }
            this._array[this._size++] = obj;
            this._version++;
        }
    
    
     public virtual object Pop()
        {
            if (this._size == 0)
            {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EmptyStack"));
            }
            this._version++;
            object obj2 = this._array[--this._size];
            this._array[this._size] = null;
            return obj2;
        }
    

    【讨论】:

    • 而解决问题的方法是 Stack.toArray() 返回的是反向副本,而不是 this._array
    • 是的,你是对的,它是 ToArray 方法,这很奇怪,并且与我的预期相反,但我不明白为什么你会像堆栈一样使用返回的 Array。但要回答这个问题,我不知道微软为什么会这样做。
    【解决方案5】:

    要添加到其他答案,如果在调试器中向下滚动到 Stack 元素的底部并打开 Raw View->Non-Public members->_array 你可以看到实际的内容用于保存项目并验证它们是否符合预期顺序的内部数组。

    【讨论】:

      【解决方案6】:

      只要你现在知道它是哪一端,我看不出他们认为哪一端是堆栈的顶部有什么关系。实际上更有意义的是,当您将某些东西“推”到堆栈上时,您将它推到顶部(开始)并将其他项目向下移动......

      【讨论】:

      • 但是向下移动需要 O(n) 时间来推送和弹出。只需将其放在顶部就需要 O(1) (如果您需要将数组复制到更大的数组中,则摊销)。因此,将堆栈的顶部放在数组的末尾可以获得巨大的效率。
      • 如丹涛回答的那样,实际上并不是这样存储的,只是这样枚举的。
      猜你喜欢
      • 2010-11-27
      • 1970-01-01
      • 1970-01-01
      • 2020-09-13
      • 2016-12-16
      • 1970-01-01
      • 2010-11-26
      • 1970-01-01
      • 2010-12-15
      相关资源
      最近更新 更多