Java实现栈与队列

在阅读本篇文章之前,建议读者优先阅读专栏内前面的文章。

目录

前言

一、栈:

二、栈的应用场景:

三、栈、虚拟机栈、栈帧:

四、队列:

五、特殊队列:

六、队列与栈的相互转化:

总结


前言

本篇文章主要介绍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 + " ");
}
}

第三个重要的应用就是可以用来实现括号的匹配,可以访问下面的链接:

20. 有效的括号 - 力扣(LeetCode)

这个就是典型的用栈来解决的问题,具体的情况可能出现下面几种:

读者可以先自行实现一下,我这里给出我的代码:

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;
    }
}

第四个重要应用就是可以用于逆波兰式求值,可以访问下面的链接:

150. 逆波兰表达式求值 - 力扣(LeetCode)

具体关于逆波兰式的相关信息可以访问题目中的百科链接,我这里给出我的代码:

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属性进行记录,第二种是保留一个固定的空位,第三种则是使用变量进行标记。我们常用的就是下面这种方法:

我们可以访问下面的链接,来更好的体会循环队列:

622. 设计循环队列 - 力扣(LeetCode)

读者可以先行实现,我这里给出我的代码:

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题目示例,展示了如何用队列实现栈和用栈实现队列的相互转换方法,提供了完整的代码实现。全文通过理论讲解与代码实践相结合的方式,帮助读者深入理解这两种基础数据结构的特点与应用。