栈和队列相关的一些问题
作者:Grey
原文地址:
最小栈
题目链接见:LeetCode 155. Min Stack
主要思路
准备两个栈,一个栈叫 stack, 用于记录原始数据信息; 一个栈叫 min,用来存此时原始栈中的最小值,和 stack 同步增长,只不过 min 栈在每次 push 的时候,要用当前值和 min 栈顶部元素比较一下,如果 min 顶部元素较小,则 min 栈不 push 原始数,而是再次 push 一次 min 顶部的元素,这样每次 min 顶部的元素,就是原始栈中最小的那个值;
此外,弹出的时候,min 栈跟随 stack 栈同步弹出元素即可。
完整代码如下
class MinStack { Stack<Integer> stack; Stack<Integer> min; public MinStack() { stack = new Stack<>(); min = new Stack<>(); } public void push(int val) { stack.push(val); if (min.isEmpty()) { min.push(val); } else { min.push(Math.min(min.peek(),val)); } } public void pop() { stack.pop(); min.pop(); } public int top() { return stack.peek(); } public int getMin() { return min.peek(); } }
用双链表实现栈和队列
主要思路:使用双向链表实现一个双端队列,然后用双端队列去实现栈和队列。
双向链表的数据结构定义为
private final static class Node<T> { public T data; // 下一个节点 public Node<T> next; // 上一个节点 public Node<T> last; public Node(T data) { this.data = data; } }
双端队列(可以从头部进,头部出,也可以从尾部进,尾部出)的数据结构定义如下:
public final static class DoubleEndsQueue<T> { public Node<T> head; public Node<T> tail; public void addFromHead(T value) { // TODO // 从头部进入 } public void addFromBottom(T value) { // TODO // 从尾部进入 } public T popFromHead() { // TODO // 从头部弹出 } public T popFromBottom() { // TODO // 从尾部弹出 } public boolean isEmpty() { return head == null || tail == null; } }
如果我们可以实现上述双端队列,那么实现栈和队列就很简单了,不赘述,直接上代码。
使用自定义双端队列实现栈结构代码如下:
public final static class MyStack<T> { private DoubleEndsQueue<T> queue; public MyStack() { queue = new DoubleEndsQueue<T>(); } public void push(T value) { queue.addFromHead(value); } public T pop() { if (null == queue || isEmpty()) { return null; } return queue.popFromHead(); } public boolean isEmpty() { return queue.isEmpty(); } }
使用自定义双端队列实现队列结构代码如下:
public final static class MyQueue<T> { private DoubleEndsQueue<T> queue; public MyQueue() { queue = new DoubleEndsQueue<>(); } public void push(T value) { if (null == queue) { return; } queue.addFromHead(value); } public T poll() { if (isEmpty()) { return null; } return queue.popFromBottom(); } public boolean isEmpty() { return queue.isEmpty(); } }
双端队列的几个主要方法
void addFromHead(T value)
: 从头部进入
主要思路
1.如果链表没初始化,则新建头节点和尾部节点。
2.如果链表已经有元素了,则新建一个元素,并连上头节点,然后把头节点上一个节点设置为新建节点。
3.头节点指向新建节点。
代码如下
public void addFromHead(T value) { Node<T> node = new Node<>(value); if (head == null) { tail = node; } else { node.next = head; head.last = node; } head = node; }
void addFromBottom(T value)
: 从尾部进入
主要思路
1.如果链表没初始化,则新建头节点和尾部节点。
2.如果链表已经有元素了,则新建一个元素,尾部节点的下一个节点设置为这个新建节点,这个新建节点的上一个节点指向尾节点。
3.尾节点指向新建节点。
代码如下
public void addFromBottom(T value) { Node<T> node = new Node<>(value); if (tail == null) { head = node; } else { tail.next = node; node.last = tail; } tail = node; }
T popFromHead()
: 从头部弹出
主要思路:
-
如果是空链表,直接返回 null。
-
如果非空,但是只有一个节点,则把头部和尾部节点都置为空即可。
-
如果不止一个节点,则先找到头节点的下一个节点,然后把这个节点的上一个节点置为空,然后把头节点指向其下一个节点。
代码如下
public T popFromHead() { if (null == head || tail == null) { return null; } T data = head.data; if (head == tail) { head = null; tail = null; return data; } head = head.next; head.last = null; return data; }
T popFromBottom()
: 从尾部弹出
主要思路:
-
如果是空链表,直接返回 null。
-
如果非空,但是只有一个节点,则把头部和尾部节点都置为空即可。
-
如果不止一个节点,则先找到尾节点的上一个节点,然后把这个节点的下一个节点置为空,然后把尾节点指向其上一个节点。
主要代码
public T popFromBottom() { if (tail == null || head == null) { return null; } T data = tail.data; if (tail == head) { tail = null; head = null; return data; } tail = tail.last; tail.next = null; return data; }
LeetCode 有同样的题目,链接见:LeetCode 641. Design Circular Deque
注:LeetCode 这题需要多引入两个变量,一个是 size ,用于标识当前链表长度,一个是 capacity, 标识当前队列可以扩展的最大长度。其余方法和上述例子一样处理。
LeetCode 641. Design Circular Deque 这题完整代码如下
package leetcode.medium; /** * 用双向链表实现双端队列 * * @link <a href="https://leetcode-cn.com/problems/design-circular-deque/">双向链表实现双端队列</a> * @see snippet.Code_0011_DoubleEndsToStackAndQueue */ public class LeetCode_0641_DesignCircularDeque { class MyCircularDeque { // 双向链表 static class DoubleNode { public int val; public DoubleNode next; public DoubleNode last; public DoubleNode(int v) { val = v; } } // 头节点 DoubleNode head; // 尾节点 DoubleNode tail; // 当前元素数量 int size; // 容量 int capacity; public MyCircularDeque(int k) { capacity = k; } // 头部进 public boolean insertFront(int value) { if (isFull()) { return false; } if (isEmpty()) { // 链表是空的,先初始化 head = tail = new DoubleNode(value); } else { // 新建节点 DoubleNode newHead = new DoubleNode(value); newHead.next = head; head.last = newHead; head = newHead; } size++; return true; } // 尾部进 public boolean insertLast(int value) { if (isFull()) { return false; } if (isEmpty()) { head = tail = new DoubleNode(value); } else { DoubleNode rearNode = new DoubleNode(value); tail.next = rearNode; rearNode.last = tail; tail = rearNode; } size++; return true; } // 头部出 public boolean deleteFront() { if (isEmpty()) { return false; } if (size == 1) { head = tail = null; } else { // 不止一个元素 DoubleNode newHead = head.next; newHead.last = null; head = newHead; } size--; return true; } // 尾部出 public boolean deleteLast() { if (isEmpty()) { return false; } if (size == 1) { head = tail = null; } else { DoubleNode newRear = tail.last; newRear.next = null; tail = newRear; } size--; return true; } // 获取头部元素 public int getFront() { if (isEmpty()) { return -1; } else { return head.val; } } // 获取尾部元素 public int getRear() { if (isEmpty()) { return -1; } else { return tail.val; } } // 判断链表是否为空 public boolean isEmpty() { return size == 0; } // 判断链表是否满 public boolean isFull() { return size == capacity; } } }
使用栈来实现队列
题目描述见:LeetCode 232. Implement Queue using Stacks
主要思路
使用两个栈,一个是 push 栈,一个是 pop 栈,通过这两个栈互相导入的方式实现队列操作,每次入队列的元素,会直接存到 push 栈中,在队列弹出数据的时候,从 pop 栈中弹出,不过弹出需要按如下规则来处理
-
先将 push 栈中的内容一次性导入到 pop 栈中,然后 pop 栈弹出的元素,即为队列要弹出的元素。
-
只有 pop 栈空了才能导数据。
-
pop 栈不为空不用导数据。
完整代码见
class MyQueue { private final Stack<Integer> push; private final Stack<Integer> pop; public MyQueue() { push = new Stack<>(); pop = new Stack<>(); } public void push(int x) { push.push(x); } public int pop() { while (!push.isEmpty()) { pop.push(push.pop()); } int result = pop.pop(); while (!pop.isEmpty()) { push.push(pop.pop()); } return result; } public int peek() { while (!push.isEmpty()) { pop.push(push.pop()); } int result = pop.peek(); while (!pop.isEmpty()) { push.push(pop.pop()); } return result; } public boolean empty() { return push.isEmpty(); } }
使用队列来实现栈
使用两个队列,一个队列是 data 队列,一个队列是 help 队列,每次栈要存入数据的时候,其实是存在 data 队列中,但是栈在弹出数据的时候,要先把 data 队列里面的数据先一一存入 help 队列,然后把最后一个要存入的数据弹出,然后把 data 和 help 两个队列互换,在下一轮操作中,继续以上流程操作即可。
完整代码见
class MyStack { private Queue<Integer> data; private Queue<Integer> help; public MyStack() { data = new LinkedList<>(); help = new LinkedList<>(); } // 从尾部进 public void push(int x) { data.offer(x); } public int pop() { int result = 0; while (!data.isEmpty()) { int x = data.poll(); if (data.isEmpty()) { result = x; } else { help.offer(x); } } Queue<Integer> t = data; data = help; help = t; return result; } public int top() { int result = 0; while (!data.isEmpty()) { int x = data.poll(); help.offer(x); if (data.isEmpty()) { result = x; } } Queue<Integer> t = data; data = help; help = t; return result; } public boolean empty() { return data.isEmpty(); } }
数组实现不超过固定大小的循环队列
题目链接见:LeetCode 622. Design Circular Queue
使用数组可以实现,但是需要一些辅助变量,首先需要这两个变量来判断队列是否为空或者已经满了
// 当前队列的元素有多少了 private int size; // 当前队列可以支持的最大元素个数是多少 private final int limit;
其次,需要定义两个指针,用于标识当前的出队列位置和当前的入队列位置。
// 当前的出队列位置 private int popIndex; // 当前的入队列位置 private int pushIndex;
核心方法是如下两个
// 入队列 public boolean enQueue(int value) { if (isFull()) { // 满了无法入队列 return false; } // 元素增加1 size++; // 入队列的位置把元素填上 arr[pushIndex] = value; // 尾指针当前应该是入队列的位置 rear = pushIndex; // 下一个入队列的位置 pushIndex = next(pushIndex); return true; } // 出队列 public boolean deQueue() { if (isEmpty()) { // 空了怎么入队 return false; } // 元素减少一个 size--; // 到下一个出队列的位置即可 // 不需要处理当前出队列的位置,因为后续这个位置会被新的元素覆盖 popIndex = next(popIndex); return true; }
其中的 next
方法就是循环下标的意思
// 循环下标 // 0,1,2,3...N, 0, 1, 2....N, 0, 1,2... private int next(int pre) { return pre < limit - 1 ? pre + 1 : 0; }
完整代码见
class MyCircularQueue { private final int[] arr; // 当前的出队列位置 private int popIndex; // 当前的入队列位置 private int pushIndex; private int rear; // 当前队列的元素有多少了 private int size; // 当前队列可以支持的最大元素个数是多少 private final int limit; public MyCircularQueue(int k) { limit = k; arr = new int[limit]; } public boolean enQueue(int value) { if (isFull()) { return false; } size++; arr[pushIndex] = value; rear = pushIndex; pushIndex = next(pushIndex); return true; } public boolean deQueue() { if (isEmpty()) { return false; } size--; popIndex = next(popIndex); return true; } public int Front() { return isEmpty() ? -1 : arr[popIndex]; } public int Rear() { return isEmpty() ? -1 : arr[rear]; } public boolean isEmpty() { return size == 0; } public boolean isFull() { return size == limit; } private int next(int pre) { return pre < limit - 1 ? pre + 1 : 0; } }