深入理解栈数据结构(Java实现):从原理到实战应用
在计算机科学的世界里,数据结构是构建高效程序的基石,而栈作为其中最基础且应用广泛的一种数据结构,其独特的 “后进先出(LIFO)” 特性,使其在众多领域发挥着关键作用。从算法设计到编译器实现,从函数调用机制到日常业务逻辑处理,栈无处不在。本文将深入剖析栈的核心概念、实现方式、典型应用场景、高阶用法,以及常见问题的解决方案,帮助读者全面掌握栈这一重要的数据结构。
一、栈的核心概念
栈(Stack) 是一种遵循后进先出(LIFO,Last In First Out) 原则的线性数据结构。这意味着最后进入栈的数据会最先被取出,就像一摞盘子,最后放上去的盘子总是最先被拿走。栈的核心操作主要有两个:
- 入栈(Push):将数据元素添加到栈的顶部。
- 出栈(Pop):从栈顶移除并返回一个数据元素。
此外,栈通常还提供查看栈顶元素(Peek) 和判断栈是否为空(IsEmpty) 等辅助操作。栈的这种特性使其在许多场景中有着不可替代的作用,比如函数调用时记录调用信息、编译器处理表达式求值、浏览器的前进后退功能实现等。
二、栈的两种实现方式(Java 实现)
在 Java 中,栈可以通过数组和链表两种方式实现,每种实现方式都有其独特的优缺点和适用场景。
2.1 数组实现(固定大小)
使用数组实现栈时,需要预先指定数组的大小,即栈的容量。栈顶元素通过一个变量top
来记录其在数组中的位置,初始时top
为 -1,表示栈为空。
class ArrayStack {private int[] data;private int top = -1;public ArrayStack(int capacity) {data = new int[capacity];}public void push(int val) {if (top == data.length - 1) {throw new RuntimeException("Stack overflow");}data[++top] = val;}public int pop() {if (isEmpty()) {throw new RuntimeException("Stack underflow");}return data[top--];}public boolean isEmpty() {return top == -1;}
}
优点:数组实现的栈内存连续,通过索引访问元素速度快,适合对读取性能要求较高的场景。
缺点:容量固定,当栈中元素达到预设容量后,无法继续添加元素,可能会导致栈溢出。
适用场景:在明确知道数据量上限的情况下,使用数组实现栈可以获得较好的性能和空间利用率。
2.2 链表实现(动态扩容)
采用链表实现栈,每个节点存储一个数据元素和指向下一个节点的引用。栈顶元素由链表的头节点表示,当链表为空时,栈为空。
class LinkedStack {private static class Node {int val;Node next;Node(int val) { this.val = val; }}private Node top;public void push(int val) {Node newNode = new Node(val);newNode.next = top;top = newNode;}public int pop() {if (isEmpty()) throw new RuntimeException("Stack is empty");int val = top.val;top = top.next;return val;}public boolean isEmpty() {return top == null;}
}
优点:链表实现的栈可以动态扩容,无需预先指定容量,适合数据量变化较大的场景。
缺点:由于每个节点需要额外存储指针,内存开销相对较大;而且链表节点的内存地址不连续,访问效率不如数组。
适用场景:当无法预估数据量大小,或者数据量可能会频繁变化时,链表实现的栈更为合适。
2.3 实现对比
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
数组 | 内存连续,访问快 | 容量固定 | 明确数据量上限 |
链表 | 动态扩容,更灵活 | 内存开销大(指针) | 数据量变化大 |
三、栈的典型应用场景
栈的 “后进先出” 特性使其在许多实际问题中成为高效的解决方案,以下是一些常见的应用场景。
3.1 括号匹配(LeetCode 20)
在代码编辑器、编译器等工具中,需要检查括号(如()
、[]
、{}
)是否匹配。可以使用栈来解决这个问题:遇到左括号时入栈,遇到右括号时检查栈顶元素是否为对应的左括号,如果是则出栈,否则说明括号不匹配。
public boolean isValid(String s) {Deque<Character> stack = new ArrayDeque<>();Map<Character, Character> map = Map.of(')', '(', ']', '[', '}', '{');for (char c : s.toCharArray()) {if (map.containsValue(c)) {stack.push(c);} else if (stack.isEmpty() || stack.pop() != map.get(c)) {return false;}}return stack.isEmpty();
}
3.2 函数调用栈
在程序执行过程中,函数调用遵循 “后进先出” 的原则。当一个函数被调用时,其相关信息(如局部变量、返回地址等)会被压入栈中;当函数执行完毕,这些信息会从栈中弹出。例如:
void funcA() {System.out.println("进入A");funcB();System.out.println("离开A");
}void funcB() {System.out.println("进入B");funcC();System.out.println("离开B");
}void funcC() {System.out.println("执行C");
}// 输出结果:
// 进入A → 进入B → 执行C → 离开B → 离开A
3.3 逆波兰表达式求值(LeetCode 150)
逆波兰表达式(后缀表达式)是一种无需括号即可明确运算顺序的表达式形式。通过栈可以方便地对其进行求值:遇到操作数时入栈,遇到操作符时从栈中弹出相应数量的操作数进行运算,并将结果入栈。
public int evalRPN(String[] tokens) {Deque<Integer> stack = new ArrayDeque<>();for (String token : tokens) {if (token.matches("-?\\d+")) {stack.push(Integer.parseInt(token));} else {int b = stack.pop(), 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;}}}return stack.pop();
}
四、栈的高阶应用
除了上述典型应用,栈在一些复杂问题中也能发挥强大的作用。
4.1 最小栈(LeetCode 155)
设计一个支持在常数时间内获取栈中最小元素的栈。可以使用两个栈,一个用于存储数据,另一个用于存储当前的最小元素。
class MinStack {Deque<Integer> dataStack = new ArrayDeque<>();Deque<Integer> minStack = new ArrayDeque<>();public void push(int val) {dataStack.push(val);if (minStack.isEmpty() || val <= minStack.peek()) {minStack.push(val);}}public void pop() {if (dataStack.pop().equals(minStack.peek())) {minStack.pop();}}public int getMin() {return minStack.peek();}
}
4.2 单调栈应用(LeetCode 739 每日温度)
给定一个数组,对于每个元素,找出下一个比它大的元素出现的天数。可以使用单调栈来解决:维护一个单调递增的栈,当遇到比栈顶元素大的元素时,计算两者的索引差并更新结果数组。
public int[] dailyTemperatures(int[] T) {int[] res = new int[T.length];Deque<Integer> stack = new ArrayDeque<>();for (int i = 0; i < T.length; i++) {while (!stack.isEmpty() && T[i] > T[stack.peek()]) {int idx = stack.pop();res[idx] = i - idx;}stack.push(i);}return res;
}
五、常见问题与解决方案
在使用栈的过程中,可能会遇到一些常见问题,以下是这些问题的解决方案。
5.1 栈溢出(Stack Overflow)
场景:当递归函数调用层数过深,或者栈中元素过多超出了 JVM 分配的栈空间时,会发生栈溢出错误。
解决方案:
- 改用迭代算法:将递归实现改为迭代实现,避免函数调用栈的无限增长。
- 增加 JVM 栈空间:通过启动参数
-Xss
增加栈的大小,例如-Xss2m
将栈空间设置为 2MB。但这种方法只是治标不治本,且可能会消耗过多内存。 - 尾递归优化:虽然 Java 本身不支持尾递归优化,但可以通过手动改写代码,将递归调用转化为循环结构来模拟尾递归优化的效果。
5.2 空栈异常(EmptyStackException)
场景:在对空栈执行pop()
或peek()
操作时,会抛出空栈异常。
防御性编程:在执行相关操作前,先通过isEmpty()
方法判断栈是否为空,避免异常的发生。
public int peek() {if (isEmpty()) throw new EmptyStackException();return top.val;
}
5.3 线程安全问题
场景:在多线程环境下,多个线程同时操作栈时,可能会出现数据不一致的问题。
解决方案:
- 使用
Collections.synchronizedCollection
:将栈包装成线程安全的集合,例如Deque<Integer> synchronizedStack = Collections.synchronizedDeque(new ArrayDeque<>());
。 - 改用并发栈实现:使用 Java 并发包中的线程安全栈实现,如
LinkedBlockingDeque
,它提供了线程安全的操作方法。
六、高频面试题精选
栈是面试中的高频考点,以下是一些常见的面试题目:
- 用队列实现栈(LeetCode 225):使用两个队列或一个队列模拟栈的 “后进先出” 特性。
- 验证栈序列(LeetCode 946):给定入栈序列和出栈序列,判断出栈序列是否合法。
- 基本计算器(LeetCode 224):实现一个基本计算器来计算字符串表达式的值,涉及括号处理和运算符优先级。
- 二叉树的中序遍历(栈实现):使用栈实现二叉树的中序遍历,无需递归。
- 字符串解码(LeetCode 394):对包含重复子串的字符串进行解码,例如
3[a2[c]]
解码为accaccacc
。
// 用队列实现栈(解法示例)
class MyStack {Queue<Integer> queue = new LinkedList<>();public void push(int x) {queue.offer(x);for (int i = 1; i < queue.size(); i++) {queue.offer(queue.poll());}}public int pop() {return queue.poll();}
}
七、性能优化策略
为了提高栈的性能和效率,可以采用以下优化策略:
- 动态扩容数组:实现一个支持自动扩容的数组栈,当栈满时自动增加数组容量,避免栈溢出。
- 内存预分配:根据业务场景预先分配合适的栈容量,减少内存分配和重新分配的开销。
- 对象池技术:对于链表实现的栈,使用对象池复用节点对象,减少频繁创建和销毁对象的开销。
- 延迟出栈:在某些场景下,可以批量处理出栈操作,减少出栈的频率,提高整体性能。
八、栈的扩展知识
除了软件层面的栈,栈在计算机系统的其他领域也有重要应用:
- 硬件栈:CPU 中包含栈寄存器(如 ESP、EBP),用于在指令执行过程中管理函数调用栈和存储临时数据。
- 协程栈:在 Go 语言等支持协程的编程语言中,协程拥有自己的轻量级栈,相比传统线程栈更加节省内存,且切换效率更高。
- 表达式树:在编译器中,栈常用于构建和处理表达式树,将中缀表达式转换为后缀表达式,并进行求值。
- DFS 算法:深度优先搜索(DFS)算法可以使用栈来实现,通过栈记录待访问的节点,实现对图或树的深度优先遍历。
结语
栈作为计算机科学中最基础且重要的数据结构之一,其 “后进先出” 的特性和丰富的应用场景使其在各种程序设计中扮演着不可或缺的角色。掌握栈的关键在于:
- 理解 LIFO 特性:深刻理解栈的 “后进先出” 原则,这是解决栈相关问题的核心。
- 掌握经典应用场景:熟练掌握括号匹配、函数调用、表达式求值等典型应用场景,能够灵活运用栈解决实际问题。
- 熟练实现方式:清楚数组和链表两种实现方式的优缺点及适用场景,根据需求选择合适的实现方式。
- 解决实际问题:通过大量练习 LeetCode 等平台的题目,提升运用栈解决复杂问题的能力。
推荐学习路线:
从栈的基础概念和实现方式入手,逐步深入到典型应用场景和高阶变形问题,最后结合系统设计和实际项目,将栈的知识融会贯通。
希望本文能帮助读者深入理解栈数据结构,并在实际开发和学习中灵活运用。如果你在学习过程中有任何疑问或想法,欢迎在评论区交流讨论!
相关文章:
深入理解栈数据结构(Java实现):从原理到实战应用
在计算机科学的世界里,数据结构是构建高效程序的基石,而栈作为其中最基础且应用广泛的一种数据结构,其独特的 “后进先出(LIFO)” 特性,使其在众多领域发挥着关键作用。从算法设计到编译器实现,…...
支付宝 SEO 优化:提升小程序曝光与流量的完整指南
在拥有庞大用户基数的支付宝平台上,小程序已成为商家触达用户、提供服务的重要渠道。然而,随着平台上小程序数量的快速增长,如何在激烈的竞争中脱颖而出,获得更多的曝光和流量,成为每个开发者和运营者必须面对的关键挑…...

