現實生活中的事情往往都能總結歸納成一定的數據結構,例如餐館中餐盤的堆疊和使用,羽毛球筒里裝的羽毛球等都是典型的棧結構。而在.NET中,值類型在線程棧上進行分配,引用類型在托管堆上進行分配,本文所說的“棧”正是這種數據結構。棧和隊列都是常用的數據結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制。因此,棧和隊列也被稱為操作受限的線性表。這里,我們首先來了解一下棧。
一、棧的概念及操作
1.1 棧的基本特征
棧(stack)是限定僅在表尾進行插入和刪除操作的線性表。其特點是:”后進先出“或”先進后出“。
1.2 棧的基本操作
(1)棧的插入操作,叫作進棧,也稱壓棧、入棧:
(2)棧的刪除操作,叫作出棧,也有的叫作彈棧:
二、棧的基本實現
既然棧屬於特殊的線性表,那么其實現也會有兩種形式:順序存儲結構和鏈式存儲結構。首先,對於Stack,我們希望能夠提供以下幾個方法供調用:
Stack<T>() |
創建一個空的棧 |
void Push(T s) |
往棧中添加一個新的元素 |
T Pop() |
移除並返回最近添加的元素 |
bool IsEmpty() |
棧是否為空 |
int Size() |
棧中元素的個數 |
2.1 棧的順序存儲實現
對於順序存儲,我們可以參照順序表的實現方式,借助數組來存儲各個數據元素,然后對這個數組進行一定的封裝,提供指定的操作對數據元素進行插入和刪除即可。
(1)入棧操作實現
/// <summary> /// 入棧 /// </summary> /// <param name="node">節點元素</param> public void Push(T node) { if (index == nodes.Length) { // 增大數組容量 ResizeCapacity(nodes.Length * 2); } nodes[index] = node; index++; }
借助數組來實現入棧操作,其關鍵之處就在於top指針的移動。這里index初始值為0,每次入棧一個則將index加1,即指向下一個即將入棧的位置。由於這里采用了動態擴容的機制,所以沒有判斷棧中元素個數是否達到了最大值。
(2)出棧操作實現
出棧操作需要先去的要出棧的元素,然后將index減1,即指向下一個即將出棧的元素的位置。
/// <summary> /// 出棧 /// </summary> /// <returns>出棧節點元素</returns> public T Pop() { if(index == 0) { return default(T); } T node = nodes[index - 1]; index--; nodes[index] = default(T); if (index > 0 && index == nodes.Length / 4) { // 縮小數組容量 ResizeCapacity(nodes.Length / 2); } return node; }
這里首先需要判斷index是否已經到達了最小值,出棧的元素位置需要置為默認值(如果是int數組,那么會重置為0),最后返回出棧的元素對象。這里當元素個數小於數組的四分之一時會進行容量收縮操作。
(3)完整的類實現

/// <summary> /// 基於數組的棧實現 /// </summary> /// <typeparam name="T">類型</typeparam> public class MyArrayStack<T> { private T[] nodes; private int index; public MyArrayStack(int capacity) { this.nodes = new T[capacity]; this.index = 0; } /// <summary> /// 入棧 /// </summary> /// <param name="node">節點元素</param> public void Push(T node) { if (index == nodes.Length) { // 增大數組容量 ResizeCapacity(nodes.Length * 2); } nodes[index] = node; index++; } /// <summary> /// 出棧 /// </summary> /// <returns>出棧節點元素</returns> public T Pop() { if(index == 0) { return default(T); } T node = nodes[index - 1]; index--; nodes[index] = default(T); if (index > 0 && index == nodes.Length / 4) { // 縮小數組容量 ResizeCapacity(nodes.Length / 2); } return node; } /// <summary> /// 重置數組大小 /// </summary> /// <param name="newCapacity">新的容量</param> private void ResizeCapacity(int newCapacity) { T[] newNodes = new T[newCapacity]; if(newCapacity > nodes.Length) { for (int i = 0; i < nodes.Length; i++) { newNodes[i] = nodes[i]; } } else { for (int i = 0; i < newCapacity; i++) { newNodes[i] = nodes[i]; } } nodes = newNodes; } /// <summary> /// 棧是否為空 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.index == 0; } /// <summary> /// 棧中節點個數 /// </summary> public int Size { get { return this.index; } } }
(4)簡單的功能測試
首先,順序入棧10個隨機數,輸出其元素個數與是否為空;然后依次出棧,輸出每個數據元素;最后,再入棧15個隨機數並出棧輸出。
/// <summary> /// 基於數組的棧的測試 /// </summary> static void StackWithArrayTest() { MyArrayStack<int> stack = new MyArrayStack<int>(10); Console.WriteLine(stack.IsEmpty()); Random rand = new Random(); for (int i = 0; i < 10; i++) { stack.Push(rand.Next(1, 10)); } Console.WriteLine("IsEmpty:{0}",stack.IsEmpty()); Console.WriteLine("Size:{0}", stack.Size); Console.WriteLine("-------------------------------"); for (int i = 0; i < 10; i++) { int node = stack.Pop(); Console.Write(node + " "); } Console.WriteLine(); Console.WriteLine("IsEmpty:{0}", stack.IsEmpty()); Console.WriteLine("Size:{0}", stack.Size); Console.WriteLine("-------------------------------"); for (int i = 0; i < 15; i++) { stack.Push(rand.Next(1, 15)); } for (int i = 0; i < 15; i++) { int node = stack.Pop(); Console.Write(node + " "); } Console.WriteLine(); }
運行結果如下所示:
2.2 棧的鏈式存儲實現
對棧的鏈式存儲結構,我們可以參照單鏈表,為其設置一個頭結點。這里,我們先來看看節點的定義:
(1)節點的定義實現
/// <summary> /// 基於鏈表的棧節點 /// </summary> /// <typeparam name="T"></typeparam> public class Node<T> { public T Item { get; set; } public Node<T> Next { get; set; } public Node(T item) { this.Item = item; } public Node() { } }
(2)入棧操作的實現
實現Push方法,即向棧頂壓入一個元素,首先保存原先的位於棧頂的元素,然后新建一個新的棧頂元素,然后將該元素的下一個指向原先的棧頂元素。
/// <summary> /// 入棧 /// </summary> /// <param name="item">新節點</param> public void Push(T item) { Node<T> oldNode = first; first = new Node<T>(); first.Item = item; first.Next = oldNode; index++; }
(3)出棧操作的實現
實現Pop方法,首先保存棧頂元素的值,然后將棧頂元素設置為下一個元素:
/// <summary> /// 出棧 /// </summary> /// <returns>出棧元素</returns> public T Pop() { T item = first.Item; first = first.Next; index--; return item; }
這里還可以考慮將出棧元素的實例對象進行釋放資源操作。
(4)完整的代碼實現

