棧和佇列
在學習棧和佇列前,先補充兩個概念:物理結構和邏輯結構。
1.什麼是資料儲存的物理結構呢?
舉個易懂的例子,如果把資料結構比作活生生的人,那麼物理結構就是人的血肉和骨骼,看得見,摸得著,實實在在。那麼,陣列和連結串列就是實實在在的儲存結構。
2.什麼是資料儲存的邏輯結構呢?
就剛剛的例子來說,人體之上,還存在著人的思想和精神,它看不見摸不著。那麼,棧和佇列以及數和圖等資料結構都是屬於邏輯結構。雖然棧和佇列是屬於邏輯結構的,但是他們的物理實現既可以利用陣列,也可以利用連結串列來實現。
3.什麼是棧?
棧(stack)是一種線性資料結構,它就像一個放羽毛球的圓筒容器,棧中的元素只能先入後出,最早進入的元素存放的位置叫做棧底(bottom),最後進入的元素存放的位置叫做棧頂(top)。
3.1 棧的基本操作
3.1.1 入棧
入棧操作(push)就是把新元素放入棧中,只允許從棧頂一側放入元素,新元素的位置將會稱為新的棧頂。
3.1.2 出棧
出棧操作(pop)就是把元素從棧中彈出,只有棧頂元素才允許出棧,出棧元素的前一個元素將會稱為新的棧頂。
java自己封裝的棧:
1 package com.algorithm.test; 2 3 import java.util.Stack; 4 5 /** 6 * @Author Jack丶WeTa 7 * @Date 2020/7/28 12:06 8 * @Description 關於棧的簡單操作(入棧和出棧)9 */ 10 public class StackTest { 11 public static void main(String[] args) { 12 Stack<Integer> stack = new Stack<>(); 13 14 for (int i = 0; i <= 10; i++) { 15 stack.push(i); 16 } 17 18 while (!stack.isEmpty()) { 19 System.out.println("棧:" + stack.toString() + "\t棧大小為:" + stack.size() + "\t出棧元素為:" + stack.pop());20 } 21 } 22 }
我們自己也可以用陣列來簡單實現一下:
package com.algorithm.test; /** * @Author Jack丶WeTa * @Date 2020/7/28 12:06 * @Description 關於棧的簡單操作(入棧和出棧) */ public class MyStackTest { private int[] array; private int top; //棧頂 public MyStackTest(int capacity){ this.array = new int[capacity]; } public void push(int element){ if(top >= array.length){ throw new IndexOutOfBoundsException("滿棧了,無法進行入棧操作!"); } array[top] = element; top++; } public int pop() { if(top <= 0){ throw new RuntimeException("棧為空,無法進行出棧操作!"); } int popElement = array[top-1]; top--; return popElement; } public void output(){ for (int i = 0; i < top; i++){ System.out.print(array[i] + ","); } } public static void main(String[] args) { MyStackTest myStackTest = new MyStackTest(6); myStackTest.push(5); myStackTest.push(2); myStackTest.push(4); myStackTest.push(8); myStackTest.push(0); myStackTest.push(9); myStackTest.output(); System.out.println(); System.out.println("-----開始出棧操作-----"); for (int i = 0; i < 6; i++){ System.out.print("出棧第" + (i+1) + "次:"); myStackTest.pop(); myStackTest.output(); System.out.println(); } } }
總結下棧操作的時間複雜度。由於入棧和出棧指揮影響到最後一個元素,不涉及到其他元素的整體移動,所以無論是以陣列還是連結串列實現,入棧和出棧的時間複雜度都是O(1)。
4.什麼是佇列呢?
佇列(queue)是一種線性資料結構,它的特徵和行駛車輛的單行隧道很相似。不同於棧的先入後出,佇列中的元素只能先入先出。佇列的出口端叫做隊頭(front),佇列的入口端叫隊尾(rear)。
4.1 佇列的基本操作
4.1.1 入隊
入隊(enqueue)就是把新元素放入佇列中,只允許在隊尾的位置放置元素,新元素的下一個位置將會成為新的隊尾。
4.1.2 出隊
出隊(dequeue)就是把元素移出佇列,只允許在對頭一側移出元素,出隊元素的後一個元素將會成為新的對頭。但是,如果像這樣子不斷出隊的話,對頭左邊的空間將失去作用,那佇列的容量豈不是越來越小了?那麼,我們就需要採取一些措施,用陣列實現的佇列可以採用迴圈佇列的方式來維持佇列容量的恆定。
5.什麼是迴圈佇列呢?
假設佇列經過反覆的入隊和出隊操作,還剩下2個元素,在“物理”上分佈於陣列的末尾位置,這時又有一個新元素將要入隊。在陣列不做擴容的前提下,如何讓新元素入隊並確定新的隊尾位置呢?我們可以利用已出隊元素留下的空間,讓隊尾指標重新指回陣列的首位。這樣一來,整個佇列的元素就“迴圈”起來了。在物理儲存上,隊尾的位置也可以在對頭之前。當再有元素入隊時,將其放在陣列的首位,隊尾指標繼續後移即可。一直到(隊尾下標+1)%陣列長度 = 對頭下標 時,代表此佇列真的已經滿了。需要注意的是,隊尾指標指向的位置永遠空出1位,所以佇列最大容量比陣列長度小1。
那麼我們也可以用程式碼自己實現一下所謂的迴圈佇列:
package com.algorithm.test; /** * @Author Jack丶WeTa * @Date 2020/7/28 14:57 * @Description 迴圈佇列的實現demo */ public class MyQueueTest { private int[] array; //隊頭 private int front; //隊尾 private int rear; public MyQueueTest(int capacity) { this.array = new int[capacity]; } /** * @param element 入隊的元素 * @throws Exception */ public void enQueue(int element) throws Exception { if ((rear + 1) % array.length == front) { // (隊尾下標+1)% 陣列長度 == 隊頭下標 //佇列已經滿了 throw new Exception("佇列已滿!"); } array[rear] = element; rear = (rear + 1) % array.length; //入隊後,隊尾向後挪一位 } /** * @return 出隊的元素 * @throws Exception */ public int deQueue() throws Exception { if (rear == front) { throw new Exception("佇列已空!"); } int deQueueElement = array[front]; //出隊的元素 front = (front + 1) % array.length; //出隊後,隊頭向後挪1位 return deQueueElement; } /** * 輸出佇列 */ public void output() { for (int i = front; i != rear; i = (i+1)%array.length){ System.out.print(array[i] + ","); } } public static void main(String[] args) throws Exception { MyQueueTest myQueueTest = new MyQueueTest(6); myQueueTest.enQueue(3); myQueueTest.enQueue(5); myQueueTest.enQueue(6); myQueueTest.enQueue(8); myQueueTest.enQueue(1); myQueueTest.output(); System.out.println("--------------------------------------"); myQueueTest.deQueue(); myQueueTest.deQueue(); myQueueTest.deQueue(); myQueueTest.output(); System.out.println("--------------------------------------"); myQueueTest.enQueue(2); myQueueTest.enQueue(4); myQueueTest.enQueue(9); myQueueTest.output(); } }
迴圈佇列不僅充分利用了陣列的空間,還避免了陣列元素整體移動的麻煩,那麼入隊和出隊的時間複雜度也同樣是O(1)。
6.棧和佇列的應用
6.1 棧的應用
棧的輸出順序和輸入順序相反,所以棧通常用於對“歷史”的回溯,也就是逆流而上追溯“歷史”。
例如實現遞迴的邏輯,就可以用棧來代替,因為棧可以回溯方法的呼叫鏈。
棧還有一個著名的應用場景是麵包屑導航,使使用者在瀏覽頁面時可以輕鬆的回溯到上一級或更上一級的頁面。
6.2 佇列的應用
佇列的輸出順序和輸入順序相同,所以佇列通常用於對“歷史”的回放,也就是按照“歷史”的順序把“歷史”重演一遍。
例如在多執行緒中,爭奪公平鎖的等待佇列,就是按照訪問順序來決定執行緒在佇列中的次序的。
再如網路爬蟲實現網路抓取時,也是把抓取的網站URL存入佇列中,在按照存入佇列的順序來依次抓取和解析的。
6.3 雙端佇列
雙端佇列這種資料結構可以說是綜合了棧和佇列的優點,對雙端佇列來說,從對頭一端可以入隊或出隊,從隊尾一端也可以入隊或出隊。
6.4 優先佇列
還有一種佇列,它遵循的不是先入先出,而是誰的優先順序最高,誰先出隊。這種佇列叫做優先佇列。有限佇列已經不屬於線性資料機構的範疇了,他是基於二叉堆來實現的。
總結一下,本次主要學習棧和簡單的佇列的概念和運用,6.3和6.4的知識後面會進行研究和學習的!