从栈序列验证到队列操作:Java 数据结构入门实战总结
今天通过两道经典算法题,系统学习了栈、队列、链表的核心概念与实战应用,还解决了输入输出、指针逻辑等新手常见问题。整理成笔记,方便后续回顾~
一、核心数据结构知识点
1. 栈(Stack)
- 核心特性:先进后出(LIFO, Last In First Out),仅允许在栈顶执行入栈(push)和出栈(pop)操作。
- 关键方法(Java 中推荐用 ArrayDeque 实现,效率高于 Stack 类):
-
- push(num):将元素压入栈顶;
-
- pop():移除并返回栈顶元素;
-
- peek():查看栈顶元素(不移除);
-
- isEmpty():判断栈是否为空。
- 应用场景:验证出栈序列合法性、表达式求值、括号匹配等。
2. 队列(Queue)
- 核心特性:先进先出(FIFO, First In First Out),仅允许在队尾入队、队首出队 / 查询。
- 关键方法(Java 中用 Queue 接口 + LinkedList 实现):
-
- offer(x):将元素添加到队尾(入队);
-
- poll():移除并返回队首元素(出队);
-
- peek():查看队首元素(不移除);
-
- size():返回队列元素个数;
-
- isEmpty():判断队列是否为空。
- 应用场景:任务排队、消息队列、广度优先搜索(BFS)等。
3. 链表(LinkedList 底层实现)
- 核心定义:线性数据结构,元素(节点)分散存储,通过「引用」连接前后节点,无需连续内存。
- 节点组成:
-
- 数据域:存储实际值;
-
- 引用域:存储下一个节点的地址。
- 与数组对比:
| 特性 | 数组 | 链表 |
|---|---|---|
| 内存存储 | 连续空间 | 分散空间 |
| 访问效率 | 高(下标直接访问) | 低(需从头遍历) |
| 增删效率 | 低(需移动元素) | 高(仅改引用指向) |
| 空间灵活性 | 固定大小 | 动态扩容(用多少占多少) |
二、实战题目与解题思路
题目 1:验证合法出栈序列
题目描述
给定入栈序列 pushed 和出栈序列 popped(均为 1~n 的排列),判断 popped 是否为合法出栈序列。
核心思路:模拟栈操作
- 用 ArrayDeque 作为模拟栈,popIndex 作为出栈序列的「进度指针」(标记当前待匹配的位置);
- 遍历入栈序列,逐个将元素压入模拟栈;
- 每次压栈后,循环检查栈顶元素是否等于 popped[popIndex]:
-
- 若相等,弹出栈顶元素,popIndex 后移(匹配成功);
-
- 若不相等,继续压栈;
- 遍历结束后,若 popIndex == n(所有元素都匹配完成),则序列合法。
关键代码片段
private static boolean checkValidPopSequence(int[] pushed, int[] popped, int n) {
Deque<Integer> stack = new ArrayDeque<>();
int popIndex = 0; // 出栈序列进度指针
for (int num : pushed) {
stack.push(num);
// 栈顶匹配则弹出,移动指针
while (!stack.isEmpty() && stack.peek() == popped[popIndex]) {
stack.pop();
popIndex++;
}
}
return popIndex == n; // 指针到末尾则合法
}
题目 2:队列的基本操作
题目描述
实现队列的 4 种操作:
- 1 x:将 x 入队;
- 2:队非空则出队,否则输出 ERR_CANNOT_POP;
- 3:查询队首元素,队空输出 ERR_CANNOT_QUERY;
- 4:输出队列元素个数。
核心思路:分支处理操作类型
- 初始化 Queue 队列和 Scanner 输入工具;
- 读取操作总数 n,循环 n 次处理每个操作;
- 按操作类型分支执行:
-
- 操作 1:读取 x 并调用 offer(x) 入队;
-
- 操作 2:判断队列是否为空,空则输出错误,否则调用 poll() 出队;
-
- 操作 3:空队列输出错误,否则调用 peek() 输出队首;
-
- 操作 4:调用 size() 输出队列大小。
完整代码
import java.util.Scanner;
import java.util.Queue;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Queue<Integer> queue = new LinkedList<>();
int n = sc.nextInt();
for (int i = 0; i < n; i++) {
int op = sc.nextInt();
if (op == 1) {
int x = sc.nextInt();
queue.offer(x);
} else if (op == 2) {
System.out.println(queue.isEmpty() ? "ERR_CANNOT_POP" : queue.poll());
} else if (op == 3) {
System.out.println(queue.isEmpty() ? "ERR_CANNOT_QUERY" : queue.peek());
} else if (op == 4) {
System.out.println(queue.size());
}
}
sc.close();
}
}
三、新手避坑指南
1. Scanner 输入陷阱(nextInt() 与 nextLine() 混用)
- 问题:nextInt() 读取整数后,不会消耗换行符,后续 nextLine() 会读取空行;
- 解决:
-
- 仅读取整数时,全程用 nextInt()(自动跳过空格 / 换行符);
-
- 若 nextInt() 后需用 nextLine(),先加 sc.nextLine() 消耗残留换行符。
2. 「指针」的通俗理解
- 编程中「逻辑指针」(如 popIndex)不是 C 语言的内存指针,而是「标记当前处理位置的变量」;
- 类比:像看书时的书签,标记 “下一个要处理的内容位置”,处理完后往后移。
3. 数据结构选择技巧
- 处理「先进后出」场景 → 选栈;
- 处理「先进先出」场景 → 选队列;
- 大数据量(n≤10^5)时,栈用 ArrayDeque,队列用 LinkedList,效率更高。
4. 边界情况处理
- 栈 / 队列为空时,执行出队 / 查询操作必须输出错误提示;
- 循环遍历前,确认指针 / 索引的初始值(如 popIndex 初始为 0)。
四、总结
今天的学习围绕「数据结构特性 + 实战应用」展开,核心收获:
- 理解栈、队列、链表的核心特性与适用场景;
- 掌握「模拟栈操作」验证出栈序列、「分支处理」队列操作的解题模板;
- 避开输入输出、边界处理等新手常见坑。
数据结构是算法的基础,后续可以继续练习括号匹配、BFS 等题目,加深对这些结构的理解~