在阅读本篇文章之前,建议读者优先阅读专栏内前面的文章。
目录
前言
本篇文章主要介绍Java中的栈和队列这两种数据结构自行模拟实现的过程和使用示例。
一、栈:
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。栈的删除操作叫做出栈,出数据在栈顶。


我们可以先去看一下它的源码:
java
public class Stack<E> extends Vector<E> {
/**
* Creates an empty Stack.
*/
public Stack() {
}
/**
* Pushes an item onto the top of this stack. This has exactly
* the same effect as:
* <blockquote><pre>
* addElement(item)</pre></blockquote>
*
* @param item the item to be pushed onto this stack.
* @return the {@code item} argument.
* @see java.util.Vector#addElement
*/
public E push(E item) {
addElement(item);
return item;
}
/**
* Removes the object at the top of this stack and returns that
* object as the value of this function.
*
* @return The object at the top of this stack (the last item
* of the {@code Vector} object).
* @throws EmptyStackException if this stack is empty.
*/
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
/**
* Looks at the object at the top of this stack without removing it
* from the stack.
*
* @return the object at the top of this stack (the last item
* of the {@code Vector} object).
* @throws EmptyStackException if this stack is empty.
*/
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
/**
* Tests if this stack is empty.
*
* @return {@code true} if and only if this stack contains
* no items; {@code false} otherwise.
*/
public boolean empty() {
return size() == 0;
}
/**
* Returns the 1-based position where an object is on this stack.
* If the object {@code o} occurs as an item in this stack, this
* method returns the distance from the top of the stack of the
* occurrence nearest the top of the stack; the topmost item on the
* stack is considered to be at distance {@code 1}. The {@code equals}
* method is used to compare {@code o} to the
* items in this stack.
*
* @param o the desired object.
* @return the 1-based position from the top of the stack where
* the object is located; the return value {@code -1}
* indicates that the object is not on the stack.
*/
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
/** use serialVersionUID from JDK 1.0.2 for interoperability */
@java.io.Serial
private static final long serialVersionUID = 1224463164541339165L;
}
可以看到其中的方法是很少的,因此栈是一种相对比较简单的数据结构,总的来说,它的常用方法有以下几种:
| 方法 | 功能 |
|---|---|
| Stack() | 构造一个空的栈 |
| E push(E e) | 将e入栈,并返回e |
| E pop() | 将栈顶元素出栈并返回 |
| E peek() | 获取栈顶元素 |
| int size() | 获取栈中有效元素个数 |
| boolean empty() | 检测栈是否为空 |
读者可以用下面这段代码自行简单测试一下结果:
java
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s.size()); // 获取栈中有效元素个数---> 4
System.out.println(s.peek()); // 获取栈顶元素---> 4
s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
if(s.empty()){
System.out.println("栈空");
}else{
System.out.println(s.size());
}
}
简单了解之后,我们可以来自行模拟实现一下这个结构,我们拿数组来实现一下:
java
public class MyStack {
public int[] elem;
public int usedSize;
public MyStack() {
this.elem = new int[10];
}
}
然后根据源码和我们前面说的具体的逻辑构建,读者可以自己尝试去实现这个结构的常见方法。我这里给出我的实现:
java
import java.util.Arrays;
public class MyStack {
public int[] elem;
public int usedSize;
public MyStack() {
this.elem = new int[10];
}
public void push(int val) {
if(isFull()){
this.elem = Arrays.copyOf(elem,elem.length * 2);
}
elem[usedSize++] = val;
}
public boolean isFull() {
return usedSize == elem.length;
}
public int pop() {
if(isEmpty()){
throw new EmptyStackException();
}else{
return elem[--usedSize];
}
}
public int peek(){
if(isEmpty()){
throw new EmptyStackException();
}
return elem[usedSize-1];
}
public boolean isEmpty() {
return usedSize == 0;
}
}
java
public class EmptyStackException extends RuntimeException{
public EmptyStackException() {
}
public EmptyStackException(String message) {
super(message);
}
}
可以看到我们上面用数组实现的栈,那我们是否可以用链表来实现栈呢?答案显然是可以的,我们上面实现的就叫做顺序栈,而用链表实现的则称为链栈。那么我们是用单链表还是双链表来实现呢?如果使用单链表的话,我们就要采用头插法的方法来实现入栈和出栈;而使用双链表的话,不管是头插法还是尾插法我们都可以去实现。
二、栈的应用场景:
栈的特性就决定了它能够有很多的应用场景,我们可以一个一个来说。首先就是栈可以改变元素的序列,比如说下面这个问题:
bash
若进栈序列为1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()
A: 1,4,3,2 B: 2,3,4,1 C: 3,1,4,2 D: 3,4,2,1
这道题的答案是C选项,因为即使可以同时入栈和出栈,也不可能会出现元素3第一个出栈,元素1跳过元素2优先出栈的情况。
第二中应用就是栈可以把递归转化为循环,比如说来逆序打印链表,可以看下下面的代码:
java
// 递归方式
void printList(Node head){
if(null != head){
printList(head.next);
System.out.print(head.val + " ");
}
}
// 循环方式
void printList(Node head){
if(null == head){
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while(null != cur){
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
System.out.print(s.pop().val + " ");
}
}
第三个重要的应用就是可以用来实现括号的匹配,可以访问下面的链接:
这个就是典型的用栈来解决的问题,具体的情况可能出现下面几种:

读者可以先自行实现一下,我这里给出我的代码:
java
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i = 0; i < s.length(); i++){
char ch = s.charAt(i);
if(ch == '(' || ch == '[' || ch == '{'){
stack.push(ch);
}else{
if(stack.isEmpty()){
return false;
}
char ch2 = stack.peek();
if(ch == ')' && ch2 == '(' || ch == ']' && ch2 == '[' || ch == '}' && ch2 == '{'){
stack.pop();
}else{
return false;
}
}
}
if(!stack.isEmpty()){
return false;
}
return true;
}
}
第四个重要应用就是可以用于逆波兰式求值,可以访问下面的链接:
具体关于逆波兰式的相关信息可以访问题目中的百科链接,我这里给出我的代码:
java
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack();
for(String str : tokens){
if(!isOperator(str)){
int x = Integer.parseInt(str);
stack.push(x);
}else{
int val2 = stack.pop();
int val1 = stack.pop();
switch(str){
case "+":
stack.push(val1 + val2);
break;
case "-":
stack.push(val1 - val2);
break;
case "*":
stack.push(val1 * val2);
break;
case "/":
stack.push(val1 / val2);
break;
}
}
}
return stack.pop();
}
private boolean isOperator(String ch){
if(ch.equals("+") || ch.equals("-") || ch.equals("*") || ch.equals("/")){
return true;
}else{
return false;
}
}
}

