单向队列、双端队列、栈的模型实现
引言
自己實現簡單的隊列、棧的邏輯結構。
隊列都包含頭和尾兩個指針,簡單的單向隊列只能在一端(如:head端)入列,在另一端(如:tail 端)出列;雙端隊列可以在 head 進出,也可以在 tail 進出。
棧的模型更加簡單,它分為 top 和 bottom 兩個指針,只能在 top 端進出。
經典的模型結構包含幾個主要方法,進、出、isEmpty、size 等。
一、隊列的實現
不論是單向隊列還是雙端隊列,始終都是在操作 head 和 tail 兩個指針。它們的實現沒有什么本質區別,實際上,掌握了雙端隊列,單向隊列就易如反掌。
在手動實現隊列的時候,需要在腦海中模擬出隊列的構造,左側為 head ,右側為 tail:
因此,隊列中一定需要兩個特殊的成員變量:head 和 tail。
其次,由于隊列和棧都屬于邏輯結構,因此,我們必須考慮使用哪種底層數據結構來實現,一般有兩個最典型的結構可供使用:雙向鏈表、數組。
1.1 雙端隊列的實現
首先,我們需要敲定隊列中的節點的數據結構,雙向鏈表是個不錯的選擇,它不受空間的約束,這是相對于數組實現的一個明顯的有點。
經典的雙向鏈表結構:
private static class Node<T> {public T value;private Node<T> prev;private Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}建議把Node設計為私有靜態內部類,作為隊列內部的存儲對象 T 的容器,它不需要被外界感知。
雙端隊列需要四個進出方法:頭進,頭出,尾進,尾出。
以頭進為例,當接收到一個入列對象時,我們需要先封裝為 Node 類型的 cur ,然后判斷隊列是否為空,即是否 head == null,如果為空,那么當前元素即是 head 也是 tail:
if (head == null) {// 如果是第一個元素,則頭尾都指向curhead = cur;tail = cur; }如果不為空,那么,需要建立 cur 與 head 之間的鏈表關系,即 cur.next = head,head.prev = cur。然后head角色變為 cur,就完成了入列操作:
else {// 如果不是第一個元素// cur在前,head在后,建立鏈表關系cur.next = head;head.prev = cur;// head更新head = cur; }再以尾出為例,返回值為泛型 T,首先同樣是要判斷隊列是否為空(head == null?),如果空,則直接返回null。若存在元素,首先我們要拿到 tail 元素,將它保存下來。
然后關鍵要判斷是否隊列中僅存在一個元素,即 head == tail,如果只有一個元素,那么取出后需要將 head 和 tail 都變為 null:
if (head == tail) {head = null;tail = null; }若不是最后一個,那么和入列相反,我們需要斬斷 Node 之間的引用關系,剛剛我們已經將原來的 tail 并保存到了一個新的 Node 局部變量里,此時已經可以將其成功返回,但除此之外,還需要釋放一些指針空間,并改變 tail 的指向:
else {// tail 前移tail = tail.prev;// 釋放空間cur.prev = null;tail.next = null; } return cur.value;剩下的方法邏輯大同小異,一定不要搞混 Node 節點的 prev 和 next,與 head 和 tail,明確并牢記這些概念的含義,以及鏈表左頭右尾的方向,入列時要建立兩個節點之間的對應關系,出列時要切斷兩個節點之間的關系。以下是完整代碼示例:
/*** 使用鏈表實現的雙端隊列*/ public class MyDoubleEndsQueue<T> {private Node<T> head;private Node<T> tail;private int size = 0;private static class Node<T> {T value;Node<T> prev;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}public void addFromHead(T value) {size++;Node<T> cur = new Node<T>(value);if (head == null) {// 如果是第一個元素,則頭尾都指向curhead = cur;tail = cur;} else {// 如果不是第一個元素// cur在前,head在后,建立鏈接關系cur.next = head;head.prev = cur;// head更新head = cur;}}public void addFromTail(T value) {size++;Node<T> cur = new Node<>(value);if (tail == null) {head = cur;tail = cur;} else {tail.next = cur;cur.prev = tail;tail = cur;}}public T popFromHead() {if (head == null) return null;size--;Node<T> cur = head;if (head == tail) {// 只有一個元素head = null;tail = null;} else {head = head.next;cur.next = null;head.prev = null;}return cur.value;}public T popFromTail() {if (tail == null) return null;size--;Node<T> cur = tail;if (head == tail) {head = null;tail = null;} else {// tail 前移tail = tail.prev;// 釋放空間cur.prev = null;tail.next = null;}return cur.value;}public boolean isEmpty() {return head == null;}public int size() {return size;} }1.2 單向隊列
1.2.1 雙向鏈表實現
單向隊列的實現可以建立在雙端隊列的基礎之上,直接調用其方法,或使用相同的邏輯來完成實現,完整代碼如下:
/*** 使用鏈表實現的單向隊列*/ public class MyQueue<T> {private Node<T> head;private Node<T> tail;private int size = 0;private static class Node<T> {Node<T> prev;T value;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}/*** 頭進** @param t*/public void push(T t) {Node<T> cur = new Node<>(t);size++;if (isEmpty()) {head = cur;tail = cur;} else {head.prev = cur;cur.next = head;head = cur;}}/**** 尾出* @return*/public T pop() {if (isEmpty()) return null;size--;Node<T> cur = tail;if (head == tail) {head = null;tail = null;} else {tail = tail.prev;cur.prev = null;tail.next = null;}return cur.value;}public boolean isEmpty() {return head == null;}public int size() {return size;} }1.2.2 數組實現隊列
數組實現隊列相對復雜一些,由于數組是定長結構,因此,需要在創建之初設定大小,且這一大小如果不涉及擴容,在使用過程中是不可變更的。
另一方面,由于數組的大小被限定,因此,必須循環使用數組空間,如果數組滿了再添加元素,以及數組空了再取元素,這兩種情況都需要報錯。
為了不陷入頭尾追趕的怪圈邏輯,在使用數組實現隊列時,需要特別小心,不要去考慮收尾追擊的問題,只需要通過 size 來控制,首尾自然不會出現“交叉”的情況。
/*** 環形數組隊列*/ public class RingArrayQueue<T> {private T[] arr;private int pushIdx;private int popIdx;private int size;private final int limit;public RingArrayQueue(Class<T> type, int limit) {this.arr = (T[]) Array.newInstance(type, limit);this.pushIdx = 0;this.popIdx = 0;this.size = 0;this.limit = limit;}public void push(T t) {if (size == limit)throw new RuntimeException("隊列滿了,不能再加了");size++;arr[pushIdx] = t;pushIdx = nextIndex(pushIdx);}public T pop() {if (size == 0)throw new RuntimeException("隊列空了!");size--;T t = (T) arr[popIdx];popIdx = nextIndex(popIdx);return t;}public boolean isEmpty() {return size == 0;}public int size() {return size;}// 如果現在的下標是i,返回下一個位置private int nextIndex(int i) {return i < limit - 1 ? i + 1 : 0;} }二、棧的實現
有了雙端隊列的邏輯鋪墊,棧的實現完全可以照搬,完整代碼如下:
/*** 使用鏈表實現的棧*/ public class MyStack<T> {private Node<T> top;private Node<T> bottom;private int size = 0;private static class Node<T> {Node<T> prev;T value;Node<T> next;public Node(T value) {this.value = value;}@Overridepublic String toString() {return value.toString();}}public void push(T t) {Node<T> cur = new Node<>(t);if (isEmpty()) {top = cur;bottom = cur;} else {top.next = cur;cur.prev = top;top = cur;}}public T pop() {if (isEmpty()) {return null;}size--;Node<T> cur = top;if (top == bottom) {top = null;bottom = null;} else {top = top.prev;top.next = null;cur.prev = null;}return cur.value;}public boolean isEmpty() {return top == null;}public int size() {return size;} }三、測試
使用jdk自帶的數據結構來對照測試自定義的隊列和棧,完整代碼如下:
import java.util.LinkedList; import java.util.Queue; import java.util.Stack;public class Checker {public static void main(String[] args) {int oneTestDataNum = 100;int value = 10000;int testTimes = 100000;for (int i = 0; i < testTimes; i++) {MyStack<Integer> myStack = new MyStack<>();MyQueue<Integer> myQueue = new MyQueue<>();// 對照組Stack<Integer> stack = new Stack<>();Queue<Integer> queue = new LinkedList<>();for (int j = 0; j < oneTestDataNum; j++) {int nums = (int) (Math.random() * value);if (stack.isEmpty()) {myStack.push(nums);stack.push(nums);} else {if (Math.random() < 0.5) {myStack.push(nums);stack.push(nums);} else {if (!isEqual(myStack.pop(), stack.pop())) {System.out.println("oops!");}}}int numq = (int) (Math.random() * value);if (stack.isEmpty()) {myQueue.push(numq);queue.offer(numq);} else {if (Math.random() < 0.5) {myQueue.push(numq);queue.offer(numq);} else {if (!isEqual(myQueue.pop(), queue.poll())) {System.out.println("oops!");}}}}}System.out.println("finish!");}public static boolean isEqual(Object o1, Object o2) {if (o1 == null) {return o1 == o2;}return o1.equals(o2);} }四、實現一個查找最小值的時間復雜度是 O(1) 的棧
設計思路:使用一個單獨的棧,用于存儲數據棧的最小值,在每次push 的時候,判斷是否為最小值,如果是,則如 minStack,如果不是,則繼續壓入上一次的最小值。完整代碼如下:
import java.util.Stack;public class GetMinStack {public static class MyMinStack {private Stack<Integer> dataStack;private Stack<Integer> minStack;public MyMinStack() {this.dataStack = new Stack<>();this.minStack = new Stack<>();}public void push(Integer num) {if (minStack.isEmpty()) {minStack.push(num);} else if (getMin() >= num) {minStack.push(num);} else {minStack.push(getMin());}dataStack.push(num);}public Integer pop() {if (dataStack.isEmpty()) {return null;}minStack.pop();return dataStack.pop();}public Integer getMin() {if (minStack.isEmpty())return null;return minStack.peek();}}public static void main(String[] args) {MyMinStack stack1 = new MyMinStack();stack1.push(3);System.out.println(stack1.getMin());stack1.push(4);System.out.println(stack1.getMin());stack1.push(1);System.out.println(stack1.getMin());System.out.println(stack1.pop());System.out.println(stack1.getMin());}}五、只使用棧實現隊列
只使用棧實現隊列和只使用隊列實現棧在面試中還比較常見,例如,用棧來實現一個圖的寬度優先遍歷,或用隊列來實現圖的深度優先遍歷,但圖的寬度優先遍歷一定需要用隊列來實現,而深度優先遍歷也一定要用棧來實現,因此,我們可以通過棧與隊列的轉化來完成類似這樣的面試題。
使用棧來實現隊列的思路是,用兩個棧,分別存儲入棧的順序,和出棧的順序,因為棧的出棧順序是入棧時的反向,因此,只要兩個棧互倒就可以實現隊列的結構。
互倒是一種思路,還有一種思路是,只從push 棧往 pop 棧倒,這樣的方式可以減少一定的性能開銷,但需要保證兩個原則:1、如果決定要倒數據,必須一定倒完;2、只要pop為空時,才可以倒入。
以下是兩種實現代碼,第一個是互倒的方式,第二個是單向倒入的方式:
import java.util.Stack;/*** 使用兩個棧來實現隊列*/ public class TwoStackQueue<T> {private Stack<T> pushStack;private Stack<T> popStack;public TwoStackQueue() {this.pushStack = new Stack<>();this.popStack = new Stack<>();}private synchronized void reverseStack(Stack<T> from, Stack<T> to) {while (!from.isEmpty()) {to.push(from.pop());}}public void push(T t) {if (!popStack.isEmpty()) {reverseStack(popStack, pushStack);}pushStack.push(t);}public T pop() {if (!pushStack.isEmpty())reverseStack(pushStack, popStack);if (!popStack.isEmpty())return popStack.pop();elsereturn null;} } import java.util.Stack;/*** 單向倒入雙棧隊列*/ public class SingleDirTwoStackQueue<T> {private Stack<T> pushStack;private Stack<T> popStack;public SingleDirTwoStackQueue() {pushStack = new Stack<>();popStack = new Stack<>();}private void pushToPop() {// 兩點原則:// 1、pop棧必須為空時才倒入if (popStack.empty()) {// 2、要倒就一次性全倒完while (!pushStack.empty()) {popStack.push(pushStack.pop());}}}public void push(T t) {pushStack.push(t);pushToPop();}public T pop() {if (popStack.empty() && pushStack.empty()) {return null;}pushToPop();return popStack.pop();}public T peek() {if (popStack.empty() && pushStack.empty()) {return null;}pushToPop();return popStack.peek();}public static void main(String[] args) {SingleDirTwoStackQueue<Integer> queue = new SingleDirTwoStackQueue<>();for (int i = 0; i < 100; i++) {// 多存少取if (Math.random() < 0.7) {queue.push(i);} else {System.out.print(queue.pop() + "\t");}}System.out.println("\n==================");while (true) {Integer tail = queue.pop();if (tail == null) break;System.out.print(tail + "\t");}} }六、只使用隊列實現棧
使用經典的單向隊列實現棧結構。
思路是,使用兩個隊列,masterQueue和slaveQueue,放入的時候,正常放入,當要取出的時候,循環將元素從master放入到slave中,最后只留 1 個元素,并返回,然后將 slave 與 master 的引用互換。
完整代碼如下:
import java.util.LinkedList; import java.util.Queue;/*** 雙隊列棧*/ public class TwoQueueStack<T> {private Queue<T> masterQueue;private Queue<T> slaveQueue;public TwoQueueStack() {this.masterQueue = new LinkedList<>();this.slaveQueue = new LinkedList<>();}public void push(T t) {masterQueue.offer(t);}public T poll() {while (masterQueue.size() > 1)slaveQueue.offer(masterQueue.poll());T ans = masterQueue.poll();Queue<T> tmp = masterQueue;masterQueue = slaveQueue;slaveQueue = tmp;return ans;}public T peek() {while (masterQueue.size() > 1)slaveQueue.offer(masterQueue.poll());T ans = masterQueue.poll();// peek 與 poll的區別就是取出后,再次放回slaveQueue.offer(ans);Queue<T> tmp = masterQueue;masterQueue = slaveQueue;slaveQueue = tmp;return ans;}}?
總結
以上是生活随笔為你收集整理的单向队列、双端队列、栈的模型实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html 图片使用scale,缩放:sc
- 下一篇: python数据结构递归树_python