当前位置: 首页 > news >正文

数据结构 —— 栈

栈是一种先进后出(LIFO, Last In First Out) 的线性数据结构,它只允许在一端(称为栈顶)进行插入和删除操作。栈的特性使其在很多场景中发挥重要作用,如表达式求值、函数调用、括号匹配、深度优先搜索等。本文将详细讲解栈的概念、实现及应用。

1.栈的基本概念

核心术语

  • 栈顶(Top):允许插入和删除操作的一端
  • 栈底(Bottom):固定的,不允许操作的一端
  • 压栈(Push):在栈顶插入元素的操作
  • 弹栈(Pop):从栈顶删除元素的操作
  • 栈空(Empty):栈中没有任何元素的状态
  • 栈满(Full):栈中元素达到最大容量的状态

栈的特性

  • 元素的插入和删除只能在栈顶进行;
  • 元素的访问顺序是 "先进后出",最后进入的元素最先被访问;
  • 栈是一种操作受限的线性表。

栈的基本操作

  • push(element):向栈顶插入元素
  • pop():移除并返回栈顶元素
  • peek():返回栈顶元素但不移除
  • isEmpty():判断栈是否为空
  • isFull():判断栈是否已满(仅数组实现)
  • size():返回栈中元素个数
  • clear():清空栈中所有元素

2.栈的两种实现方式

一、基于数组实现

数组实现的栈使用固定大小的数组存储元素,实现简单但容量固定。