第五个我们则是来看下压入与弹出序列是否能对应上,也就是用代码形式实现对于第一个应用的判断:
基本思路可以根据第一中应用部分那里去推,我这里给出我的代码:
java
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pushV int整型一维数组
* @param popV int整型一维数组
* @return bool布尔型
*/
public boolean IsPopOrder (int[] pushV, int[] popV) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for(int i = 0; i < pushV.length; i++){
stack.push(pushV[i]);
while(!stack.empty() && j < popV.length && stack.peek() == popV[j]){
stack.pop();
j++;
}
}
return stack.empty();
}
}

最后一个则是最小栈的问题,可以点击下面的链接去访问:155. 最小栈 - 力扣(LeetCode)
这道题的基本思路就是我们定义两个栈,普通栈按顺序存放所有元素,最小栈遵循以下原则,如果最小栈为空,就把元素放入;而如果最小栈的栈顶元素没有当前元素的元素小,也要放。出栈的时候,我们最小栈的元素如果和普通栈的栈顶元素相同的话,就一起出栈。读者可自行实现,我这里给出我的代码:
java
class MinStack {
public Stack<Integer> stack;
public Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int value) {
stack.push(value);
if(minStack.empty()){
minStack.push(value);
}else{
int peekVal = minStack.peek();
if(value <= peekVal){
minStack.push(value);
}
}
}
public void pop() {
if(stack.empty()){
return;
}
int popVal = stack.pop();
if(popVal == minStack.peek()){
minStack.pop();
}
}
public int top() {
if(stack.empty()){
return -1;
}
return stack.peek();
}
public int getMin() {
if(minStack.empty()){
return -1;
}
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(value);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/

三、栈、虚拟机栈、栈帧:
栈、虚拟机栈和栈帧是三个层次不同的概念。栈本身是一种后进先出的数据结构,也可以泛指程序运行时用于保存临时数据的内存区域;虚拟机栈则是Java虚拟机运行时数据区中的一部分,它是线程私有的,每个线程创建时都会拥有自己的虚拟机栈,用来记录该线程中方法调用和执行的过程;栈帧则是虚拟机栈中的基本单位,每调用一个方法,虚拟机栈中就会压入一个新的栈帧,方法执行结束后该栈帧再被弹出。一个栈帧中通常包含局部变量表、操作数栈、动态链接、方法返回地址等信息,分别用于保存方法参数和局部变量、参与字节码指令运算、支持方法调用关系以及记录方法执行完后返回到哪里。因此,可以简单理解为:栈是一种通用的数据组织方式,虚拟机栈是JVM为每个线程分配的方法调用栈,而栈帧就是虚拟机栈中对应一次具体方法调用的执行记录。
四、队列:
队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(FirstIn First Out) 的特点。其中进行插入操作的一端称为队尾,进行删除操作的一端称为队头。

我们可以来看一下它的源码:
java
public interface Queue<E> extends Collection<E> {
/**
* Inserts the specified element into this queue if it is possible to do so
* immediately without violating capacity restrictions, returning
* {@code true} upon success and throwing an {@code IllegalStateException}
* if no space is currently available.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this queue
* @throws NullPointerException if the specified element is null and
* this queue does not permit null elements
* @throws IllegalArgumentException if some property of this element
* prevents it from being added to this queue
*/
boolean add(E e);
/**
* Inserts the specified element into this queue if it is possible to do
* so immediately without violating capacity restrictions.
* When using a capacity-restricted queue, this method is generally
* preferable to {@link #add}, which can fail to insert an element only
* by throwing an exception.
*
* @param e the element to add
* @return {@code true} if the element was added to this queue, else
* {@code false}
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this queue
* @throws NullPointerException if the specified element is null and
* this queue does not permit null elements
* @throws IllegalArgumentException if some property of this element
* prevents it from being added to this queue
*/
boolean offer(E e);
/**
* Retrieves and removes the head of this queue. This method differs
* from {@link #poll() poll()} only in that it throws an exception if
* this queue is empty.
*
* @return the head of this queue
* @throws NoSuchElementException if this queue is empty
*/
E remove();
/**
* Retrieves and removes the head of this queue,
* or returns {@code null} if this queue is empty.
*
* @return the head of this queue, or {@code null} if this queue is empty
*/
E poll();
/**
* Retrieves, but does not remove, the head of this queue. This method
* differs from {@link #peek peek} only in that it throws an exception
* if this queue is empty.
*
* @return the head of this queue
* @throws NoSuchElementException if this queue is empty
*/
E element();
/**
* Retrieves, but does not remove, the head of this queue,
* or returns {@code null} if this queue is empty.
*
* @return the head of this queue, or {@code null} if this queue is empty
*/
E peek();
}
对于队列来说,它常用的方法有如下:

需要注意的是,Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。读者可以使用下面的代码自行测试结果:
java
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5); // 从队尾入队列
System.out.println(q.size());
System.out.println(q.peek()); // 获取队头元素
q.poll();
System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println(q.size());
}
}
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种,顺序结构和链式结构。读者可以思考下,队列的实现使用顺序结构还是链式结构好?就我而言,双链表是比较好的选择。