/// <summary> /// 基於鏈表的棧節點 /// </summary> /// <typeparam name="T">元素類型</typeparam> public class Node<T> { public T Item { get; set; } public Node<T> Next { get; set; } public Node(T item) { this.Item = item; } public Node() { } } /// <summary> /// 基於鏈表的棧實現 /// </summary> /// <typeparam name="T">類型</typeparam> public class MyLinkStack<T> { private Node<T> first; private int index; public MyLinkStack() { this.first = null; this.index = 0; } /// <summary> /// 入棧 /// </summary> /// <param name="item">新節點</param> public void Push(T item) { Node<T> oldNode = first; first = new Node<T>(); first.Item = item; first.Next = oldNode; index++; } /// <summary> /// 出棧 /// </summary> /// <returns>出棧元素</returns> public T Pop() { T item = first.Item; first = first.Next; index--; return item; } /// <summary> /// 是否為空棧 /// </summary> /// <returns>true/false</returns> public bool IsEmpty() { return this.index == 0; } /// <summary> /// 棧中節點個數 /// </summary> public int Size { get { return this.index; } } }
(5)簡單的功能測試
這里跟順序存儲結構的測試代碼一致,就不再貼出來,直接看運行結果吧:
三、棧的基本應用
棧的應用場景很多,最常見的莫過於遞歸操作了,另外在運算表達式的求值上也有應用。這里看一個最經典的應用場景,進制轉換問題。講一個非負的十進制整數N轉換成其他D進制數是計算機計算的一個基本問題,如(135)10進制=(207)8進制。最簡單的解決辦法就是連續取模%和整除/,例如將10進制的50轉換為2進制數的過程如下圖所示:
由上圖的計算過程可知,D進制各位數的產生順序是從低位到高位,而輸出順序卻是從高位到低位,剛好和計算過程是相反的,因此可以利用棧進行逆序輸出。
private static string DecConvert(int num, int dec) { if (dec < 2 || dec > 16) { throw new ArgumentOutOfRangeException("dec", "只支持將十進制數轉換為二進制到十六進制數"); } MyLinkStack<char> stack = new MyLinkStack<char>(); int residue; // 余數入棧 while (num != 0) { residue = num % dec; if (residue >= 10) { // 如果是轉換為16進制且余數大於10則需要轉換為ABCDEF residue = residue + 55; } else { // 轉換為ASCII碼中的數字型字符1~9 residue = residue + 48; } stack.Push((char)residue); num = num / dec; } // 反序出棧 string result = string.Empty; while (stack.Size > 0) { result += stack.Pop(); } return result; }
這里考慮到輸出,所以使用了char類型作為節點數據類型,因此需要考慮ASCII碼中的數字型字符與字母型字符。運行結果如下圖所示:
①10進制數:350=>8進制數:536
②10進制數:72=>16進制數:48
③10進制數:38=>2進制數:100110
四、.NET中的Stack<T>
在.NET中,微軟已經為我們提供了一個強大的棧類型:Stack<T>,這里我們使用Reflector工具查看其具體實現,具體看看Push和Pop兩個方法,其他的各位園友可以自己去查看。
(1)Push方法源碼
public void Push(T item) { if (this._size == this._array.Length) { T[] destinationArray = new T[(this._array.Length == 0) ? 4 : (2 * this._array.Length)]; Array.Copy(this._array, 0, destinationArray, 0, this._size); this._array = destinationArray; } this._array[this._size++] = item; this._version++; }
(2)Pop方法源碼
public T Pop() { if (this._size == 0) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyStack); } this._version++; T local = this._array[--this._size]; this._array[this._size] = default(T); return local; }
可以看出,在.NET中Stack的實現是基於數組來實現的,在初始化時為其設置了一個默認的數組大小,在Push方法中當元素個數達到數組長度時,擴充2倍容量,然后將原數組拷貝到新的數組中。Pop方法中則跟我們剛剛實現的代碼基本相同。
參考資料
(1)程傑,《大話數據結構》
(2)陳廣,《數據結構(C#語言描述)》
(3)段恩澤,《數據結構(C#語言版)》
(4)yangecnu,《淺談算法與數據結構:—棧和隊列》