【leetcode100】最长重复子数组
1、题目描述 给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。 示例 1: 输入:nums1 [1,2,3,2,1], nums2 [3,2,1,4,7] 输出:3 解释:长度最长的公共子数组是 [3,2,1] 。示例 2&…...
代码随想录算法训练营第五十六天| 图论2—卡码网99. 岛屿数量(dfs bfs)
假期归来继续刷题,图论第二天,主要是进一步熟悉dfs 和 bfs 的运用。 99. 岛屿数量(dfs) 99. 岛屿数量 ACM模式还是需要练,不过现在输入输出的感觉已经比较熟悉了。首先是要按照输入搭建一个grid,然后有一…...
源码示例:使用SpringBoot+Vue+ElementUI+UniAPP技术组合开发一套小微企业ERP系统
目录 一、系统架构设计 1、技术分层 2、开发环境 二、快速开发实践 1、后端搭建(Spring Boot) 2、前端管理端(VueElementUI) 3、移动端开发(UniAPP) 三、关键集成方案 1、统一接口处理 2、跨平台…...

基于Django框架的股票分红数据爬虫和展示系统
项目截图 一、项目简介 本项目是一个基于 Django 框架的股票分红数据爬虫和展示系统。它可以从东方财富网站爬取股票分红数据,并将数据存储到 Django 数据库中,同时提供数据查询、导出和图表展示功能。该系统为用户提供了一个方便的平台,用于…...

