資料結構基礎-棧和佇列
棧的理論描述
棧是一個有序線性表,只能在表的一端(成為棧頂,top)執行插入和刪除操作。最後插入的元素將第一個被刪除。所以棧也稱為後進先出(Last In First Out)或先進後出(First In Last Out)線性表。棧主要有兩個操作,一個入棧(push),表示在棧中插入一個元素,一個出棧(pop),表示將棧頂元素刪除。試圖對空棧執行出棧操作稱為UnderFlow,對滿棧執行入棧操作稱為OverFlow。
棧的抽象資料結構
棧的主要操作:
- void push(int data):將data插入棧
- int pop():刪除並返回最後一個插入棧的元素
棧的輔助操作
- int top():返回最後一個插入棧的元素
- int size():返回儲存在棧中元素的個數
- int isEmpty():判斷棧中是否有元素
- int StackFull():判斷棧中是否存滿元素
異常
pop和top操作在棧空的時候是不能操作的。試圖對空棧執行pop和top會丟擲異常。試圖對滿棧執行push操作也會丟擲異常。
程式碼實現
棧抽象資料結構有多種實現方式:
- 基於簡單陣列的實現方法
- 基於動態陣列的實現方法
- 基於連結串列的實現方法
簡單陣列:
public class DynArrayStack { private int top; private int capacity; private int[] array; public DynArrayStack(){ capacity = 1; array = new int[capacity]; top = -1; } public boolean isEmpty() { return (top == -1); } public boolean isStackFull() { return (top == capacity -1); } public void push(int data) { if (isStackFull()) { System.out.println("Stack overflow!"); }else { array[++top] = data; } } public int pop() { if (isEmpty()) { System.out.println("Stack is empty!"); return 0; }else { return array[top--]; } } }
動態陣列的實現方法大致一樣,只是比上面的多了當到達陣列最大容量的時候將容量擴大到現有的一倍。
private void doubleSize(){ int newArray[] = new int[capacity << 1]; System.arraycopy(array, 0, newArray, 0, capacity); capacity = capacity << 1; array = newArray; } public void push(int data) { if (isStackFull()) { doubleSize(); }else { array[++top] = data; } }
連結串列的棧實現方式:
public class LLStack { private LinkedNode headNode; public LLStack(){} public void push(int data) { if (headNode == null) { headNode = new LinkedNode(data); }else if (headNode.getData() == null) { headNode.setData(data); }else { LinkedNode node = new LinkedNode(data); node.setNext(headNode); headNode = node; } } public int pop(){ if (headNode == null) { throw new EmptyStackException("Stack Empty"); } int data = headNode.getData(); headNode = headNode.getNext(); return data; } public int top(){ return headNode == null ? null : headNode.getData(); } public boolean isEmpty(){ return headNode == null || headNode.getData() == null; } }
各種實現方式的比較:
基於陣列實現的棧:
- 各個操作都是常數時間開銷
- 每隔一段時間倍增的開銷教大
- n個操作的任意序列的平攤時間開銷為O(n)
基於連結串列實現的棧:
- 棧規模增加減少都很簡潔
- 各個操作都是常數時間開銷
- 每個操作都要使用額外的時間和空間開銷來處理指標
應用
- IDE和編譯器中符號匹配
- 中綴表示式轉換為字尾表示式
- 實現函式呼叫(包括遞迴)
棧的相關問題
eg:計算跨度:給定陣列A,A[i]的跨度S[i]定義為:滿足A[j]<=
A[j+1]而且在A[i]前的連續元素A[j]的最大個數。如圖
/* time:O(n), space:(n) */ public int[] findingSpans(int[] inputArray) { int[] spans = new int[inputArray.length]; Stack<Integer> stack = new Stack(); int p; for (int i = 0; i < inputArray.length; i++) { while (!stack.isEmpty() && inputArray[i] > inputArray[stack.pop()]) { stack.pop(); } if (stack.isEmpty()) { p = -1; }else { p = stack.top(); } spans[i] = i - p; stack.push(i); } return spans; }
eg:設計一個可以把棧中元素按照升序排列的排序演算法,並且不能限定棧的實現方法。
/* time:O(n^2) , space:O(n) */ public Stack<Integer> sort(Stack<Integer> s) { Stack<Integer> r = new Stack<>(); while (!s.isEmpty()) { int temp = s.pop(); while (!r.isEmpty() && r.peek() > temp) { s.push(r.pop()); } r.push(temp); } return r; }
eg:刪除所有相鄰的重複元素:給定一個數字陣列,刪除相鄰的重複數字,結果陣列中不能有任何相鄰的重複數字。
input | 1, 5, 6, 8, 8, 0, 1, 1, 0, 6, 5 |
---|---|
optput | 1 |
/* space:O(n) , time:O(n) */ public int removeAdjacentDuplicates(int[] a){ int stkptr = -1; int i = 0; while (i < a.length) { if (stkptr == -1 || a[stkptr] != a[i]) { stkptr++; a[stkptr] = a[i]; }else { while (i < a.length && a[stkptr] == a[i]) { i++; } stkptr--; } } return stkptr; }
佇列的理論描述
定義:佇列是一種只能在一端插入(隊尾),在另一端(隊首)的有序線性表。佇列中第一個插入就是第一個被刪除的元素。所以佇列是一種先進先出(FIFO,first in first out)或者後進後出(LILO,last in last out)線性表。
佇列的抽象資料結構
佇列的主要操作:
- void enQueue(int data):將data插入佇列
- int deQueue():刪除並返回隊首的元素
棧的輔助操作
- int front():返回隊首元素
- int size():返回儲存在佇列中元素的個數
- int isEmpty():判斷佇列中是否儲存了元素
異常
- 隊空時異常(執行deQueue操作)
- 隊滿時異常(執行enQueue操作)
程式碼實現
基於迴圈陣列
為什麼需要迴圈陣列?由佇列定義,在一端插入,一端刪除。 當執行多次插入和刪除操作後,就可以容易地發現數組靠前位置的空間被浪費了,所以基於簡單陣列實現佇列不是一個靠譜的方法。迴圈陣列剛好可以用來解決這個問題
public class DynArrayQueue { private int front; private int rear; private int capacity; private int[] array; public DynArrayQueue(){ capacity = 1; front = rear = -1; array = new int[capacity]; } public boolean isEmpty() { return front == -1; } public boolean isFull() { return (rear + 1) % capacity == front; } public int getSize() { if (front== -1) { return 0; } int size = (capacity + rear + 1 - front); if (size == 0) { return capacity; }else { return size; } } public void resizeQueue() { int initCapacity = capacity; capacity *= 2; int[] old = array; array = new intp[capacity]; for (int i = 0; i < old.length; i++) { array[i] = old[i]; } if (front > rear) { for (int i = 0; i < front; i++) { array[i + capacity] = array[i]; array[i] = null; } rear += initCapacity; } } public void enQueue(int data) { if (isFull()) { resizeQueue(); } rear = (rear + 1) % capacity; array[rear] = data; if (front == -1) { front = rear; } } public int deQueue(){ int data = null; if (isEmpty()) { throw new EmptyQueueException("Queue is empty"); }else { data = array[front]; if (front == rear) { front = rear = -1; }else { front = (front + 1) % capacity; } } return data; } }
基於動態迴圈陣列
基於上面的理解,迴圈陣列其實還是有個問題,就是當分配給陣列的個數到達最大值的時候,再插入元素就會溢位,所以有了動態迴圈陣列。
public class DynArrayQueue { private int front; private int rear; private int capacity; private int[] array; public DynArrayQueue(){ capacity = 1; front = rear = -1; array = new int[capacity]; } public boolean isEmpty() { return front == -1; } public boolean isFull() { return (rear + 1) % capacity == front; } public int getSize() { return ((capacity - front + rear + 1)%capacity); } public void enQueue(int data) { if (isFull()) { throw new QueueOverFlowException("queue overflow"); } rear = (rear + 1) % capacity; array[rear] = data; if (front == -1) { front = rear; } } public int deQueue(){ int data = null; if (isEmpty()) { throw new EmptyQueueException("Queue is empty"); }else { data = array[front]; if (front == rear) { front = rear = -1; }else { front = (front + 1) % capacity; } } return data; } }
基於連結串列
public class LLQueue { private LinkedListNode<Integer> frontNode; private LinkedListNode<Integer> rearNode; public boolean isEmpty() { return frontNode == null; } public void enQueue(int data){ LinkedListNode newNode = new LinkedListNode(data); if (rearNode != null) { rearNode.setNext(newNode); }else { frontNode = rearNode = newNode; } } public int deQueue() { int data; if (isEmpty()) { throw new EmptyQueueEmptyException("Queue Empty"); } data = frontNode.getData(); frontNode = frontNode.getNext(); return data; } }
連結串列和陣列的對比和棧是一樣的區別:連結串列需要花費指標這些額外的空間,但是操作和思路都很簡便。
應用
- 作業系統根據任務到達的順序排程任務(如列印佇列)
- 模擬現實世界中的佇列
- 多道程式設計
- 非同步資料傳輸
佇列的相關問題
eg:如果需要反向輸出佇列中元素,應該用什麼資料結構?
答:
public Queue<Integer> reverseQueue(Queue<Integer> queue){ Stack stack = new Stack<Integer>(); while(!queue.isEmpty(){ stack.push(queue.deQueue()); } while(!stack.isEmpty()){ queue.enQueue(stack.poll()); } return queue; }
eg:用兩個佇列實現一個棧的資料結構介面。
解答:
public class StackWithTwoQueues { LLQueue queue1; LLQueue queue2; public StackWithTwoQueues(){ queue1 = new LLQueue(); queue2 = new LLQueue(); } public void push(int data) { if (queue1.isEmpty()) { queue2.enQueue(data); }else { queue1.enQueue(data); } } public int pop(){ int i, size, value; i = 0; if (queue1.isEmpty()) { size = queue2.getSize(); while (i < size -1) { queue1.enQueue(queue2.deQueue()); i++; } value = queue2.deQueue(); }else { size = queue1.getSize(); while (i < size -1) { queue2.enQueue(queue1.deQueue()); i++; } value = queue1.deQueue(); } return 0; } }
eg:給定一個整數棧,如何檢查棧中每隊相鄰數字是否是連續的。每對數字的值可以是遞增或遞減的。如果棧中元素的個數是奇數,那麼組隊時忽略棧頂元素。例如,假設棧中元素為[4,5,-2,-3,11,10,5,6,20],那演算法應該輸出真,因為每對二元組(4,5),(-2,-3),(11,10)和(5,6)都是連續數字。
解答:
public boolean checkStackPairwiseOrder(Stack<Integer> s) { boolean pairwiseOrder = true; LLQueue queue = new LLQueue(); while (!s.isEmpty()) { queue.enQueue(s.pop()); } while (!queue.isEmpty()) { s.push(queue.enQueue()); } while (!s.isEmpty()) { int n = s.pop(); if (!s.isEmpty()) { int m = s.pop(); queue.add(m); if (Math.abs(n - m) != 1) { pairwiseOrder = false; break; } } } return pairwiseOrder; }
eg:給定一個整數k和一個整數佇列,如何把佇列中前k個元素逆置,其餘的元素保持不變?例如,如果k等於4,佇列中元素序列為[10,20,30,40,50,60,70,80,90],那麼應該輸出[40,30,20,10,50,60,70,80,90]。
解答:
public void reverseQueueFirstKElements(int k, Queue<Integer> q) { if (q == null || k > q.getSize()) { throw new IllegalArgumentException(); } if (k > 0) { Stack<Integer> stack = new Stack<Integer>(); for(int i=0;i<k;i++){ stack.push(q.remove()); } while(!stack.isEmpty()){ q.add(stack.pop()); } for (int i = 0; i < q.size() - k; i++) { q.add(q.remove()); } } }