1. 程式人生 > >資料結構基礎溫故-2.棧

資料結構基礎溫故-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#語言版)》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

資料結構基礎-2.

現實生活中的事情往往都能總結歸納成一定的資料結構,例如餐館中餐盤的堆疊和使用,羽毛球筒裡裝的羽毛球等都是典型的棧結構。而在.NET中,值型別線上程棧上進行分配,引用型別在託管堆上進行分配,本文所說的“棧”正是這種資料結構。棧和佇列都是常用的資料結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制

資料結構基礎-5.圖(中):圖的遍歷演算法

上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用

資料結構基礎-1.線性表(中)

在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。 一、單鏈表基礎 1.1 單鏈表的

資料結構基礎-6.查詢(上):基本查詢與樹表查詢

只要你開啟電腦,就會涉及到查詢技術。如炒股軟體中查股票資訊、硬碟檔案中找照片、在光碟中搜DVD,甚至玩遊戲時在記憶體中查詢攻擊力、魅力值等資料修改用來作弊等,都要涉及到查詢。當然,在網際網路上查詢資訊就更加是家常便飯。查詢是計算機應用中最常用的操作之一,也是許多程式中最耗時的一部分,查詢方法的優劣對於系統的執

資料結構基礎-4.樹與二叉樹(下)

上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過

資料結構基礎-6.查詢(下):雜湊表

雜湊(雜湊)技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖示表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。雜湊技術最適合的求解問題是查詢與給定值相等的記錄。

資料結構基礎-5.圖(中):最小生成樹演算法

圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成樹 1.1 生成樹   對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過

資料結構基礎-5.圖(上):圖的基本概念

前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的關係。圖(Graph)是一種複雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關係是任意的。現實生活中的很多

資料結構基礎-4.樹與二叉樹(中)

在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系

資料結構基礎-5.圖(下):最短路徑

圖的最重要的應用之一就是在交通運輸和通訊網路中尋找最短路徑。例如在交通網路中經常會遇到這樣的問題:兩地之間是否有公路可通;在有多條公路可通的情況下,哪一條路徑是最短的等等。這就是帶權圖中求最短路徑的問題,此時路徑的長度不再是路徑上邊的數目總和,而是路徑上的邊所帶權值的和。帶權圖分為無向帶權圖和有向帶權圖,但如

資料結構基礎-4.樹與二叉樹(上)

前面所討論的線性表元素之間都是一對一的關係,今天我們所看到的結構各元素之間卻是一對多的關係。樹在計算機中有著廣泛的應用,甚至在計算機的日常使用中,也可以看到樹形結構的身影,如下圖所示的Windows資源管理器和應用程式的選單都屬於樹形結構。樹形結構是一種典型的非線性結構,除了用於表示相鄰關係外,還可以表示層次

資料結構基礎-1.線性表(下)

在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。 一、迴圈連結串列基礎 1.1 迴圈連結串列節點結構   迴圈連結串列和單鏈表的

資料結構基礎-3.佇列

在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如

資料結構基礎-7.排序

排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為按關鍵字“有序”的記錄序列。如何進行排序,特別是高效率地進行排序時計算機工作者學習和研究的重要課題之一。排序有內部排序和外部排序之分,若整個排序過程不需要訪問外存便能完成,則稱此類排序為內部排序,反之則為外部排序。本篇主

資料結構基礎篇-------6.1 的順序儲存

//棧的順序儲存(與順序表類似) #include <stdio.h> #include <stdlib.h> #define N 32 //定義資料型別 typedef int datatype_t; //定義結構體 typedef stru

資料結構與演算法分析 - 2 - ADT

1.描述:實質是一種受到限制的表,即插入刪除只能在表的末端,能夠實現LIFO(後進先出) 2.棧的實現   連結串列實現(鏈棧)   陣列實現(順序棧)   3.鏈棧 建立一個空棧 1 struct Node 2 { 3 int value; 4 N

資料結構(c++)(2)--的應用

        接著上一篇部落格中的棧,這次介紹下棧的一些應用。         在看到的棧的這個特性後進先出的性質時,第一感覺就是這樣做有什麼用呢?把一個表的操作限制成這個樣子,不是在削減嗎?然而,在實際的應用中,這些存在於棧中的少數的操作卻是非常的強大和重要。下面給出三

資料結構與演算法(2)—— (java)

1 棧的實現 1.1 簡單陣列實現棧 package mystack; public class ArrayStack { private int top; //當前棧頂元素的下標 private int[] array; public ArraySt

軟考:資料結構基礎——

我們實現了可以動態增加棧大小的動態棧 並對realloc 函式的使用方法回顧了一下   一、棧的定義     1.棧是隻能訪問它的一端來實現資料的儲存和檢索的一種線性結構,他是一種LIFO(Last In First Out)先進先出的線性表。

資料結構基礎02-和佇列

本文系列 資料結構基礎01-基本概念和術語/線性表 資料結構基礎02-棧和佇列 棧 棧(Stack):所有的插入和刪除只在表的一端進行的線性表,即是一種操作受限的線性表。在表中,允許插入和刪除的一端叫棧頂(top),不允許插 入和刪除的另一端叫棧底(bottom)。 特點