栈和队列相关的一些问题
作者: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;
}
}