我们这里就拿双向链表的形式来实现,实现过程相对简单,请读者先自行实现,我这里给出我的代码:
java
public class MyQueue {
static class ListNode {
int val;
public ListNode prev;
public ListNode next;
ListNode(int val) {
this.val = val;
}
}
public ListNode first = null;
public ListNode last = null;
public int usedSize = 0;
public void offer(int val){
ListNode node = new ListNode(val);
if(isEmpty()){
first = last = node;
}else{
last.next = node;
node.prev = last;
last = last.next;
}
usedSize++;
}
public int poll(){
if(isEmpty()){
return -1;
}
int val = first.val;
first = first.next;
if(first != null){
first.prev = null;
}
usedSize--;
return val;
}
public int peek(){
if(isEmpty()){
return -1;
}
return first.val;
}
public boolean isEmpty(){
return usedSize == 0;
}
}
五、特殊队列:
我们首先先来介绍第一种特殊的队列,也就是环形队列。操作系统课程讲解生产者消费者模型时可能就会使用循环队列。环形队列通常使用数组实现。

对于我们的循环队列来说,它的精髓就是对于取模运算的使用,当下标最后再往后时,也就是offset小于array.length时,index = (index + offset) % array.length:

而当下标最前再往前时,也就是offset小于array.length时,index = (index + array.length - offset) % array.length:

除了这种增加减少元素的问题之外,循环队列另一个重要的问题就是如何判断队列是满还是空。总的来说有三种方法,第一种是添加一个size属性进行记录,第二种是保留一个固定的空位,第三种则是使用变量进行标记。我们常用的就是下面这种方法:

