栈 与 队列

        在数据结构中,栈和队列是两种极具实用性的特殊线性表。它们在遵循线性表基本存储特性的同时,对元素的插入与删除操作施加了严格的位置限制,这一特性使其在算法设计、JVM 内存管理、操作系统任务调度等场景中发挥着不可替代的作用。

一、栈(Stack)

1.1 栈的核心定义与特性

        栈是一种仅允许在固定一端进行插入和删除操作的线性表,该操作端被称为栈顶,另一端则为栈底。其最核心的特性是后进先出(LIFO,Last In First Out) —— 最后进入栈的元素,会成为第一个被弹出的元素。

  • 压栈(入栈):栈的插入操作,始终在栈顶执行,新元素入栈后会成为新的栈顶,栈内元素个数随之增加。
  • 出栈:栈的删除操作,同样在栈顶执行,栈顶元素弹出后,其下方的元素会成为新的栈顶,栈内元素个数减少。

        举个直观例子:若依次将 1、2、3、4 压栈,栈内元素从栈底到栈顶的顺序为 [1,2,3,4];此时执行出栈操作,会先弹出 4,再弹出 3,后续依次为 2、1,完全遵循 “后进先出” 规则。

1.2 栈的常用方法与使用规范

        在 Java 中,Stack类封装了栈的核心操作,通过这些方法可便捷地完成栈的创建、元素操作及状态查询,具体方法及功能如下:

方法名称功能描述
Stack()构造一个空栈,初始状态下无任何元素
push(E e)将元素e压入栈顶,操作成功后返回该元素e
pop()弹出栈顶元素并返回,若栈为空会抛出异常,操作后栈内有效元素个数减少 1
peek()获取栈顶元素(仅读取不弹出),栈结构保持不变,栈为空时会抛出异常
size()返回栈内当前有效元素的个数,结果为非负整数
empty()判断栈是否为空,若栈内无元素返回true,否则返回false

        需要注意的是,Java 中的Stack类继承自Vector,而Vector是线程安全的动态顺序表,因此Stack的所有操作也具备线程安全特性,适合多线程场景下使用。

1.3 栈的模拟实现逻辑

        若需手动模拟栈的实现,核心是选择数组作为底层存储结构(数组在随机访问和操作效率上更优),并通过size变量记录栈内有效元素个数,同时处理数组扩容问题,具体步骤如下:

  1. 初始化:创建一个固定长度的数组(如初始长度为 3),size初始化为 0,代表栈为空。
  2. 压栈(push):先检查数组是否已满(size == 数组长度),若已满则通过Arrays.copyOf()将数组长度翻倍(实现动态扩容);再将元素存入array[size],最后size++
  3. 出栈(pop):先调用peek()获取栈顶元素(即array[size-1]),再将size--(通过逻辑删除实现元素出栈,无需真正清空数组元素),最后返回栈顶元素。
  4. 获取栈顶(peek):先判断栈是否为空(size == 0),若为空则抛出 “栈空” 异常,否则返回array[size-1]的值。

1.4 栈的典型应用场景

        栈的 “后进先出” 特性使其在多个领域中成为关键工具,常见应用场景包括:

  1. 元素序列判断:给定入栈序列,判断某一出栈序列是否合法。例如入栈序列为 1,2,3,4,选项 “3,1,4,2” 是不可能的 —— 因为 3 出栈后,栈内剩余元素为 1,2,下一个出栈元素只能是 2,而非 1。
  2. 递归转循环:递归本质是依赖虚拟机栈保存函数调用状态,若递归深度过大易导致栈溢出。此时可手动用栈模拟递归,例如逆序打印链表:先将链表节点依次压栈,再弹出节点并打印,即可实现逆序效果。
  3. 其他高频场景:还包括括号匹配(用栈存储左括号,遇到右括号时弹出栈顶匹配)、逆波兰表达式求值(用栈存储操作数,遇到运算符时弹出两个数计算)、最小栈(额外维护一个栈存储当前最小值,实现 O (1) 时间获取最小值)等。

1.5 概念辨析:栈、虚拟机栈、栈帧

        很多学习者会混淆这三个概念,实则它们分属不同层面,具体区别如下:

  • :数据结构层面,是用于存储数据的容器,遵循 LIFO 原则,可独立于编程语言存在。
  • 虚拟机栈:JVM 内存区域层面,是 JVM 为每个线程分配的内存空间,用于存储方法调用时的局部变量、操作数栈等信息。
  • 栈帧:虚拟机栈的基本组成单位,每个方法调用时会创建一个栈帧,方法执行完毕后栈帧出栈,释放内存。

