栈(Stack)和队列(Queue)

文章目录

  • 前言
  • 1. 栈(Stack)
    • 1.1 什么是栈
    • 1.2 栈的常用操作
    • 1.3 栈的模拟实现
    • 1.4 栈的应用场景
      • 1.4.1 元素序列处理
      • 1.4.2 字符串反转
      • 1.4.3 括号匹配
      • 1.4.4 逆波兰表达式求值
      • 1.4.5 栈的压入、弹出序列
      • 1.4.6 最小栈
      • 1.4.7 递归转循环
    • 1.5 概念区分
      • 1.5.1 数据结构中的栈
      • 1.5.2 JVM中的虚拟机栈
      • 1.5.3 栈帧
      • 1.5.4 三者关系
  • 2. 队列(Queue)
    • 2.1 队列简介
    • 2.2 队列的使用
    • 2.3 自定义队列实现
    • 2.4 循环队列
      • 2.4.1 循环队列原理
      • 2.4.2 区分队空与队满
      • 2.4.3 循环队列实现
  • 3. 双端队列(Deque)
    • 3.1 双端队列概述
    • 3.2 双端队列操作
      • 3.2.1 创建双端队列
      • 3.2.2 常用方法
        • 插入操作
        • 删除操作
        • 查看操作
      • 3.2.3 双端队列的使用示例
    • 3.3 双端队列的实现

前言

本文基于课堂课件与个人学习体会,将全面(也许吧)讲解Java中栈和队列这两种经典数据结构的原理、实现和应用。内容如有不足,欢迎指正与交流。

1. 栈(Stack)

1.1 什么是栈

是一种特殊的线性表,其特点是仅允许在一端(称为栈顶)进行插入和删除操作。不允许操作的另一端称为栈底。栈遵循**后进先出LIFO(Last In First Out)**原则,就像一摞盘子,只能从顶部拿取或放置。

栈的基本操作包括:

  • 压栈(Push):将新元素添加到栈顶
  • 出栈(Pop):移除栈顶元素并返回

在这里插入图片描述

1.2 栈的常用操作

方法功能
Stack()构造一个空的栈
E push (E e)将e压栈,并返回e
E pop()将栈顶元素出栈并返回
E peek()获取栈顶元素(不出栈)
int size()获取栈中有效元素个数
boolean empty()检查栈是否为空

下面是一个使用Java标准库中Stack类的简单示例:

public static void main(String[] args) {// 创建整型栈Stack<Integer> s = new Stack<>();// 依次入栈1、2、3、4s.push(1);s.push(2);s.push(3);s.push(4);System.out.println(s.size());  // 输出: 4System.out.println(s.peek());  // 输出: 4s.pop();  // 4出栈,栈中剩余1、2、3System.out.println(s.pop());  // 输出: 3,栈中剩余1、2// 判断栈是否为空if(s.empty()) {System.out.println("栈空");} else {System.out.println(s.size());  // 输出: 2}
}

1.3 栈的模拟实现

在Java中,Stack类继承自Vector类。Vector和ArrayList类似,都是动态顺序表,区别在于Vector是线程安全的。

在这里插入图片描述

我们自己实现一个栈类,加深对栈操作的理解:

package Stack;import java.util.Arrays;public class MyStack {// 存储栈中元素的数组private int[] elem;// 当前栈中有效元素个数private int usedSize;// 构造一个默认容量为10的栈public MyStack() {this.elem = new int[10];}// 入栈操作public void push(int val) {// 栈满则扩容if (isFull()) {this.elem = Arrays.copyOf(elem, 2 * elem.length);}elem[usedSize++] = val;}// 判断栈是否已满public boolean isFull() {return usedSize == elem.length;}// 出栈操作public int pop() {if (isEmpty()) {throw new EmptyStackException();}int val = elem[usedSize-1];usedSize--;return val;}// 获取栈顶元素但不删除public int peek() {if (isEmpty()) {throw new EmptyStackException();}return elem[usedSize-1];}// 判断栈是否为空public boolean isEmpty() {return 0 == usedSize;}
}

为了处理空栈操作的异常情况,定义一个异常类:

package Stack;// 自定义空栈异常类
public class EmptyStackException extends RuntimeException {public EmptyStackException() {}public EmptyStackException(String message) {super(message);}
}

我们还可以编写一个简单的测试类来验证我们的栈实现:

