一、栈(Stack):后进先出的线性结构
1.1 核心概念与操作特性
栈是仅允许在固定一端进行插入和删除操作的特殊线性表,其核心要素与规则如下:
- 关键术语:进行数据操作的一端称为栈顶(Top),另一端称为栈底(Bottom) 。
- 核心原则:遵循后进先出(LIFO,Last In First Out) 规则,即最后进入栈的元素会最先被取出,类似日常生活中叠放的盘子——新盘子放在顶端,取盘子时也从顶端开始 。
- 基础操作:
- 压栈(Push):又称进栈、入栈,指将元素插入栈顶的操作 。
- 出栈(Pop):指将栈顶元素删除并取出的操作,出栈操作仅能在栈顶进行 。
1.2 Java中Stack类的使用方法
Java中的Stack
类提供了栈的完整实现,其核心方法及功能如下表所示 :
方法 | 功能描述 | 返回值/说明 |
---|---|---|
Stack() | 构造一个空的栈 | 空栈对象 |
E push(E e) | 将元素e入栈,操作位置为栈顶 | 返回被入栈的元素e |
E pop() | 移除栈顶元素并返回该元素 | 栈顶元素,若栈为空可能抛出异常 |
E peek() | 获取栈顶元素,但不删除该元素 | 栈顶元素,若栈为空可能抛出异常 |
int size() | 统计栈中有效元素的个数 | 非负整数,代表元素数量 |
boolean empty() | 检测栈是否为空 | 栈空返回true ,否则返回false |
代码示例与执行逻辑:
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1); // 栈内元素:[1]
s.push(2); // 栈内元素:[1,2]
s.push(3); // 栈内元素:[1,2,3]
s.push(4); // 栈内元素:[1,2,3,4]
System.out.println(s.size()); // 输出4(有效元素个数)
System.out.println(s.peek()); // 输出4(栈顶元素为4)
s.pop(); // 4出栈,栈内剩余[1,2,3]
System.out.println(s.pop()); // 输出3(3出栈,栈内剩余[1,2])
if(s.empty()){
System.out.println("栈空");
}else{
System.out.println(s.size()); // 输出2
}
}
上述代码清晰展示了栈的push
、peek
、pop
等操作的执行效果,每一步操作均围绕栈顶进行,严格遵循LIFO原则 。
1.3 栈的底层模拟实现
1.3.1 底层结构选择
从Java集合框架的继承关系可知,Stack
类继承自Vector
类,而Vector
是线程安全的动态顺序表,因此栈的底层本质是基于数组实现的 。基于数组实现栈时,需维护两个核心要素:存储元素的数组array
和记录有效元素个数的size
(同时作为栈顶指针,size-1
即为栈顶元素下标)。
1.3.2 完整实现代码
import java.util.Arrays;
public class MyStack {
int[] array; // 存储栈元素的数组
int size; // 栈中有效元素个数(栈顶指针)
public MyStack() {
array = new int[3];
}
// 压栈:将元素e入栈,返回e
public int push(int e) {
ensureCapacity(); // 确保数组容量充足,不足则扩容
array[size++] = e; // 元素存入栈顶,栈顶指针后移
return e;
}
// 出栈:移除并返回栈顶元素
public int pop() {
int e = peek(); // 先获取栈顶元素(若栈空抛出异常)
size--; // 栈顶指针前移,逻辑删除栈顶元素
return e;
}
// 获取栈顶元素:不删除元素,仅返回值
public int peek() {
if (empty()) { // 检测栈是否为空
throw new RuntimeException("栈为空,无法获取栈顶元素");
}
return array[size - 1]; // 返回栈顶元素(size-1为栈顶下标)
}
// 统计有效元素个数
public int size() {
return size;
}
// 检测栈是否为空
public boolean empty() {
return size == 0;
}
// 动态扩容:当元素个数达到数组容量时,将容量翻倍
private void ensureCapacity() {
if (size == array.length) {
array = Arrays.copyOf(array, size * 2);
}
}
}
核心逻辑说明:
- 扩容机制:通过
ensureCapacity
方法实现动态扩容,当size
等于数组长度时,使用Arrays.copyOf
将数组容量翻倍,避免元素溢出 。 - 空栈处理:
peek
方法中添加空栈检测,若栈为空则抛出运行时异常,保证操作安全性 。
1.4 概念辨析:栈、虚拟机栈、栈帧
三者分属不同层面,极易混淆,文档特别强调了其区别 :
- 栈:本文讨论的数据结构,是一种逻辑结构,用于存储数据并遵循LIFO规则。
- 虚拟机栈:Java虚拟机的内存区域,属于运行时数据区,用于存储方法调用的相关信息。
- 栈帧:虚拟机栈中的基本存储单位,每个方法被调用时会创建一个栈帧,包含局部变量表、操作数栈、动态链接等信息,方法执行完毕后栈帧出栈。
二、队列(Queue):先进先出的线性结构
2.1 核心概念与操作特性
队列是仅允许在一端插入、另一端删除的特殊线性表,其核心要素与规则如下:
- 关键术语:进行插入操作的一端称为队尾(Tail/Rear),进行删除操作的一端称为队头(Head/Front) 。
- 核心原则:遵循先进先出(FIFO,First In First Out) 规则,即先进入队列的元素会最先被取出,类似日常生活中的排队——先排队者先接受服务 。
- 基础操作:
- 入队列(Enqueue):将元素插入队尾的操作 。
- 出队列(Dequeue):将队头元素删除并取出的操作 。
2.2 Java中Queue接口的使用方法
2.2.1 接口特性与实现类
在Java中,Queue
是一个接口,无法直接实例化,其底层通常通过链表实现。由于LinkedList
类实现了Queue
接口,因此实例化时需使用LinkedList
。
2.2.2 核心方法与功能
Queue
接口的核心方法及功能如下表所示 :
方法 | 功能描述 | 返回值/说明 |
---|---|---|
boolean offer(E e) | 将元素e入队列,操作位置为队尾 | 入队成功返回true ,失败返回false |
E poll() | 移除队头元素并返回该元素 | 队头元素,若队为空返回null |
E peek() | 获取队头元素,但不删除该元素 | 队头元素,若队为空返回null |
int size() | 统计队列中有效元素的个数 | 非负整数,代表元素数量 |
boolean isEmpty() | 检测队列是否为空 | 队空返回true ,否则返回false |
2.2.3 代码示例与执行逻辑
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
q.offer(1); // 队尾入队 [1]
q.offer(2); // [1,2]
q.offer(3); // [1,2,3]
q.offer(4); // [1,2,3,4]
q.offer(5); // [1,2,3,4,5]
System.out.println(q.size()); // 5
System.out.println(q.peek()); // 1
q.poll(); // 1出队,[2,3,4,5]
System.out.println(q.poll()); // 2(2出队,[3,4,5])
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println(q.size()); // 3
}
}
上述代码中,入队操作均在队尾进行,出队操作均在队头进行,完全遵循FIFO原则 。
2.3 队列的底层模拟实现
2.3.1 底层结构选择
队列的底层实现可选择顺序结构或链式结构,但链式结构更优 。原因在于:
- 顺序结构的队头删除操作会导致后续所有元素前移,时间复杂度为O(n);
- 链式结构的队头删除、队尾插入操作均可在O(1)时间内完成(维护队头
first
和队尾last
指针实现)。
2.3.2 完整实现代码(基于双向列表)
public class MyQueue {
public static class ListNode {
ListNode next;
ListNode prev;
int value;
ListNode(int value) {
this.value = value;
}
}
ListNode first;
ListNode last;
int size = 0;
// 入队列
public void offer(int e) {
ListNode newNode = new ListNode(e);
if (first == null) {
first = newNode;
} else {
last.next = newNode;
newNode.prev = last;
}
last = newNode;
size++;
}
// 出队列
public Integer poll() {
if (first == null) {
return null;
}
int value = first.value;
if (first == last) {
first = null;
last = null;
} else {
first = first.next;
first.prev.next = null;
first.prev = null;
}
size--;
return value;
}
// 获取队头元素
public Integer peek() {
if (first == null) {
return null;
}
return first.value;
}
// 有效元素个数
public int size() {
return size;
}
// 判空
public boolean isEmpty() {
return first == null;
}
}
2.5 双端队列(Deque):灵活的双向操作结构
2.5.1 概念与特性
双端队列(Deque,全称“double ended queue”)是一种允许在两端进行入队和出队操作的队列,兼具栈和队列的特性 。其操作灵活性体现在:
- 可从队头入队/出队;
- 可从队尾入队/出队。
2.5.2 Java中的Deque接口使用
- 接口与实现类:
Deque
是Java中的接口,使用时需实例化其实现类,常用LinkedList
(链式实现)和ArrayDeque
(线性实现) 。 - 替代栈与队列:在实际工程中,
Deque
的使用频率远高于单独的Stack
和Queue
,因其可灵活实现两种结构的功能 : - 用Deque实现栈(默认操作栈顶,即双端队列的一端)
Deque<Integer> stack = new ArrayDeque<>();
- 用Deque实现队列(默认队尾入、队头出)
Deque<Integer> queue = new LinkedList<>();
三、总结
栈和队列作为两种约束性线性结构,其核心差异在于操作规则:栈遵循LIFO,适合处理“后进先出”的场景(如递归转化、括号匹配);队列遵循FIFO,适合处理“先进先出”的场景(如任务调度、缓冲区管理) 。
从实现角度看,栈的底层可基于数组或链表,Java中Stack
类继承自Vector
(数组实现);队列的底层优先选择链表,Java中Queue
接口通过LinkedList
实现,而循环队列则基于数组优化空间利用 。双端队列Deque
则凭借双向操作的灵活性,成为工程中替代栈和队列的优选 。