二、队列(Queue)

2.1 队列的核心定义与特性

        队列与栈相反,是一种 “两端操作受限” 的线性表:仅允许在队尾(Tail/Rear) 进行插入操作(入队),在队头(Head/Front) 进行删除操作(出队),核心特性是先进先出(FIFO,First In First Out) —— 最早入队的元素,会最早出队。

  • 入队(Enqueue):在队尾添加元素,新元素入队后成为新的队尾,队列长度增加 1。
  • 出队(Dequeue):在队头删除元素,原队头的下一个元素成为新的队头,队列长度减少 1。

        例如:依次将 1,2,3,4 入队,队列内顺序为 [1,2,3,4];出队时先弹出 1,再弹出 2,后续依次为 3、4,完全符合 “先进先出” 规则。

2.2 队列的常用方法与使用规范

        在 Java 中,Queue是一个接口(而非类),底层通常通过链表实现(链表可避免数组 “假溢出” 问题),实例化时需使用其实现类(如LinkedList)。Queue的核心方法及功能如下:

方法名称功能描述
offer(E e)将元素e入队(添加到队尾),操作成功返回true,失败返回false
poll()出队(删除并返回队头元素),若队列为空返回null,而非抛出异常
peek()获取队头元素(仅读取不删除),队列为空时返回null
size()返回队列内当前有效元素的个数,结果为非负整数
isEmpty()判断队列是否为空,若队内无元素返回true,否则返回false

        相较于Stack类,Queue接口的方法设计更贴合队列特性,且poll()peek()在队空时返回null,避免了异常抛出,使用更灵活。

2.3 队列的模拟实现逻辑

        队列的底层实现推荐使用双向链表(双向链表在队头删除和队尾插入操作上效率更高),核心是维护first(队头指针)、last(队尾指针)和size(元素个数)三个属性,具体实现步骤如下:

  1. 节点设计:双向链表节点包含value(存储元素值)、prev(前驱指针,指向前一个节点)、next(后继指针,指向后一个节点)。
  2. 入队(offer):若队列为空(first == null),则新节点既是队头也是队尾;否则在队尾(last)后添加新节点,更新last为新节点,最后size++
  3. 出队(poll):若队列为空返回null;若队列只有一个元素(first == last),删除后将firstlast均设为null;否则删除first节点,更新first为其下一个节点,最后size--
  4. 获取队头(peek):若队列为空返回null,否则返回first.value

2.4 循环队列:解决数组假溢出的优化方案

        在实际开发中(如操作系统生产者 - 消费者模型),常使用循环队列(基于数组实现),其核心是让数组下标 “循环利用”,避免传统数组队列的 “假溢出” 问题(即队头有空位但队尾已达数组边界)。

(1)下标循环的实现技巧
  • 下标向后移动(如入队时队尾下标更新):index = (index + offset) % array.length。例如数组长度为 9,当前下标为 7,偏移量为 4,计算结果为(7+4)%9=2,实现下标从 7 “循环” 到 2。
  • 下标向前移动(如出队时队头下标更新):index = (index + array.length - offset) % array.length。例如数组长度为 9,当前下标为 2,偏移量为 4,计算结果为(2+9-4)%9=7,实现下标从 2 “循环” 到 7。
(2)队列空与满的判断方案

        循环队列的关键问题是如何区分 “空” 和 “满”(两者均可能出现rear == front),常见三种解决方案:

  1. 添加size属性size == 0时为队空,size == array.length时为队满。
  2. 保留一个位置:约定 “队尾的下一个位置是队头” 时为队满,即(rear + 1) % array.length == frontrear == front时为队空。
  3. 使用标记变量:通过布尔变量(如isFull)标记队列状态,入队成功后设为true,出队成功后根据情况设为false