package Stack;public class Test {public static void main(String[] args) {// 创建自定义栈MyStack stack = new MyStack();// 测试入栈stack.push(10);stack.push(20);stack.push(30);stack.push(40);// 查看栈顶元素System.out.println("栈顶元素: " + stack.peek());  // 输出: 40// 测试出栈System.out.println("出栈元素: " + stack.pop());  // 输出: 40System.out.println("出栈元素: " + stack.pop());  // 输出: 30// 检查栈大小System.out.println("栈中元素个数: " + stack.usedSize);  // 输出: 2// 测试栈是否为空System.out.println("栈是否为空: " + stack.isEmpty());  // 输出: false// 清空栈stack.pop();  // 出栈20stack.pop();  // 出栈10// 验证栈空System.out.println("栈是否为空: " + stack.isEmpty());  // 输出: true// 测试空栈异常try {stack.pop();} catch (EmptyStackException e) {System.out.println("捕获到空栈异常");}}
}

1.4 栈的应用场景

栈在编程中有很多实用场景,这些例子都能很好地展示栈"后进先出"的特性。下面我们看几个经典应用,这些也是面试中的常客哦。

1.4.1 元素序列处理

栈的LIFO特性让它非常适合处理需要"反向"或"回溯"的问题,比如:

  1. 字符串反转 - 正向读取,反向输出
  2. 括号匹配检查 - 左括号入栈,右括号检查匹配
  3. 表达式求值 - 处理中缀、后缀表达式
  4. 栈序列验证 - 检查出栈序列是否有效
  5. 最小栈设计 - 追踪栈中的最小值

1.4.2 字符串反转

字符串反转是栈的入门应用,思路很直接:把字符依次压入栈中,然后弹出来,就自然形成了反序。

public static String reverseString(String str) {Stack<Character> stack = new Stack<>();// 将字符逐个入栈for (int i = 0; i < str.length(); i++) {stack.push(str.charAt(i));}// 出栈组成新字符串StringBuilder result = new StringBuilder();while (!stack.empty()) {result.append(stack.pop());}return result.toString();
}

1.4.3 括号匹配

原题链接

题目:给定一个只包含括号的字符串,判断括号是否有效匹配。

解题思路

  1. 遇到左括号就入栈(存起来等待匹配)

  2. 遇到右括号就检查栈顶的左括号是否匹配

    • 匹配就弹出栈顶元素,继续处理下一个字符
    • 不匹配或栈为空,直接返回false
  3. 最后检查栈是否为空:

    • 空栈:所有左括号都找到了匹配,返回true
    • 非空:有左括号没找到匹配,返回false

其实就像给括号"找对象",每个左括号都要找到自己匹配的右括号才行。

算法流程

遍历字符串s的每个字符
是左括号?
压入栈中
栈为空?
返回false
弹出栈顶元素
弹出元素与当前右括号匹配?
返回false
遍历完成
栈为空?
返回true
返回false

实现代码

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 top = stack.peek();// 检查匹配if ((ch == ')' && top == '(') || (ch == '}' && top == '{') || (ch == ']' && top == '[')) {stack.pop(); // 匹配成功,弹出左括号} else {return false; // 匹配失败}}}// 栈空则所有括号都匹配完成return stack.isEmpty();
}

1.4.4 逆波兰表达式求值

原题链接

题目:给定一个后缀表达式(逆波兰表达式),求其结果值。

解题思路

  1. 遇到数字?直接入栈保存起来

  2. 遇到运算符?从栈中弹出两个数字进行计算:

    • 先弹出的是第二个操作数
    • 后弹出的是第一个操作数
    • 计算结果再压回栈中
  3. 表达式处理完后,栈中就只剩下最终结果了

比如 ["2","1","+","3","*"] 的计算过程:

  • 读到 2 和 1,分别入栈,栈内:[2,1]
  • 读到 +,弹出 1 和 2,计算 2+1=3,结果入栈,栈内:[3]
  • 读到 3,入栈,栈内:[3,3]
  • 读到 ,弹出 3 和 3,计算 33=9,结果入栈,栈内:[9]
  • 结束,最终结果为 9

就像是依次处理计算任务,用栈暂存中间结果。

算法流程

遍历表达式中的每个元素
是运算符?
将数字压入栈中
弹出两个操作数
进行相应计算
将结果压回栈中
遍历完成
返回栈顶元素作为结果

实现代码