QT高级(1)QTableView自定义委托集合,一个类实现若干委托
自定义委托集合 1同系列文章2 功能3 源码 1同系列文章 QT中级(1)QTableView自定义委托(一)实现QSpinBox、QDoubleSpinBox委托 QT中级(2)QTableView自定义委托(二)实现QProgressBar委…...
kubectl系列(十一):top 查询pod连接数
在 Kubernetes 中,kubectl top 命令默认仅支持查看 Pod 或节点的 CPU/内存资源使用情况,并不直接提供 TCP 连接数的统计功能。若要获取 Pod 的 TCP 连接数,需结合其他工具和方法。以下是具体实现方案: 1. 直接进入容器查看 TCP 连…...
关于Spring
目录 事务篇 事务篇 先说结论 Spring事务实际上依赖的是Transactional接口和数据库的事务实现。 举个例子说,比如我们现在有一个**Service1类,这个类的方法MethodA执行一个向表A中插入数据;还有一个**Service2类,这个类的方法M…...
小家电专用WD5201 非隔离AC-DC稳压器|宽压80-305V|三档输出2.7/3.3/5V|多重安全保护
小家电专用WD5201 AC-DC稳压器|宽压80-305V|三档输出2.7/3.3/5V|多重安全保护 💥 WD5201,小家电电源的智能“稳压卫士”! ✨ 核心卖点: ✅ 宽压兼容:输入 80-305V AC,电网…...
Docker 核心目录结构
1. Docker 核心目录结构 数据存储目录 默认根目录:/var/lib/docker Docker 所有运行时数据(镜像、容器、卷、网络配置等)的默认存储位置。 bash 复制 下载 # 查看 Docker 数据根目录 docker info | grep "Docker Root Dir" # 输出…...
源码分析之Leaflet中的LayerGroup
概述 LayerGroup是一个图层组,通过继承Layer基类,提供了一种管理多个图层(如标记、多边形等)的容器机制,比如地图的添加/移除操作等。 源码分析 源码实现 LayerGroup的源码实现如下: export var Layer…...

小芯片大战略:Chiplet技术如何重构全球半导体竞争格局?
在科技飞速发展的今天,半导体行业作为信息技术的核心领域之一,其发展速度和创新水平对全球经济的发展具有举足轻重的影响。然而,随着芯片制造工艺的不断进步,传统的单片集成方式逐渐遇到了技术瓶颈,如摩尔定律逐渐逼近…...

普通IT的股票交易成长史--股价起伏的真相-缺口(2)
声明:本文章的内容只是自己学习的总结,不构成投资建议。价格行为理论学习可参考简介中的几位,感谢他们的无私奉献。 送给自己的话: 仓位就是生命,绝对不能满仓!!!!&…...

MindSpore框架学习项目-ResNet药物分类-模型优化
目录 5.模型优化 5.1模型优化 6.结语 参考内容: 昇思MindSpore | 全场景AI框架 | 昇思MindSpore社区官网 华为自研的国产AI框架,训推一体,支持动态图、静态图,全场景适用,有着不错的生态 本项目可以在华为云modelar…...
基于阿里云DataWorks的物流履约时效离线分析
基于阿里云DataWorks的物流履约时效离线分析2. 数仓模型构建 ORC和Parquet区别: 压缩率与查询性能 压缩率 ORC通常压缩率更高,文件体积更小,适合存储成本敏感的场景。 Parquet因支持更灵活的嵌套结构,压缩率略…...