import java.util.EmptyStackException;/*** 基于数组的栈实现* @param <T> 栈中元素的类型*/
public class ArrayStack<T> {private T[] stack;    // 存储栈元素的数组private int top;      // 栈顶指针,指向栈顶元素的索引private int capacity; // 栈的容量// 构造指定容量的栈@SuppressWarnings("unchecked")public ArrayStack(int capacity) {this.capacity = capacity;stack = (T[]) new Object[capacity];top = -1; // 栈空时,栈顶指针为-1}// 构造默认容量为10的栈public ArrayStack() {this(10);}/*** 压栈操作:向栈顶添加元素* @param element 要添加的元素* @throws StackOverflowError 如果栈已满*/public void push(T element) {if (isFull()) {throw new StackOverflowError("栈已满,无法添加元素");}stack[++top] = element; // 先移动栈顶指针,再添加元素}/*** 弹栈操作:移除并返回栈顶元素* @return 栈顶元素* @throws EmptyStackException 如果栈为空*/public T pop() {if (isEmpty()) {throw new EmptyStackException();}T element = stack[top];stack[top--] = null; // 帮助垃圾回收return element;}/*** 获取栈顶元素但不移除* @return 栈顶元素* @throws EmptyStackException 如果栈为空*/public T peek() {if (isEmpty()) {throw new EmptyStackException();}return stack[top];}/*** 判断栈是否为空* @return 如果栈为空返回true,否则返回false*/public boolean isEmpty() {return top == -1;}/*** 判断栈是否已满* @return 如果栈已满返回true,否则返回false*/public boolean isFull() {return top == capacity - 1;}/*** 获取栈中元素的个数* @return 栈中元素的个数*/public int size() {return top + 1;}/*** 清空栈*/public void clear() {// 清空元素,帮助垃圾回收for (int i = 0; i <= top; i++) {stack[i] = null;}top = -1;}/*** 打印栈中的元素*/public void printStack() {if (isEmpty()) {System.out.println("栈为空");return;}System.out.print("栈元素(从栈底到栈顶):");for (int i = 0; i <= top; i++) {System.out.print(stack[i] + " ");}System.out.println();}public static void main(String[] args) {ArrayStack<Integer> stack = new ArrayStack<>(5);// 压栈stack.push(1);stack.push(2);stack.push(3);stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2 3// 查看栈顶元素System.out.println("栈顶元素: " + stack.peek()); // 输出: 3// 弹栈System.out.println("弹出元素: " + stack.pop()); // 输出: 3stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2// 栈大小System.out.println("栈大小: " + stack.size()); // 输出: 2// 继续压栈stack.push(4);stack.push(5);stack.push(6);stack.printStack(); // 输出: 栈元素(从栈底到栈顶):1 2 4 5 6// 测试栈满try {stack.push(7);} catch (StackOverflowError e) {System.out.println(e.getMessage()); // 输出: 栈已满,无法添加元素}// 清空栈stack.clear();System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: true}}

二、基于链表的栈实现

链栈是一种基于链表实现的栈,其特点是无需事先分配固定长度的存储空间,栈的长度可以动态增长或缩小,避免了顺序栈可能存在的空间浪费和存储溢出问题。

链栈中的每个元素称为“节点”,每个节点包括两个部分:数据域和指针域。数据域用来存储栈中的元素值,指针域用来指向栈顶元素所在的节点。

链栈的基本操作包括入栈、出栈、获取栈顶元素和遍历等,相比顺序栈而言,链栈的实现难度稍高,但其在某些情况下有着更好的灵活性和效率,特别适用于在动态存储空间较为紧缺的场合。

注意:如果栈的使用过程中元素变化不可预料,那么最好使用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈。

链栈可以分为单链栈和双链栈:

单链栈使用单链表实现,每个节点只含有一个指向下一个节点的指针。因此,单链栈只能从栈顶进行插入和删除操作。

双链栈使用双向链表实现,每个节点同时包含指向前一个节点和后一个节点的指针。因此,双链栈既可以从栈顶进行插入和删除操作,也可以从栈底进行插入和删除操作,使得操作更加灵活。

import java.util.EmptyStackException;/*** 基于链表的栈实现* @param <T> 栈中元素的类型*/
public class LinkedStack<T> {// 节点类private static class Node<T> {T data;       // 节点数据Node<T> next; // 指向下一个节点的引用public Node(T data) {this.data = data;this.next = null;}}private Node<T> top;  // 栈顶节点private int size;     // 栈中元素的个数// 构造空栈public LinkedStack() {top = null;size = 0;}/*** 压栈操作:向栈顶添加元素* @param element 要添加的元素*/public void push(T element) {Node<T> newNode = new Node<>(element);newNode.next = top; // 新节点指向当前栈顶top = newNode;      // 更新栈顶为新节点size++;}/*** 弹栈操作:移除并返回栈顶元素* @return 栈顶元素* @throws EmptyStackException 如果栈为空*/public T pop() {if (isEmpty()) {throw new EmptyStackException();}T data = top.data;top = top.next; // 更新栈顶为下一个节点size--;return data;}/*** 获取栈顶元素但不移除* @return 栈顶元素* @throws EmptyStackException 如果栈为空*/public T peek() {if (isEmpty()) {throw new EmptyStackException();}return top.data;}/*** 判断栈是否为空* @return 如果栈为空返回true,否则返回false*/public boolean isEmpty() {return top == null;}/*** 获取栈中元素的个数* @return 栈中元素的个数*/public int size() {return size;}/*** 清空栈*/public void clear() {top = null;size = 0;}/*** 打印栈中的元素*/public void printStack() {if (isEmpty()) {System.out.println("栈为空");return;}System.out.print("栈元素(从栈顶到栈底):");Node<T> current = top;while (current != null) {System.out.print(current.data + " ");current = current.next;}System.out.println();}public static void main(String[] args) {LinkedStack<String> stack = new LinkedStack<>();// 压栈stack.push("A");stack.push("B");stack.push("C");stack.printStack(); // 输出: 栈元素(从栈顶到栈底):C B A// 查看栈顶元素System.out.println("栈顶元素: " + stack.peek()); // 输出: C// 弹栈System.out.println("弹出元素: " + stack.pop()); // 输出: Cstack.printStack(); // 输出: 栈元素(从栈顶到栈底):B A// 栈大小System.out.println("栈大小: " + stack.size()); // 输出: 2// 继续压栈stack.push("D");stack.push("E");stack.printStack(); // 输出: 栈元素(从栈顶到栈底):E D B A// 清空栈stack.clear();System.out.println("栈是否为空: " + stack.isEmpty()); // 输出: true}}

三、两种实现方式的对比

实现方式

优点

缺点

时间复杂度

适用场景

数组实现

实现简单访问速度快内存连续

容量固定,可能溢出扩容成本高

所有操作都是 O (1)

已知最大容量对性能要求高

链表实现

容量动态扩展没有溢出问题

实现较复杂需要额外空间存储指针

所有操作都是 O (1)

未知最大容量需要灵活扩展

3.java 中栈的实现

Java 标准库中提供了java.util.Stack类,但该类是遗留类,继承自Vector,存在一些设计缺陷,不推荐使用。

推荐使用java.util.Deque接口的实现类作为栈使用,如ArrayDeque

import java.util.Deque;
import java.util.ArrayDeque;public class StackExample {public static void main(String[] args) {// 使用ArrayDeque作为栈Deque<Integer> stack = new ArrayDeque<>();// 压栈stack.push(1);stack.push(2);stack.push(3);// 查看栈顶元素System.out.println("栈顶元素: " + stack.peek()); // 输出: 3// 弹栈System.out.println("弹出元素: " + stack.pop()); // 输出: 3// 遍历栈System.out.print("栈元素: ");while (!stack.isEmpty()) {System.out.print(stack.pop() + " "); // 输出: 2 1}}
}

ArrayDeque作为栈使用的优势:

  • 性能优于Stack
  • 接口设计更合理
  • 没有同步开销(Stack是线程安全的,带来额外开销)

4.栈的应用

1.函数递归调用

函数递归调用时,计算机会把函数调用时需要的参数和返回地址等信息放入栈中,函数执行完毕后再从栈中取回这些信息。

以汉诺塔为例:

public static void main(String[] args) {Hanoi(6,'A','B','C');
}private static int count = 0;  //统计步数
//汉诺方法
public static void Hanoi(int n,char A,char B,char C){//当n为1时,直接移动if(n == 1){System.out.println("第"+ ++count + "步:"+A +"-->"+C);} else {//当n不为1时//首先 为了将第n个盘子从A移到C  可先将第n-1个盘子借助C从A移到BHanoi(n - 1, A, C, B);System.out.println("第" + ++count + "步:" + A + "-->" + C);//然后再将第n-1个盘子借助A从B移到CHanoi(n - 1, B, A, C);}
}

2.括号匹配

public static void main(String[] args) {String expr1 = "((a + b) * (c - d))";String expr2 = "((a + b) * [c - d})";String expr3 = "a + b) * (c - d";System.out.println(expr1 + " 括号匹配: " + isBalanced(expr1)); // trueSystem.out.println(expr2 + " 括号匹配: " + isBalanced(expr2)); // falseSystem.out.println(expr3 + " 括号匹配: " + isBalanced(expr3)); // false
}
//检查括号匹配
public static boolean isBalanced(String expression){
Deque<Character> stack = new ArrayDeque<>();
for(char c : expression.toCharArray()){//如果是左括号,入栈if(c == '(' || c == '{' || c == '['){stack.push(c);}//如果是右括号else if(c == ')' || c == '}' || c == ']'){if(stack.isEmpty()){return false;}//弹出栈顶元素进行匹配char top = stack.pop();if(top == '(' && c != ')' || top == '{' && c != '}' || top == '[' && c!= ']'){return false;}}
}
return stack.isEmpty();
}

3.表达式求值

/*** 使用栈求后缀表达式(逆波兰表达式)的值*/
public class ExpressionEvaluation {public static int evaluatePostfix(String[] tokens) {Deque<Integer> stack = new ArrayDeque<>();for (String token : tokens) {// 如果是运算符,弹出两个元素进行运算if (token.equals("+") || token.equals("-") ||token.equals("*") || token.equals("/")) {int b = stack.pop(); // 注意弹出顺序,后弹出的是第一个操作数int a = stack.pop();int result = 0;switch (token) {case "+":result = a + b;break;case "-":result = a - b;break;case "*":result = a * b;break;case "/":result = a / b; // 假设除数不为0break;}stack.push(result);}// 如果是数字,直接入栈else {stack.push(Integer.parseInt(token));}}return stack.pop();}public static void main(String[] args) {// 表达式: 3 + 4 * 2 / (1 - 5)// 后缀表达式: 3 4 2 * 1 5 - / +String[] tokens = {"3", "4", "2", "*", "1", "5", "-", "/", "+"};System.out.println("表达式结果: " + evaluatePostfix(tokens)); // 输出: 1}
}

4.浏览器历史记录

栈可以用于实现浏览器的前进和后退功能:

  • 访问新页面时,将当前页面压入历史栈
  • 点击后退按钮时,从历史栈弹出页面
  • 可以使用两个栈实现前进功能

5.栈性能的分析

栈的所有基本操作(push、pop、peek、isEmpty)的时间复杂度都是O(1),因为这些操作只涉及栈顶元素,不需要遍历整个栈。

栈的空间复杂度是O(n),其中 n 是栈中元素的数量,因为需要存储所有元素。

http://www.dtcms.com/a/569281.html

相关文章:

  • 微信小程序开发案例 | 个人相册小程序(下)
  • 网站域名账号网址申请注册
  • 电商 API 数据交互最佳实践:JSON 格式优化、数据校验与异常处理
  • 重庆网站建设 沛宣织梦cms 官方网站
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十七)ELK日志分析模块--Elasticsearch介绍与配置
  • 如何使用elasticdump进行elasticsearch数据还原
  • 【运维记录】Centos 7 基础命令缺失
  • 手写 RPC 框架
  • etcd 高可用分布式键值存储
  • 【ETCD】ETCD单节点二进制部署(TLS)
  • 小网站 收入请简述网站制作流程
  • 抗辐照MCU芯片在无人叉车领域的性能评估与选型建议
  • 什么是LLM?
  • Java/PHP源码解析:一站式上门维修服务系统的全栈实现
  • MPU6050 DMP 移植中 mpu_run_self_test () 自检失败的原因与解决方法
  • 系统端实现看门狗功能
  • 算法--二分查找(二)
  • 没有网站备案可以做诚信认证嘛商城网站大概多少钱
  • 保定市场产品投放策略分析
  • Linux网络——连接、TCP全连接队列TCPdump抓包
  • Firefox 浏览器:引领网络浏览新时代
  • 【个人成长笔记】解决在Linux/Windows系统中 git pull 之后提示有未提交的更改错误信息(亲测有效)
  • 分布式训练一站式入门:DP,DDP,DeepSpeed Zero Stage1/2/3(数据并行篇)
  • 优化网站的目的佛山标书设计制作
  • Slurm:高性能计算集群的调度利器
  • Qt 开发终极坑点手册图表版本
  • 2019阿里java面试题(一)
  • 云手机与云服务器之间的关系
  • 网站建设的经验东莞大岭山楼盘最新价格表
  • 网站策划书 范文兰州装修公司哪家口碑最好