public int evalRPN(String[] tokens) {Stack<Integer> stack = new Stack<>();for(String token : tokens) {if(isOperator(token)) {// 是运算符,弹出两个操作数计算int b = stack.pop();int a = stack.pop();switch(token) {case "+": stack.push(a + b); break;case "-": stack.push(a - b); break;case "*": stack.push(a * b); break;case "/": stack.push(a / b); break;}} else {// 是操作数,转换为整数并入栈stack.push(Integer.parseInt(token));}}// 最终栈中只有一个元素,即为计算结果return stack.pop();
}private boolean isOperator(String token) {return "+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token);
}

1.4.5 栈的压入、弹出序列

原题链接

题目:给定两个整数序列,第一个为入栈顺序,判断第二个是否是可能的出栈顺序。

解题思路

  1. 准备一个辅助栈,按照给定顺序依次"入栈"
  2. 每次入栈后,都尝试"出栈"操作:
    • 如果栈顶元素等于当前期望的出栈元素,就执行出栈
    • 一直出栈,直到栈顶元素不等于期望出栈元素或栈空
  3. 继续入栈下一个元素,重复上述过程
  4. 最后检查栈是否为空:
    • 空栈:说明所有元素都按照期望序列出栈了,返回true
    • 非空:出栈序列不匹配,返回false

打个比方,就像排队进场又出场,我们看看是否能按照给定的出场顺序安排人员进出。

实现代码

public boolean validateStackSequences(int[] pushed, int[] popped) {// 参数校验if (pushed == null || popped == null || pushed.length != popped.length) {return false;}Stack<Integer> stack = new Stack<>();int popIdx = 0;for (int val : pushed) {// 将当前元素压入栈stack.push(val);// 尝试出栈操作while (!stack.isEmpty() && stack.peek() == popped[popIdx]) {stack.pop();popIdx++;}}// 如果辅助栈为空,说明所有元素都正确匹配了return stack.isEmpty();
}

1.4.6 最小栈

原题链接

题目:设计一个栈,支持常规操作的同时能够在O(1)时间内获取栈中的最小值。

解题思路

这个问题的难点在于:每次出栈后,如何快速知道剩下元素中的最小值?

最直接的思路是用一个辅助栈,专门用来记录当前栈中的最小值:

  1. 主栈:正常保存所有入栈的元素
  2. 辅助栈(最小栈):同步记录每个状态下的最小值
    • 当新元素小于或等于当前最小值时,将新元素也压入辅助栈
    • 当出栈元素等于当前最小值时,辅助栈也要出栈

这样,辅助栈的栈顶始终是当前主栈中的最小值。

举个例子:

  • 依次入栈:5, 2, 6, 1
  • 主栈变化:[5] -> [5,2] -> [5,2,6] -> [5,2,6,1]
  • 辅助栈变化:[5] -> [5,2] -> [5,2] -> [5,2,1]
  • 每一步,辅助栈的栈顶都是当前所有元素中的最小值

实现代码

class MinStack {// 主栈:存储所有元素private Stack<Integer> stack;// 辅助栈:存储最小值private Stack<Integer> minStack;public MinStack() {stack = new Stack<>();minStack = new Stack<>();}public void push(int val) {stack.push(val);// 如果最小栈为空或新元素不大于最小栈栈顶,则将新元素也压入最小栈if (minStack.empty() || val <= minStack.peek()) {minStack.push(val);}}public void pop() {if(!stack.empty()) {// 如果出栈元素是当前最小值,最小栈也要出栈if(stack.peek().equals(minStack.peek())) {minStack.pop();}stack.pop();}}public int top() {if(stack.empty()) {throw new EmptyStackException();}return stack.peek();}public int getMin() {if(minStack.empty()) {throw new EmptyStackException();}return minStack.peek();}
}

1.4.7 递归转循环

栈的另一个重要应用是将递归算法转为迭代形式。递归本质上是利用系统栈来管理函数调用,我们可以用显式的栈来模拟这个过程,从而避免递归可能导致的栈溢出问题。

以逆序打印链表为例:

// 递归方法 - 简洁但有栈溢出风险
void printListReversely(Node head) {if(head != null) {printListReversely(head.next);  // 先递归处理后续节点System.out.print(head.val + " "); // 再处理当前节点}
}// 用栈模拟的迭代方法 - 更安全
void printListReverselyWithStack(Node head) {Stack<Node> stack = new Stack<>();// 将所有节点入栈Node curr = head;while(curr != null) {stack.push(curr);curr = curr.next;}// 出栈并打印while(!stack.isEmpty()) {System.out.print(stack.pop().val + " ");}
}

这两种方法的核心思想相同,但迭代版本使用显式栈管理,可以更好地控制内存使用,避免大数据时可能的栈溢出问题。

1.5 概念区分

在讨论栈时,我们经常会遇到几个相关但不同的概念:数据结构中的栈、JVM中的虚拟机栈以及栈帧。让我们来理清它们之间的关系。

1.5.1 数据结构中的栈

我们前面讨论的栈是一种抽象数据类型,具有后进先出的特性。它的基本操作包括:

  • push:将元素添加到栈顶
  • pop:从栈顶移除元素
  • peek:查看栈顶元素但不移除
  • isEmpty:检查栈是否为空

Java中通过java.util.Stack类提供了这种数据结构的实现。

1.5.2 JVM中的虚拟机栈

Java虚拟机栈(JVM Stack)是Java虚拟机内存模型的一部分,为Java程序的运行提供服务。它具有以下特点:

  • 线程私有:每个线程在创建时都会创建一个虚拟机栈
  • 生命周期:与线程的生命周期相同
  • 作用:存储方法执行过程的各种数据

JVM栈可能出现的异常:

  • StackOverflowError:当请求的栈深度超过虚拟机允许的最大深度时
  • OutOfMemoryError:当栈动态扩展时无法申请到足够的内存时
JVM内存结构
虚拟机栈
方法区
程序计数器
本地方法栈
线程1的栈
线程2的栈
栈帧1
栈帧2
栈帧3

1.5.3 栈帧

栈帧(Stack Frame)是虚拟机栈中的基本单位,对应一次方法调用所需的内存空间。当方法被调用时,一个新的栈帧被创建并压入当前线程的虚拟机栈;当方法执行完成后,栈帧被弹出。

栈帧的主要组成部分:

  1. 局部变量表:存储方法参数和局部变量
  2. 操作数栈:存储操作的中间结果
  3. 动态链接:指向运行时常量池的方法引用
  4. 方法返回地址:记录方法执行完后的返回位置
  5. 附加信息:如调试信息

举个例子:

public static int add(int a, int b) {int result = a + b;return result;
}public static void main(String[] args) {int x = 5;int y = 10;int sum = add(x, y);System.out.println(sum);
}

当执行到add(x, y)时,会创建一个新的栈帧,包含参数a和b以及局部变量result。当add方法返回后,这个栈帧被销毁,控制权回到main方法的栈帧。

1.5.4 三者关系

  1. 数据结构栈与JVM栈

    • 数据结构栈是一种抽象概念
    • JVM栈是具体的内存区域实现
  2. JVM栈与栈帧

    • JVM栈是容器,包含多个栈帧
    • 栈帧是元素,每个方法调用对应一个栈帧
  3. 它们的共同点

    • 都遵循后进先出(LIFO)原则
    • 都用于管理程序执行的状态和数据

2. 队列(Queue)

2.1 队列简介

队列是一种特殊的线性表,它只允许在一端(队尾)进行插入操作,在另一端(队头)进行删除操作。队列遵循**先进先出FIFO(First In First Out)**原则,就像排队买票一样,先到的人先服务。

队列的基本操作包括:

  • 入队(Enqueue):在队尾添加元素
  • 出队(Dequeue):从队头移除元素

在这里插入图片描述

2.2 队列的使用

在Java中,Queue是一个接口,定义了队列数据结构的标准操作。LinkedList是实现Queue接口的常用类,它通过链表实现了队列功能。

在这里插入图片描述

Queue接口定义的主要方法:

方法功能
boolean offer(E e)将元素添加到队尾
E poll()移除并返回队头元素
E peek()获取队头元素但不移除
int size()获取队列中元素个数
boolean isEmpty()检查队列是否为空

下面是一个使用Java队列的示例:

public static void main(String[] args) {// 创建队列Queue<Integer> queue = new LinkedList<>();// 添加元素到队列queue.offer(1);queue.offer(2);queue.offer(3);queue.offer(4);queue.offer(5);// 查看队列信息System.out.println("队列大小: " + queue.size());     // 输出:5System.out.println("队头元素: " + queue.peek());     // 输出:1// 移除队头元素queue.poll();  // 移除元素1System.out.println("出队后队头: " + queue.poll());   // 输出:2// 检查队列状态if(queue.isEmpty()) {System.out.println("队列为空");} else {System.out.println("队列剩余元素: " + queue.size());  // 输出:3}
}

2.3 自定义队列实现

队列可以通过数组或链表实现。链表实现更为灵活,特别是在需要频繁调整队列大小的场景下。下面我们使用双向链表实现一个简单的队列:
在这里插入图片描述

/*** 基于双向链表的队列实现*/
public class MyQueue {// 链表节点类static class ListNode {int val;           // 节点值ListNode prev;     // 前驱节点ListNode next;     // 后继节点ListNode(int val) {this.val = val;}}private ListNode first;    // 队头指针private ListNode last;     // 队尾指针private int size;          // 队列大小// 入队操作public void offer(int val) {ListNode newNode = new ListNode(val);if (isEmpty()) {// 空队列,新节点同时是队头和队尾first = last = newNode;} else {// 非空队列,在尾部添加节点last.next = newNode;newNode.prev = last;last = newNode;}size++;}// 出队操作public int poll() {if (isEmpty()) {throw new IllegalStateException("队列为空");}int value = first.val;if (first == last) {// 队列只有一个元素first = last = null;} else {// 移除队头first = first.next;first.prev = null;}size--;return value;}// 查看队头元素public int peek() {if (isEmpty()) {throw new IllegalStateException("队列为空");}return first.val;}// 判断队列是否为空public boolean isEmpty() {return first == null;}// 获取队列大小public int size() {return size;}
}

2.4 循环队列

循环队列是首尾相连的队列,通常使用数组实现。它能有效解决普通队列在使用数组实现时可能面临的"假溢出"问题(数组空间未满但无法添加新元素)。

2.4.1 循环队列原理

循环队列使用两个指针:front(指向队头元素)和rear(指向队尾元素的下一个位置)。当指针到达数组末尾时,它会"绕回"到数组开头,形成一个逻辑上的环形结构。

在这里插入图片描述

在循环队列中,下标计算采用取模运算:

  • 向后移动:newIndex = (oldIndex + 1) % array.length
  • 向前移动:newIndex = (oldIndex + array.length - 1) % array.length

2.4.2 区分队空与队满

在循环队列实现中,一个核心问题是如何区分队列空和队列满,因为在这两种情况下front和rear可能指向相同位置。常用的解决方案有:

  1. 牺牲一个存储单元:保留一个空位置作为队满标志

    • 队空条件:front == rear
    • 队满条件:(rear + 1) % capacity == front
  2. 使用计数器:维护一个size变量记录元素个数

    • 队空条件:size == 0
    • 队满条件:size == capacity
  3. 使用状态标志:额外使用一个布尔变量标记队列状态

2.4.3 循环队列实现

下面是一个基于数组实现的循环队列:

/*** 数组实现的循环队列*/
class MyCircularQueue {private int front;    // 队头指针private int rear;     // 队尾指针private int[] elem;   // 存储队列元素的数组public MyCircularQueue(int k) {// 多分配一个空间用于区分队空和队满elem = new int[k + 1];front = rear = 0;}// 入队操作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;}// 队尾元素位于rear-1位置int index = (rear - 1 + elem.length) % elem.length;return elem[index];}// 检查队列是否为空public boolean isEmpty() {return front == rear;}// 检查队列是否已满public boolean isFull() {return (rear + 1) % elem.length == front;}
}

循环队列实现中的关键点:

  • 实际可用空间为k,而数组长度为k+1(牺牲一个位置区分队空队满)
  • 入队时,先存值再移动rear指针
  • 出队时,只需移动front指针
  • 队满条件是rear的下一个位置等于front
  • 计算队尾元素位置需要考虑rear为0的情况

3. 双端队列(Deque)

3.1 双端队列概述

双端队列(Deque)是一种允许在两端进行插入和删除操作的队列。名称"Deque"是"Double-Ended Queue"的缩写。它结合了栈和队列的特点,使得数据操作更加灵活。

在这里插入图片描述

在Java中,Deque是一个接口,扩展了Queue接口。常用的实现类包括:

  • LinkedList:基于链表实现
  • ArrayDeque:基于数组实现(通常比LinkedList更高效)

在这里插入图片描述

3.2 双端队列操作

3.2.1 创建双端队列

Java提供了两种常见的双端队列实现:

// 基于数组的实现,适用于大多数场景
Deque<Integer> arrayDeque = new ArrayDeque<>();// 基于链表的实现,适用于频繁的插入删除操作
Deque<Integer> linkedDeque = new LinkedList<>();

3.2.2 常用方法

双端队列支持在两端进行元素操作,下面是主要方法分类:

插入操作
方法描述队列满时行为
addFirst(E e)在队头添加元素抛出异常
addLast(E e)在队尾添加元素抛出异常
offerFirst(E e)在队头添加元素返回false
offerLast(E e)在队尾添加元素返回false
push(E e)在队头添加元素(同addFirst)抛出异常
add(E e)在队尾添加元素(同addLast)抛出异常
删除操作
方法描述队列空时行为
removeFirst()移除并返回队头元素抛出异常
removeLast()移除并返回队尾元素抛出异常
pollFirst()移除并返回队头元素返回null
pollLast()移除并返回队尾元素返回null
pop()移除并返回队头元素(同removeFirst)抛出异常
poll()移除并返回队头元素(同pollFirst)返回null
查看操作
方法描述队列空时行为
getFirst()获取队头元素但不移除抛出异常
getLast()获取队尾元素但不移除抛出异常
peekFirst()获取队头元素但不移除返回null
peekLast()获取队尾元素但不移除返回null
peek()获取队头元素但不移除(同peekFirst)返回null

3.2.3 双端队列的使用示例

public static void main(String[] args) {// 创建双端队列Deque<String> deque = new LinkedList<>();// 从两端添加元素deque.addFirst("开头");deque.offerFirst("最前面");deque.addLast("结尾");deque.offerLast("最后面");// 查看队列内容System.out.println(deque);  // 输出: [最前面, 开头, 结尾, 最后面]// 不删除元素查看两端System.out.println("队头: " + deque.peekFirst());  // 输出: 最前面System.out.println("队尾: " + deque.peekLast());   // 输出: 最后面// 删除并返回元素System.out.println("删除队头: " + deque.pollFirst());  // 输出: 最前面System.out.println("删除队尾: " + deque.pollLast());   // 输出: 最后面// 删除后的队列System.out.println(deque);  // 输出: [开头, 结尾]// 使用栈方法(LIFO)deque.push("新头部");            // 在队头添加System.out.println(deque);       // 输出: [新头部, 开头, 结尾]System.out.println(deque.pop()); // 输出: 新头部// 使用队列方法(FIFO)deque.offer("新结尾");           // 在队尾添加System.out.println(deque);       // 输出: [开头, 结尾, 新结尾]System.out.println(deque.poll()); // 输出: 开头
}

3.3 双端队列的实现

双端队列可以通过链表或数组来实现,Java中的LinkedList和ArrayDeque分别代表这两种实现方式。下面我们来看一个基于双向链表的简单实现:

/*** 双向链表实现的双端队列*/
public class MyLinkedDeque<E> {// 节点类,包含元素值和前后引用private static class Node<E> {E item;Node<E> prev;Node<E> next;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.prev = prev;this.next = next;}}private Node<E> first;    // 队列头节点private Node<E> last;     // 队列尾节点private int size;         // 元素个数// 从队头添加元素public void addFirst(E e) {Node<E> f = first;Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null) {// 如果队列为空,新节点同时是尾节点last = newNode;} else {// 更新原头节点的前驱f.prev = newNode;}size++;}// 从队尾添加元素public void addLast(E e) {Node<E> l = last;Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null) {// 如果队列为空,新节点同时是头节点first = newNode;} else {// 更新原尾节点的后继l.next = newNode;}size++;}// 移除并返回队头元素public E removeFirst() {if (isEmpty()) {throw new NoSuchElementException("队列为空");}E element = first.item;Node<E> next = first.next;// 释放资源,帮助垃圾回收first.item = null;first.next = null;first = next;if (next == null) {// 队列变空last = null;} else {next.prev = null;}size--;return element;}// 移除并返回队尾元素public E removeLast() {if (isEmpty()) {throw new NoSuchElementException("队列为空");}E element = last.item;Node<E> prev = last.prev;// 释放资源last.item = null;last.prev = null;last = prev;if (prev == null) {// 队列变空first = null;} else {prev.next = null;}size--;return element;}// 获取队头元素但不移除public E peekFirst() {if (isEmpty()) {return null;}return first.item;}// 获取队尾元素但不移除public E peekLast() {if (isEmpty()) {return null;}return last.item;}// 检查队列是否为空public boolean isEmpty() {return size == 0;}// 返回队列大小public int size() {return size;}// 清空队列public void clear() {// 清理所有节点,帮助垃圾回收for (Node<E> x = first; x != null; ) {Node<E> next = x.next;x.item = null;x.next = null;x.prev = null;x = next;}first = last = null;size = 0;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder("[");Node<E> x = first;if (x != null) {sb.append(x.item);x = x.next;while (x != null) {sb.append(", ").append(x.item);x = x.next;}}return sb.append("]").toString();}
}

双向链表实现的双端队列有以下优点:

  • 两端操作均为O(1)时间复杂度
  • 无需预先指定容量,可动态扩展
  • 实现直观,易于理解

主要缺点:

  • 每个元素需要额外空间存储前后节点引用
  • 内存分配不连续,可能影响缓存效率

相比之下,数组实现的双端队列(如ArrayDeque)在连续内存访问方面有优势,但需要处理数组扩容和循环索引计算等问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/87566.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/87566.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5G MEC四大核心挑战技术解析报告

一、MEC园区部署挑战:数据本地化与低时延接入 痛点深度解析 数据不出园区:工业质检、医疗影像等敏感业务需数据在本地闭环处理。但运营商基站与企业MEC间若经公网绕行,时延超50ms且存在泄露风险。L2网络局限:传统L2接入网无法实现基站→UPF的智能路由,导致业务流绕行城域…

【硬核拆解】英伟达Blackwell芯片架构如何重构AI算力边界?

前言 前些天发现了一个巨牛的人工智能免费学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站 一、Blackwell诞生的算力危机&#xff08;2025现状&#xff09; graph TD A[2025年AI算力需求] --> B[千亿参数模型训练能耗…

【深度学习模块】图像的相对位置编码

这个是一个常用的模块&#xff0c;就是我们可以对输入的特征嵌入位置编码。 位置编码&#xff08;Positional Encoding&#xff09;是一种将空间位置信息嵌入到特征中的方法&#xff0c;通常用于帮助模型更好地理解特征的空间关系。 这里介绍的这个是相对位置编码&#xff0c;…

osg加入实时光照SilverLining 天空和3D 云

OSG系列文章目录 文章目录 OSG系列文章目录一、前言官网的介绍&#xff1a; 二、编译官网例子 一、前言 osg本身也可以加入动态云&#xff0c;但是效果有点差强人意&#xff0c;这里我们使用sundog公司的动态云&#xff1a;SilverLining 天空和 3D 云。 官网的介绍&#xff1…

spring-ai-alibaba 1.0.0.2 学习(十二)——聊天记忆扩展包

学习spring-ai时提到过&#xff0c;spring-ai除了内置的InMemoryChatMemoryRepository&#xff0c;还提供jdbc、cassandra、neo4j三个扩展包。 而spring-ai-alibaba则提供了jdbc、redis、elasticsearch三个扩展包。 两者都提供了jdbc扩展包&#xff0c;有什么区别呢&#xff…

c语言-指针(数组)练习2

题目&#xff1a;将数组中n个元素按逆序存放并打印出来&#xff0c;使用函数封装与指针 思路&#xff1a; 1.定义一个数组arr[5]和用于存放数组大小&#xff08;数组大小通过sizeof关键字来进行计算&#xff09;的变量len&#xff1b; 2.创建三个函数initArr、printArr、rev…

Redis服务器

Redis&#xff0c;一款Key-Value型内存数据库 常用于网站开发场景 Redis服务器只发布了Linux版本 Redis服务器安装&#xff0c;2种办法 自动安装 apt install redis-server手动编译安装 从官网下载源码&#xff0c;编译&#xff0c;部署 1 安装redis apt install redis-s…

LeetCode 第91题:解码方法

题目描述&#xff1a; 一条包含字母A-Z的消息通过以下映射进行了编码 1-A ...... 26-Z 要特别注意&#xff0c;11106可以映射为AAJF或KJF 06不是一个合法编码 给你一个只含数字的非空字符串s&#xff0c;请计算并返回解码方法的总数。如果没有合法的方法解码整个字符串&#xf…

Rocky Linux 9 源码包安装Mysql8

Rocky Linux 9 源码包安装Mysql8 大家好我是星哥&#xff0c;之前介绍了&#xff0c;Rocky Linux 9 源码包安装Mysql5.7。 本文将介绍如何在Rocky Linux 9操作系统上&#xff0c;从源码一步步安装MySQL 8&#xff0c;为您提供一个稳定、高效且可控的数据库解决方案。 为什么…

AI小智项目全解析:软硬件架构与开发环境配置

AI小智项目全解析&#xff1a;软硬件架构与开发环境配置 一、项目整体架构 AI小智是一款基于ESP32的智能物联网设备&#xff0c;集成了语音交互、边缘计算等功能。整体系统架构如下&#xff1a; 终端设备&#xff1a;ESP32模组作为核心通信方式&#xff1a; WebSocket实现实…

设计模式之上下文对象设计模式

目录 一、模式介绍 二、架构设计 三、Demo 示例 四、总结 一、模式介绍 上下文对象&#xff08;Context Object&#xff09;模式 最早由《Core J2EE Patterns》第二版提出&#xff0c;其核心目标是在多层或多组件间共享与当前作用域&#xff08;如一次请求、一次会话、一次…

@Linux服务器加域退域

文章目录 **一、加入Active Directory域****1. 准备工作****2. 配置步骤****步骤1&#xff1a;验证网络和DNS****步骤2&#xff1a;发现域****步骤3&#xff1a;加入域****步骤4&#xff1a;配置SSSD&#xff08;可选&#xff09;****步骤5&#xff1a;配置sudo权限&#xff08…

鸿蒙系统(HarmonyOS)4.2 设备上实现无线安装 APK 并调试

在鸿蒙系统&#xff08;HarmonyOS&#xff09;4.2 设备上实现无线安装 APK 并调试的步骤与 Android 类似&#xff0c;但需注意鸿蒙系统的特殊设置。以下是详细操作指南&#xff1a; 鸿蒙系统特殊准备 开启开发者选项&#xff1a; - 设置 > 关于手机 > 连续点击"H…

MyBatis时间戳查询实战指南

在 MyBatis 中通过时间戳&#xff08;Timestamp&#xff09;作为查询条件&#xff0c;需注意数据库时间类型与 Java 类型的映射。以下是具体实现方式&#xff1a; 一、Java 实体类与数据库字段映射 实体类定义 使用 java.sql.Timestamp 或 java.time.LocalDateTime&#xff08;…

【Verilog硬件语言学习笔记4】FPGA串口通信

串口通信是系统设计中比较基部分&#xff0c;其原理其实也很通俗易懂。单次建立通信会传输8个bit&#xff0c;其时序也很简单&#xff0c;这里就不再赘述了。其对应的实例代码如下所示&#xff1b; 首先是接受部分&#xff08;因为我的变量命名也很规范&#xff0c;通俗易懂&a…

Go 语言安装教程(Windows 系统)

2025年07月02日 准备工作 确认系统为 Windows 7 及以上版本&#xff08;推荐 Windows 10/11&#xff09;。64 位系统选择 amd64 版本安装包&#xff0c;32 位系统选择 386 版本。确保安装目录&#xff08;默认 C:\Program Files\Go\&#xff09;有至少 1GB 空间。 下载安装包…

接口测试之postman

一、Postman功能简介 3天精通Postman接口测试&#xff0c;全套项目实战教程&#xff01;&#xff01; Postman是由Postdot Technologies公司打造的一款功能强大的调试HTTP接口的工具。在做接口测试的时候&#xff0c;Postman相当于一个客户端&#xff0c;它可以模拟用户发起的各…

【记录】Ubuntu安装Mysql

本文记录Ubuntu系统下安装Mysql 1 查看系统信息 lsb_release -a 2 使用apt下载安装Mysql 1 打开终端,首先更新你的系统包索引,以确保所有包都是最新的 sudo apt update 2 安装mysql服务器 sudo apt install mysql-server (也可以选择对应的mysql-server 版本) 3 查看mysql状…

【深度学习:进阶篇】--4.1.循环神经网络(改进)

RNN存在的问题&#xff1a;梯度爆炸&#xff0c;长期依赖参数量过大等问题 目录 1.GRU(门控循环单元) 1.1.什么是GRU 1.2.直观理解 1.3.本质解决问题 2.LSTM(长短记忆网络) 2.1.作用 3.结构扩展与效率优化​ 1.GRU(门控循环单元) 2014年&#xff0c;出现的算法&#x…

中心化钱包安全方案

先来看独立的密钥安全技术 1 自建或单租户 CloudHSM 优点&#xff1a;密钥永不出硬件&#xff0c;无法导出&#xff0c;只能对外提供公钥。 交易时&#xff0c;外部应用把消息哈希传进去签名&#xff0c;再把签好名的结果拿出来用。 这种方式安全性拉满&#xff0c;但成本高、…