數據結構基礎溫故-2.棧


現實生活中的事情往往都能總結歸納成一定的數據結構,例如餐館中餐盤的堆疊和使用,羽毛球筒里裝的羽毛球等都是典型的棧結構。而在.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;
            }
        }
    }
View Code

  (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;
            }
        }
    }
View Code

  (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,《淺談算法與數據結構:—棧和隊列

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM