淺談演算法和資料結構: 一 棧和佇列
最近晚上在家裡看Algorithems,4th Edition,我買的英文版,覺得這本書寫的比較淺顯易懂,而且“圖碼並茂”,趁著這次機會打算好好學習做做筆記,這樣也會印象深刻,這也是寫這一系列文章的原因。另外普林斯頓大學在Coursera 上也有這本書同步的公開課,還有另外一門演算法分析課,這門課程的作者也是這本書的作者,兩門課都挺不錯的。
計算機程式離不開演算法和資料結構,本文簡單介紹棧(Stack)和佇列(Queue)的實現,.NET中與之相關的資料結構,典型應用等,希望能加深自己對這兩個簡單資料結構的理解。
1. 基本概念
概念很簡單,棧 (Stack)是一種後進先出(last in first off,LIFO)的資料結構,而佇列(Queue)則是一種先進先出 (fisrt in first out,FIFO)的結構,如下圖:
2. 實現
現在來看如何實現以上的兩個資料結構。在動手之前,Framework Design Guidelines這本書告訴我們,在設計API或者實體類的時候,應當圍繞場景編寫API規格說明書。
1.1 Stack的實現
棧是一種後進先出的資料結構,對於Stack 我們希望至少要對外提供以下幾個方法:
Stack<T>() |
建立一個空的棧 |
void Push(T s) |
往棧中新增一個新的元素 |
T Pop() |
移除並返回最近新增的元素 |
boolean IsEmpty() |
棧是否為空 |
int Size() |
棧中元素的個數 |
要實現這些功能,我們有兩中方法,陣列和連結串列,先看連結串列實現:
棧的連結串列實現:
我們首先定義一個內部類來儲存每個連結串列的節點,該節點包括當前的值以及指向下一個的值,然後建立一個節點儲存位於棧頂的值以及記錄棧的元素個數;
class Node { public T Item{get;set;} public Node Next { get; set; } }
private Node first = null; private int number = 0;
現在來實現Push方法,即向棧頂壓入一個元素,首先儲存原先的位於棧頂的元素,然後新建一個新的棧頂元素,然後將該元素的下一個指向原先的棧頂元素。整個Pop過程如下:
實現程式碼如下:
void Push(T node) { Node oldFirst = first; first = new Node(); first.Item= node; first.Next = oldFirst; number++; }
Pop方法也很簡單,首先儲存棧頂元素的值,然後將棧頂元素設定為下一個元素:
T Pop() { T item = first.Item; first = first.Next; number--; return item; }
基於連結串列的Stack實現,在最壞的情況下只需要常量的時間來進行Push和Pop操作。
棧的陣列實現:
我們可以使用陣列來儲存棧中的元素Push的時候,直接新增一個元素S[N]到陣列中,Pop的時候直接返回S[N-1].
首先,我們定義一個數組,然後在建構函式中給定初始化大小,Push方法實現如下,就是集合裡新增一個元素:
T[] item; int number = 0; public StackImplementByArray(int capacity) { item = new T[capacity]; }
public void Push(T _item) { if (number == item.Length) Resize(2 * item.Length); item[number++] = _item; }
Pop方法:
public T Pop() { T temp = item[--number]; item[number] = default(T); if (number > 0 && number == item.Length / 4) Resize(item.Length / 2); return temp; }
在Push和Pop方法中,為了節省記憶體空間,我們會對陣列進行整理。Push的時候,當元素的個數達到陣列的Capacity的時候,我們開闢2倍於當前元素的新陣列,然後將原陣列中的元素拷貝到新陣列中。Pop的時候,當元素的個數小於當前容量的1/4的時候,我們將原陣列的大小容量減少1/2。
Resize方法基本就是陣列複製:
private void Resize(int capacity) { T[] temp = new T[capacity]; for (int i = 0; i < item.Length; i++) { temp[i] = item[i]; } item = temp; }
當我們縮小陣列的時候,採用的是判斷1/4的情況,這樣效率要比1/2要高,因為可以有效避免在1/2附件插入,刪除,插入,刪除,從而頻繁的擴大和縮小陣列的情況。下圖展示了在插入和刪除的情況下陣列中的元素以及陣列大小的變化情況:
分析:1. Pop和Push操作在最壞的情況下與元素個數成比例的N的時間,時間主要花費在擴大或者縮小陣列的個數時,陣列拷貝上。
2. 元素在記憶體中分佈緊湊,密度高,便於利用記憶體的時間和空間區域性性,便於CPU進行快取,較LinkList記憶體佔用小,效率高。
2.2 Queue的實現
Queue是一種先進先出的資料結構,和Stack一樣,他也有連結串列和陣列兩種實現,理解了Stack的實現後,Queue的實現就比較簡單了。
Stack<T>() |
建立一個空的佇列 |
void Enqueue(T s) |
往佇列中新增一個新的元素 |
T Dequeue() |
移除佇列中最早新增的元素 |
boolean IsEmpty() |
佇列是否為空 |
int Size() |
佇列中元素的個數 |
首先看連結串列的實現:
Dequeue方法就是返回連結串列中的第一個元素,這個和Stack中的Pop方法相似:
public T Dequeue() { T temp = first.Item; first = first.Next; number--; if (IsEmpety()) last = null; return temp; }
Enqueue和Stack的Push方法不同,他是在連結串列的末尾增加新的元素:
public void Enqueue(T item) { Node oldLast = last; last = new Node(); last.Item = item; if (IsEmpety()) { first = last; } else { oldLast.Next = last; } number++; }
同樣地,現在再來看如何使用陣列來實現Queue,首先我們使用陣列來儲存資料,並定義變數head和tail來記錄Queue的首尾元素。
和Stack的實現方式不同,在Queue中,我們定義了head和tail來記錄頭元素和尾元素。當enqueue的時候,tial加1,將元素放在尾部,當dequeue的時候,head減1,並返回。
public void Enqueue(T _item) { if ((head - tail + 1) == item.Length) Resize(2 * item.Length); item[tail++] = _item; } public T Dequeue() { T temp = item[--head]; item[head] = default(T); if (head > 0 && (tail - head + 1) == item.Length / 4) Resize(item.Length / 2); return temp; } private void Resize(int capacity) { T[] temp = new T[capacity]; int index = 0; for (int i = head; i < tail; i++) { temp[++index] = item[i]; } item = temp; }
3. .NET中的Stack和Queue
在.NET中有Stack和Queue泛型類,使用Reflector工具可以檢視其具體實現。先看Stack的實現,下面是擷取的部分程式碼,僅列出了Push,Pop方法,其他的方法希望大家自己使用Reflector檢視:
可以看到.NET中的Stack的實現和我們之前寫的差不多,也是使用陣列來實現的。.NET中Stack的初始容量為4,在Push方法中,可以看到當元素個數達到陣列長度時,擴充2倍容量,然後將原陣列拷貝到新的陣列中。Pop方法和我們之前實現的基本上相同,下面是具體程式碼,只截取了部分:
[Serializable, ComVisible(false), DebuggerTypeProxy(typeof(System_StackDebugView<>)), DebuggerDisplay("Count = {Count}"), __DynamicallyInvokable] public class Stack<T> : IEnumerable<T>, ICollection, IEnumerable { // Fields private T[] _array; private const int _defaultCapacity = 4; private static T[] _emptyArray; private int _size; private int _version; // Methods static Stack() { Stack<T>._emptyArray = new T[0]; } [__DynamicallyInvokable] public Stack() { this._array = Stack<T>._emptyArray; this._size = 0; this._version = 0; } [__DynamicallyInvokable] public Stack(int capacity) { if (capacity < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired); } this._array = new T[capacity]; this._size = 0; this._version = 0; } [__DynamicallyInvokable] public void CopyTo(T[] array, int arrayIndex) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if ((arrayIndex < 0) || (arrayIndex > array.Length)) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); } if ((array.Length - arrayIndex) < this._size) { ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); } Array.Copy(this._array, 0, array, arrayIndex, this._size); Array.Reverse(array, arrayIndex, this._size); } [__DynamicallyInvokable] 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; } [__DynamicallyInvokable] 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++; } // Properties [__DynamicallyInvokable] public int Count { [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get { return this._size; } } }
下面再看看Queue的實現:
[Serializable, DebuggerDisplay("Count = {Count}"), ComVisible(false), DebuggerTypeProxy(typeof(System_QueueDebugView<>)), __DynamicallyInvokable] public class Queue<T> : IEnumerable<T>, ICollection, IEnumerable { // Fields private T[] _array; private const int _DefaultCapacity = 4; private static T[] _emptyArray; private int _head; private int _size; private int _tail; private int _version; // Methods static Queue() { Queue<T>._emptyArray = new T[0]; } public Queue() { this._array = Queue<T>._emptyArray; } public Queue(int capacity) { if (capacity < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNumRequired); } this._array = new T[capacity]; this._head = 0; this._tail = 0; this._size = 0; } public T Dequeue() { if (this._size == 0) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EmptyQueue); } T local = this._array[this._head]; this._array[this._head] = default(T); this._head = (this._head + 1) % this._array.Length; this._size--; this._version++; return local; } public void Enqueue(T item) { if (this._size == this._array.Length) { int capacity = (int)((this._array.Length * 200L) / 100L); if (capacity < (this._array.Length + 4)) { capacity = this._array.Length + 4; } this.SetCapacity(capacity); } this._array[this._tail] = item; this._tail = (this._tail + 1) % this._array.Length; this._size++; this._version++; } private void SetCapacity(int capacity) { T[] destinationArray = new T[capacity]; if (this._size > 0) { if (this._head < this._tail) { Array.Copy(this._array, this._head, destinationArray, 0, this._size); } else { Array.Copy(this._array, this._head, destinationArray, 0, this._array.Length - this._head); Array.Copy(this._array, 0, destinationArray, this._array.Length - this._head, this._tail); } } this._array = destinationArray; this._head = 0; this._tail = (this._size == capacity) ? 0 : this._size; this._version++; } public int Count { [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get { return this._size; } } }
可以看到.NET中Queue的實現也是基於陣列的,定義了head和tail,當長度達到陣列的容量的時候,使用了SetCapacity方法來進行擴容和拷貝。
4. Stack和Queue的應用
Stack這種資料結構用途很廣泛,比如編譯器中的詞法分析器、Java虛擬機器、軟體中的撤銷操作、瀏覽器中的回退操作,編譯器中的函式呼叫實現等等。
4.1 執行緒堆 (Thread Stack)
執行緒堆是操作系型系統分配的一塊記憶體區域。通常CPU上有一個特殊的稱之為堆指標的暫存器 (stack pointer) 。在程式初始化時,該指標指向棧頂,棧頂的地址最大。CPU有特殊的指令可以將值Push到執行緒堆上,以及將值Pop出堆疊。每一次Push操作都將值存放到堆指標指向的地方,並將堆指標遞減。每一次Pop都將堆指標指向的值從堆中移除,然後堆指標遞增,堆是向下增長的。Push到執行緒堆,以及從執行緒堆中Pop的值都存放到CPU的暫存器中。
當發起函式呼叫的時候,CPU使用特殊的指令將當前的指令指標(instruction pointer),如當前執行的程式碼的地址壓入到堆上。然後CPU通過設定指令指標到函式呼叫的地址來跳轉到被呼叫的函式去執行。當函式返回值時,舊的指令指標從堆中Pop出來,然後從該指令地址之後繼續執行。
當進入到被呼叫的函式中時,堆指標減小來在堆上為函式中的區域性變數分配更多的空間。如果函式中有一個32位的變數分配到了堆中,當函式返回時,堆指標就返回到之前的函式呼叫處,分配的空間就會被釋放。
如果函式有引數,這些引數會在函式呼叫之前就被分配在堆上,函式中的程式碼可以從當前堆往上訪問到這些引數。
執行緒堆是一塊有一定限制的記憶體空間,如果呼叫了過多的巢狀函式,或者區域性變數分配了過多的記憶體空間,就會產生堆疊溢位的錯誤。
下圖簡單顯示了執行緒堆的變化情況。
4.2 算術表示式的求值
Stack使用的一個最經典的例子就是算術表示式的求值了,這其中還包括字首表示式和字尾表示式的求值。E. W. Dijkstra發明了使用兩個Stack,一個儲存操作值,一個儲存操作符的方法來實現表示式的求值,具體步驟如下:
1) 當輸入的是值的時候Push到屬於值的棧中。
2) 當輸入的是運算子的時候,Push到運算子的棧中。
3) 當遇到左括號的時候,忽略
4) 當遇到右括號的時候,Pop一個運算子,Pop兩個值,然後將計算結果Push到值的棧中。
下面是在C#中的一個簡單的括號表示式的求值:
/// <summary> /// 一個簡單的表示式運算 /// </summary> /// <param name="args"></param> static void Main(string[] args) { Stack<char> operation = new Stack<char>(); Stack<Double> values = new Stack<double>(); //為方便,直接使用ToChar對於兩位數的陣列問題 Char[] charArray = Console.ReadLine().ToCharArray(); foreach (char s in charArray) { if (s.Equals('(')) { } else if (s.Equals('+')) operation.Push(s); else if (s.Equals('*')) operation.Push(s); else if (s.Equals(')')) { char op = operation.Pop(); if (op.Equals('+')) values.Push(values.Pop() + values.Pop()); else if (op.Equals('*')) values.Push(values.Pop() * values.Pop()); } else values.Push(Double.Parse(s.ToString())); } Console.WriteLine(values.Pop()); Console.ReadKey(); }
執行結果如下:
下圖演示了操作棧和資料棧的變化。
在編譯器技術中,字首表示式,字尾表示式的求值都會用到堆。
4.3 Object-C中以及OpenGL中的圖形繪製
在Object-C以及OpenGL中都存在”繪圖上下文”,有時候我們對區域性物件的繪圖不希望影響到全域性的設定,所以需要儲存上一次的繪圖狀態。下面是Object-C中繪製一個圓形的典型程式碼:
- (void)drawGreenCircle:(CGContextRef)ctxt { UIGraphicsPushContext(ctxt); [[UIColor greenColor] setFill]; // draw my circle UIGraphicsPopContext(); } - (void)drawRect:(CGRect)aRect { CGContextRef context = UIGraphicsGetCurrentContext(); [[UIColor redColor] setFill]; // do some stuff [self drawGreenCircle:context]; // do more stuff and expect fill color to be red }
可以看到,在drawGreenCircle方法中,在設定填充顏色之前,我們Push儲存了繪圖上下文的資訊,然後在設定當前操作的一些環境變數,繪製圖形,繪製完成之後,我們Pop出之前儲存的繪圖上下文資訊,從而不影響後面的繪圖。
4.4 一些其他場景
有一個場景是利用stack 處理多餘無效的請求,比如使用者長按鍵盤,或者在很短的時間內連續按某一個功能鍵,我們需要過濾到這些無效的請求。一個通常的做法是將所有的請求都壓入到堆中,然後要處理的時候Pop出來一個,這個就是最新的一次請求。
Queue的應用
在現實生活中Queue的應用也很廣泛,最廣泛的就是排隊了,”先來後到” First come first service ,以及Queue這個單詞就有排隊的意思。
還有,比如我們的播放器上的播放列表,我們的資料流物件,非同步的資料傳輸結構(檔案IO,管道通訊,套接字等)
還有一些解決對共享資源的衝突訪問,比如印表機的列印佇列等。訊息佇列等。交通狀況模擬,呼叫中心使用者等待的時間的模擬等等。
5. 一點點感悟
本文簡單介紹了Stack和Queue的原理及實現,並介紹了一些應用。
最後一點點感悟就是不要為了使用資料結構而使用資料結構。舉個例子,之前看到過一個陣列反轉的問題,剛學過Stack可能會想,這個簡單啊,直接將字串挨個的Push進去,然後Pop出來就可以了,完美的解決方案。但是,這是不是最有效地呢,其實有更有效地方法,那就是以中間為對摺,然後左右兩邊替換。
public static void Reverse(int[] array, int begin, int end) { while (end > begin) { int temp = array[begin]; array[begin] = array[end]; array[end] = temp; begin++; end--; } }
相關推薦
淺談演算法和資料結構: 一 棧和佇列
最近晚上在家裡看Algorithems,4th Edition,我買的英文版,覺得這本書寫的比較淺顯易懂,而且“圖碼並茂”,趁著這次機會打算好好學習做做筆記,這樣也會印象深刻,這也是寫這一系列文章的原因。另外普林斯頓大學在Coursera 上也有這本書同步的公開課,還有另外一門演算法分析課,這門課程的作者也是
淺談算法和數據結構: 一 棧和隊列
操作 拷貝 ray 對數 () stack實現 定義 pub for 原文出自:http://www.cnblogs.com/yangecnu/p/Introduction-Stack-and-Queue.html 1. 基本概念 概念很簡單,棧 (Stack)是一種後進先
浙江中醫藥大學-《資料結構》-棧和佇列演算法設計
1、從鍵盤上輸入一個字尾表示式,試編寫演算法計算表示式的值。規定:逆波蘭表示式的長度不超過一行,以$符作為輸入結束,運算元之間用空格分隔,操作符只可能有+、-、*、/四種運算。例如:234 34+2*$。 思路:逆波蘭表示式(即字尾表示式)求值規則如下:設立運算數棧OPND,對錶達式從左到右掃描(讀入),當表
演算法與資料結構+一點點ACM從入門到進階吐血整理推薦書單(珍藏版)
轉載自某大佬部落格 https://pymlovelyq.github.io/2018/10/06/Algorithm/ 前言:技術書閱讀方法論 一.速讀一遍(最好在1~2天內完成) 人的大腦記憶力有限,在一天內快速看完一本書會在大腦裡留下深刻印象,對於之後複習以及總
淺談List連結串列結構一
個人理解。主要用於筆記。 1、假設需要存的資料結構 class Data{ String key; String name; int age; } 2、建立連結串列結構的父類介面 此處只寫了在連結串列尾部新增的方法,有興趣的可以自己試試擴充套件 建議可以新增: 在表頭
常見資料結構(一)-棧,佇列,堆,雜湊表
轉載:https://blog.csdn.net/u013063153/article/details/54667361?locationNum=8&fps=1 寫在前面 本文所有圖片均截圖自coursera上普林斯頓的課程《A
資料結構之棧和佇列
棧和佇列是兩種重要的線性結構。從資料結構角度來看,棧和佇列也是線性表,它們是操作受限的線性表,被稱為限定性的資料結構。 棧(Stack) 棧是限定僅在表尾進行插入或刪除操作的線性表。 表尾端被稱為棧頂(top),表頭端稱為棧底(bottom),不含元素的空表稱為空棧。
常見的資料結構(棧、佇列、陣列、連結串列和紅黑樹)
(一)棧 棧:stack,又稱堆疊,它是運算受限的線性表,其限制是僅允許在標的一端進行插入和刪除操作,不允許在其 他任何位置進行新增、查詢、刪除等操作。 簡單的說:採用該結構的集合,對元素的存取有如下的特點先進後出(即,存進去的元素,要在後它後面的元素依次取出後,才能取出該元素)。例如,子彈
【資料結構】棧和佇列相關練習題:判斷有效的括號
給定一個只包括 '(',')','{','}','[',']' 的字串,判斷字串是否有效。 有效字串需滿足: 左括號必須用相同型別的右括號閉合。 左括號必須以正確的順序閉合。 注意空字串可被認為是有效字串。 具體實現程式碼如下: #pragma once #incl
演算法與資料結構+一點點ACM從入門到進階吐血整理推薦書單pdf附網盤下載連結
前言: 技術書閱讀方法論 一.速讀一遍(最好在1~2天內完成) 人的大腦記憶力有限,在一天內快速看完一本書會在大腦裡留下深刻印象,對於之後複習以及總結都會有特別好的作用。對於每一章的知識,先閱讀標題,弄懂大概講的是什麼主題,再去快速看一遍,不懂也沒有關係,但是一定要在不懂的
python資料結構之棧和佇列
1.功能實現 之前文章有,可以點開看看 棧 佇列 2.應用(1)括號匹配及字尾表示式 class Solution(object): def isValid(self, s): """ :type s: str :rtype
資料結構實驗---棧和佇列
#include<stdio.h> #include<stdlib.h> #define N 105 int a[N]; struct node { int data; n
資料結構---------------------------------------順序棧和鏈棧的實現
今天想把這個順序棧實現一下,就翻閱資料整理了一下。 下面分別給出順序棧和鏈棧的相應功能的實現。 1)順序棧的圖示: 下面是程式碼: #include<iostream> #include<algorithm> #include<str
演算法與資料結構(一):時間複雜度與空間複雜度
最近突然萌生了一個想法,好好系統的學習一下演算法與資料結構然後產生一系列的文章來回顧與總結學到的東西,這部分我想從最簡單的部分一一介紹總結,包括一些很基礎的內容 為什麼要學習資料結構與演算法 以前在學校的時候就知道 程式 = 演算法 + 資料結構,程式的作用是用來處理與解決現實問題,而在描述與解決現實問
資料結構順序棧和鏈棧基本操作----c++實現
順序棧: #include<iostream> using namespace std; #define MaxSize 50 class SeqStack{ private: int
【面試心得】演算法和資料結構:查詢和排序
演算法和資料結構在面試中備受面試官的青睞,其中排序和查詢是面試中考察演算法的重點。 在準備面試的時候,我們應該重點掌握二分查詢、快速排序和歸併排序,做到能隨時正確、完整地寫出程式碼。 查詢和排序都是在程式設計中常用到的演算法。關於查詢演算法應該掌握:順序查詢、二分查詢、雜
js資料結構之棧和佇列
棧是一種遵從後進先出(LIFO)原則的有序集合。新新增的或待刪除的元素都儲存在棧末尾,稱作棧頂,另一端稱作棧底。在棧裡,新元素都靠近棧頂,舊元素就接近棧底。 佇列是遵循先進先出(FIFO)原則的一組有序的項。佇列在尾部新增新元素,並從頂部移除元素。最新新增的元
C++資料結構之棧和佇列
棧和佇列也是常用的資料結構。棧“先進後出”的性質使得它有很多的應用,如果你學過組合語言,在設計程式時,中斷出現,要響應中斷,那麼程式中的重要暫存器資訊就要壓入棧中,等中斷程式執行完把斷點處的資訊出棧;
資料結構的棧和堆和程式中的堆和棧
格式和部分內容稍作修改。 在計算機領域,堆疊是一個不容忽視的概念,我們編寫的C語言程式基本上都要用到。但對於很多的初學著來說,堆疊是一個很模糊的概念。堆疊:一種資料結構、一個在程式執行時用於存放的地方,這可能是很多初學者的認識,因為我曾經就是這麼想的和組合語言中的堆疊
用資料結構的棧和佇列 寫 迴文判斷
假設稱正讀和反讀都相同的字元序列為“迴文”,例如,‘abba’和‘abcba’是迴文,‘abcde’和‘ababab’則不是迴文。試寫一個演算法判別讀入的一個以‘@’為結束符的字元序列是否是“迴文”。程式設計實現該程式。 #include <stdio.h>#i