我们可以访问下面的链接,来更好的体会循环队列:
读者可以先行实现,我这里给出我的代码:
java
class MyCircularQueue {
public int front;
public int rear;
public int[] elem;
public MyCircularQueue(int k) {
elem = new int[k + 1];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear] = value;
rear = (rear + 1) % elem.length;
return true;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front + 1) % elem.length;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
public int Rear() {
if(isEmpty()){
return -1;
}
int index = (rear == 0) ? elem.length - 1 : rear - 1;
return elem[index];
}
public boolean isEmpty() {
return rear == front;
}
public boolean isFull() {
return (rear + 1) % elem.length == front;
}
}
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* boolean param_1 = obj.enQueue(value);
* boolean param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* boolean param_5 = obj.isEmpty();
* boolean param_6 = obj.isFull();
*/

接下来,我们再来了解一下另一种特殊的队列,也就是双端队列。双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque是double ended queue的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。

Deque也 是一个接口,使用时必须创建 LinkedList的对象。在实际工程中,使用 Deque 接口是比较多的,栈和队列均可以使用该接口。

java
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现
六、队列与栈的相互转化:
我们讲的上面两种数据结构,二者本身是可以互相模拟对方实现的。这也是我们十分常见的面试题,我们首先可以先看看如何用队列实现栈:225. 用队列实现栈 - 力扣(LeetCode)
这道题最主要的就是入栈和出栈操作,基本思想就是使用两个队列,模拟入栈时就把元素放到不为空的队列之中,模拟出栈时就把不为空的队列中的size-1个元素放到另一个队列之中。最后剩下的自然就是我们模拟出栈的元素。读者可先自行实现,我这里给出我的代码:
java
class MyStack {
public Queue<Integer> qu1;
public Queue<Integer> qu2;
public MyStack() {
qu1 = new LinkedList<>();
qu2 = new LinkedList<>();
}
public void push(int x) {
if(!qu1.isEmpty()){
qu1.offer(x);
}else if(!qu2.isEmpty()){
qu2.offer(x);
}else{
qu1.offer(x);
}
}
public int pop() {
if(empty()){
return -1;
}
if(!qu1.isEmpty()){
int size = qu1.size();
for(int i = 0; i < size - 1; i++){
qu2.offer(qu1.poll());
}
return qu1.poll();
}else{
int size = qu2.size();
for(int i = 0; i < size - 1; i++){
qu1.offer(qu2.poll());
}
return qu2.poll();
}
}
public int top() {
if(empty()){
return -1;
}
if(!qu1.isEmpty()){
int size = qu1.size();
int val = 0;
for(int i = 0; i < size; i++){
val = qu1.poll();
qu2.offer(val);
}
return val;
}else{
int size = qu2.size();
int val = 0;
for(int i = 0; i < size; i++){
val = qu2.poll();
qu1.offer(val);
}
return val;
}
}
public boolean empty() {
return qu1.isEmpty() && qu2.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/

接下来我们再来看下如何用栈来模拟实现队列:232. 用栈实现队列 - 力扣(LeetCode)
同样,在这个里面比较重要的就是如何模拟入队操作和出队操作。模拟入队操作的话,就是放到第一个栈即可。模拟出队操作的话,我们还是先判断两个栈是否为空,如果是空的话,就需要把第一个栈当中的所有元素都放到第二个栈中,然后取出第二个栈中的栈顶元素;如果不是空的,我们就直接取出第二个栈的栈顶元素。读者可思考如何实现,我这里给出我的代码:
java
class MyQueue {
public ArrayDeque<Integer> stack1;
public ArrayDeque<Integer> stack2;
public MyQueue() {
stack1 = new ArrayDeque<>();
stack2 = new ArrayDeque<>();
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
if(empty()){
return -1;
}
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
public int peek() {
if(empty()){
return -1;
}
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/

总结
本文详细介绍了Java中栈和队列这两种数据结构的实现原理与应用场景。文章首先讲解了栈的后进先出(LIFO)特性,通过数组和链表两种方式实现栈结构,并分析了栈在递归转循环、括号匹配、逆波兰表达式等场景的应用。随后介绍了队列的先进先出(FIFO)特性及其实现方式,重点讨论了循环队列和双端队列这两种特殊队列的实现原理。最后通过LeetCode题目示例,展示了如何用队列实现栈和用栈实现队列的相互转换方法,提供了完整的代码实现。全文通过理论讲解与代码实践相结合的方式,帮助读者深入理解这两种基础数据结构的特点与应用。