2.5 双端队列(Deque):灵活的 “双向操作容器”

        双端队列(Deque,Double Ended Queue)是队列的扩展形式,允许在两端同时进行入队和出队操作—— 既可以从队头入队 / 出队,也可以从队尾入队 / 出队,灵活性远超普通队列。

  • Java 中的 DequeDeque是接口,实例化时可选择LinkedList(链式实现,适合频繁插入删除)或ArrayDeque(线性实现,适合随机访问)。
  • 实际应用场景Deque可同时实现栈和队列的功能 —— 模拟栈时,用addFirst()(压栈)和removeFirst()(出栈);模拟队列时,用addLast()(入队)和removeFirst()(出队),是工程中推荐的替代Stack和普通Queue的方案。

三、栈与队列的相互实现

        栈和队列的相互实现是面试中最常见的题目,核心思路是利用 “两个容器” 的元素转移,实现目标数据结构的特性,具体方案如下:

  1. 用队列实现栈:准备两个队列Q1Q2,入栈时将元素加入非空队列;出栈时,将非空队列中除最后一个元素外的所有元素转移到另一个队列,最后弹出剩余的那个元素(该元素即为栈顶元素)。

class MyStack {
    //定义两个队列作为底层容器
    public Queue<Integer> queue1;
    public Queue<Integer> queue2;

    //初始化队列
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }
    
    //入栈:将元素加入非空队列(若都为空,默认加入queue1)
    public void push(int x) {
        if (!queue1.isEmpty()) {
            queue1.offer(x);
        } else if (!queue2.isEmpty()) {
            queue2.offer(x);
        } else {
            queue1.offer(x);
        }
    }

    //出栈:将非空队列中除最后一个元素外的所有元素转移到另一个队列,弹出剩余元素
    public int pop() {
        if (empty()) {
            return -1;
        }
        if (!queue1.isEmpty()) {
            int size = queue1.size();
            for (int i = 0; i < size - 1; i++) {
                queue2.offer(queue1.poll());
            }
            return queue1.poll();
        } else {
            int size = queue2.size();
            for (int i = 0; i < size - 1; i++) {
                queue1.offer(queue2.poll());
            }
            return queue2.poll();
        }
    }

    //获取栈顶元素(不弹出)
    public int top() {
        if (empty()) {
            return -1;
        }
        if (!queue1.isEmpty()) {
            int size = queue1.size();
            int val = 0;
            for (int i = 0; i < size; i++) {
                val = queue1.poll();
                queue2.offer(val);
            }
            return val;
        } else {
            int size = queue2.size();
            int val = 0;
            for (int i = 0; i < size; i++) {
                val = queue2.poll();
                queue1.offer(val);
            }
            return val;
        }
    }

     //判断栈是否为空
    public boolean empty() {
        return queue1.isEmpty() && queue2.isEmpty();
    }
}
  1. 用栈实现队列:准备两个栈stack1stack2,入队时将元素压入stack1;出队时,若stack2为空,将stack1中所有元素弹出并压入stack2,此时stack2的栈顶即为队列的队头,弹出stack2栈顶元素即可完成出队。

class MyQueue {
    //定义两个栈作为底层容器(stack1用于入队,stack2用于出队)
    public ArrayDeque<Integer> stack1;
    public ArrayDeque<Integer> stack2;
    
    //初始化栈(推荐用Deque的实现类,功能更完善)
    public MyQueue() {
        stack1 = new ArrayDeque<>();
        stack2 = new ArrayDeque<>();
    }

    //入队:直接将元素压入stack1
    public void push(int x) {
        stack1.push(x);
    }

    //出队:若stack2为空,将stack1所有元素转移到stack2,弹出stack2栈顶元素(队头)
    public int pop() {
        if (empty()) {
            return -1;
        }
        //若stack2为空,将stack1元素全部转移到stack2(反转顺序,实现先进先出)
        if (stack2.isEmpty()) {
            while (!stack1.isEmpty()) {
            stack2.push(stack1.pop());
            }
        }
        //弹出stack2栈顶元素(即原队列的队头)
        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();
    }
}

四、总结

        栈和队列虽同为特殊线性表,但核心特性截然不同:栈遵循 “后进先出”,适合需要 “回溯” 的场景(如递归、括号匹配);队列遵循 “先进先出”,适合需要 “有序处理” 的场景(如任务调度、消息队列)。学习时需重点掌握它们的实现逻辑(数组 / 链表)、核心方法及应用场景,而栈与队列的相互实现则是面试中的高频考点。掌握这些知识点,不仅能轻松应对面试,更能在实际开发中根据场景选择更合适的数据结构,提升程序效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值