定義

棧又名堆疊,是一種操作受限的線性表,僅能在表尾進行插入和刪除操作。

它的特點是先進後出,就好比我們往桶裡面放盤子,放的時候都是從下往上一個一個放(入棧),取的時候只能從上往下一個一個取(出棧),這個比喻並非十分恰當,比如拿盤子的時候只是習慣從上面開始拿,也可以從中間拿,而棧的話是隻能操作最上面的元素,這樣比喻只是為了便於瞭解。

剛開始接觸棧可能會有些疑問,我們已經有陣列和連結串列了,為什麼還要棧這個操作受限制的資料結構呢?陣列和連結串列雖然靈活,但是操作起來也更容易出錯,而棧因為操作受限,在特定場景中使用還是有優勢的。

當某個資料集合只涉及在一端插入和刪除資料,並且滿足先進後出的特性時,我們就應該首選“棧”這種資料結構。

棧的實現

棧的實現方式有兩種,一種是基於陣列實現的順序棧,另一種是基於連結串列實現的鏈式棧。它的主要操作也就兩個,即入棧和出棧,難度並不大。

先了解一下入棧(Push)和出棧(Pop),如下圖

    

順序棧

基於陣列實現,就面臨著陣列大小固定、擴容成本大的問題,下面是使用C#實現出棧和入棧簡單功能程式碼。

    // 基於陣列實現的順序棧
   public class ArrayStack
  {
       private string[] items;  // 陣列
       private int count;       // 棧中元素個數
       private int n;           //棧的大小

       // 初始化陣列,申請一個大小為n的陣列空間
       public ArrayStack(int n)
      {
           this.items = new string[n];
           this.n = n;
           this.count = 0;
      }

       // 入棧操作
       public bool Push(string item)
      {
           // 陣列空間不夠了,直接返回false,入棧失敗。
           if (count == n) return false;
           // 將item放到下標為count的位置,並且count加一
           items[count] = item;
           ++count;
           return true;
      }

       // 出棧操作
       public string Pop()
      {
           // 棧為空,則直接返回null
           if (count == 0) return null;
           // 返回下標為count-1的陣列元素,並且棧中元素個數count減一
           string tmp = items[count - 1];
           --count;
           return tmp;
      }
  }

上面程式碼有一些很明顯的缺點,比如儲存的資料型別固定為string(C#中使用泛型可以很好的解決),大小固定...這只是簡單的功能演示,後面分析C#中Stack原始碼時這些問題都會被化解。

出棧和入棧的時間複雜度是多少呢?這個很好計算,因為出棧和入棧都只涉及棧頂的元素,所以是O(1)。

空間複雜度呢?還是O(1),因為這裡只額外使用了count和n兩個臨時變數。

‍♂ 空間複雜度是指除了原本的資料儲存空間外,演算法執行還需要額外的儲存空間。例子中大小為n的陣列是無法省略的,也就是說這n個空間是必須的,對複雜度不瞭解的可以點選檢視一文搞定演算法複雜度分析

鏈式棧

話不多說,上程式碼

    // 連結串列實現棧
   public class LinkStack<T>
  {
       //棧頂指示器
       public Node<T> Top { get; set; }

       //棧中結點的個數
       public int NCount { get; set; }

       //初始化
       public LinkStack()
      {
           Top = null;
           NCount = 0;
      }

       //獲取棧的長度
       public int GetLength()
      {
           return NCount;
      }

       //判斷棧是否為空
       public bool IsEmpty()
      {
           if ((Top == null) && (0 == NCount))
          {
               return true;
          }
           return false;
      }

       //入棧
       public void Push(T item)
      {
           Node<T> p = new Node<T>(item);
           if (Top == null)
          {
               Top = p;
          }
           else
          {
               p.Next = Top;
               Top = p;
          }
           NCount++;
      }

       //出棧
       public T Pop()
      {
           if (IsEmpty())
          {
               return default(T);
          }
           Node<T> p = Top;
           Top = Top.Next;
           --NCount;
           return p.Data;
      }
  }

   //結點定義
   public class Node<T>
  {
       public T Data;

       public Node<T> Next;

       public Node(T item)
      {
           Data = item;
      }
  }

時間複雜度和空間複雜度均為O(1).

C#中Stack原始碼分析

前面我們已經知道了順序棧和鏈式棧的優缺點,那麼C#語言中自帶的Stack是基於什麼實現的呢?

答案是順序棧。Stack是一個泛型類,裡面定義了一個泛型陣列用以儲存資料

    private T[] _array;

既然是一個順序棧,為什麼在使用的過程中什麼不需要初始化陣列大小,也不用擔心擴容問題呢?

當我們例項化Stack的時候,會呼叫它的建構函式,初始化陣列大小為0.

    public Stack()
  {
       _array = _emptyArray;
       _size = 0;
       _version = 0;
  }

向陣列中新增元素時,會檢測陣列是否還有空閒容量,如果超出陣列大小,將進行擴容

    public void Push(T item)
  {
       if (_size == _array.Length)
      {
           T[] array = new T[(_array.Length == 0) ? 4 : (2 * _array.Length)];
           Array.Copy(_array, 0, array, 0, _size);
           _array = array;
      }

       _array[_size++] = item;
       _version++;
  }

正是因為C#幫我們封裝好了,所以我們使用起來才感覺如此的方便。

Push()函式的時間複雜度是多少呢?當棧中有空閒空間時,可以直接新增,它的時間複雜度是O(1)。但當記憶體不夠需要擴容時,需要重新申請記憶體,進行資料搬移,所以時間複雜度就變成了O(n),其平均時間複雜度也為O(1).

總結