Kubernetes(k8s)学习笔记(八)--KubeSphere定制化安装
1执行下面的命令修改上一篇中yaml文件来实现定制化安装devops kubectl edit cm -n kubesphere-system ks-installer 主要是将devops几个配置由False改为True 然后使用下面的命令查看安装日志 kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l …...

养生:为健康生活筑牢根基
养生并非遥不可及的目标,而是贯穿于日常生活的点滴之中。从饮食、运动到心态调节,每一个环节都对我们的健康有着重要意义。以下为你详细介绍养生的实用策略,助力你开启健康生活模式。 饮食养生:科学搭配,滋养生命 合…...

Linux510 ssh服务 ssh连接
arning: Permanently added ‘11.1.1.100’ (ECDSA) to the list of known hosts. rooot11.1.1.100’s password: Permission denied, please try again. rooot11.1.1.100’s password: Permission denied, please try again 还没生效 登不上了 失效了 sshcaozx26成功登录 …...

关键点检测--使用YOLOv8对Leeds Sports Pose(LSP)关键点检测
目录 1. Leeds Sports Pose数据集下载2. 数据集处理2.1 获取标签2.2 将图像文件和标签文件处理成YOLO能使用的格式 3. 用YOLOv8进行训练3.1 训练3.2 预测 1. Leeds Sports Pose数据集下载 从kaggle官网下载这个数据集,地址为link,下载好的数据集文件如下…...
Elasticsearch内存管理与JVM优化:原理剖析与最佳实践
#作者:孙德新 文章目录 一、Elasticsearch缓存分类1、Node Query Cache:2、Shard Request Cache:3、Fielddata Cache: 三、内存常见的问题案例一案例二案例三案例四 四、内参分配最佳实践1、jvm heap分配2、将机器上少于一半的内…...

独立按键控制LED
目录 1.独立按键介绍 2.原理图 3.C51数据运输 解释:<< >> 编辑 解释:& | 解释:^ ~ 编辑 4.C51基本语句 5.按键的跳动 6.独立按键控制LED亮灭代码 第一步: 第二步: 第三步࿱…...

计算机科技笔记: 容错计算机设计03 系统可信性的度量 偶发故障期 浴盆曲线 韦布尔分布
可靠性 简化表达式 偶发故障期,系统发生故障概率趋近于一个常数 浴盆曲线 MTTF和计算 韦布尔分布 马尔可夫链 可靠度...

爬虫准备前工作
1.Pycham的下载 网址:PyCharm: The only Python IDE you need 2.Python的下载 网址:python.org(python3.9版本之后都可以) 3.node.js的下载 网址:Node.js — 在任何地方运行 JavaScript(版本使用18就可…...

【PostgreSQL数据分析实战:从数据清洗到可视化全流程】7.1 主流可视化工具对比(Tableau/Matplotlib/Python库)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 第七章 可视化工具集成:Tableau、Matplotlib与Python库深度对比7.1 主流可视化工具对比:技术选型的决策框架7.1.1 工具定位与核心能力矩阵7.1.2 数据…...

操作系统实验习题解析 上篇
孤村落日残霞,轻烟老树寒鸦,一点飞鸿影下。 青山绿水,白草红叶黄花。 ————《天净沙秋》 白朴 【元】 目录 实验一: 代码: 解析: 运行结果: 实验二: 代码解析 1. 类设计 …...
复习javascript
1.修改元素内的内容 <div>zsgh</div> <script> const box1document.querySelector("div") box1.innerText"ppp" box1.innerHtml<h1>修改</h1> </script> 2.随机点名练习 <!DOCTYPE html> <html lang…...

基于Arduino Nano的DIY示波器
基于Arduino Nano的DIY示波器:打造属于你的口袋实验室 前言 在电子爱好者的世界里,示波器是不可或缺的工具之一。它能够帮助我们观察和分析各种电子信号的波形,从而更好地理解和调试电路。然而,市面上的示波器价格往往较高&…...

渠道销售简历模板范文
模板信息 简历范文名称:渠道销售简历模板范文,所属行业:其他 | 职位,模板编号:KRZ3J3 专业的个人简历模板,逻辑清晰,排版简洁美观,让你的个人简历显得更专业,找到好工作…...

JAVA练习题(1) 卖飞机票
import java.util.Scanner; public class Main {public static void main(String[] args) {Scanner scnew Scanner(System.in);System.out.println("请输入飞机的票价:");int pricesc.nextInt();System.out.println("请输入月份:");…...