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

【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

Go语言系列文章目录

Docker系列文章目录

数据结构与算法系列文章目录

01-【数据结构与算法-Day 1】程序世界的基石:到底什么是数据结构与算法?
02-【数据结构与算法-Day 2】衡量代码的标尺:时间复杂度与大O表示法入门
03-【数据结构与算法-Day 3】揭秘算法效率的真相:全面解析O(n^2), O(2^n)及最好/最坏/平均复杂度
04-【数据结构与算法-Day 4】从O(1)到O(n²),全面掌握空间复杂度分析
05-【数据结构与算法-Day 5】实战演练:轻松看懂代码的时间与空间复杂度
06-【数据结构与算法-Day 6】最朴素的容器 - 数组(Array)深度解析
07-【数据结构与算法-Day 7】告别数组束缚,初识灵活的链表 (Linked List)
08-【数据结构与算法-Day 8】手把手带你拿捏单向链表:增、删、改核心操作详解
09-【数据结构与算法-Day 9】图解单向链表:从基础遍历到面试必考的链表反转
10-【数据结构与算法-Day 10】双向奔赴:深入解析双向链表(含图解与代码)
11-【数据结构与算法-Day 11】从循环链表到约瑟夫环,一文搞定链表的终极形态
12-【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现


文章目录

  • Langchain系列文章目录
  • Python系列文章目录
  • PyTorch系列文章目录
  • 机器学习系列文章目录
  • 深度学习系列文章目录
  • Java系列文章目录
  • JavaScript系列文章目录
  • Python系列文章目录
  • Go语言系列文章目录
  • Docker系列文章目录
  • 数据结构与算法系列文章目录
  • 摘要
  • 一、什么是栈 (Stack)?
    • 1.1 栈的定义与特性
    • 1.2 栈的抽象数据类型 (ADT)
    • 1.3 栈的应用场景
  • 二、基于数组实现栈 (静态栈)
    • 2.1 设计思路
    • 2.2 核心操作实现 (以 Java 为例)
      • 2.2.1 时间复杂度分析
    • 2.3 优缺点分析
  • 三、基于链表实现栈 (动态栈)
    • 3.1 设计思路
    • 3.2 核心操作实现 (以 Java 为例)
      • 3.2.1 时间复杂度分析
    • 3.3 优缺点分析
  • 四、两种实现方式的对比
  • 五、总结


摘要

欢迎来到数据结构与算法系列的第 12 篇。在探索了数组和链表这两种基础线性结构后,今天我们将学习一种非常重要但又受限的线性数据结构——栈 (Stack)。栈以其独特的 “后进先出” (LIFO) 原则,在计算机科学领域扮演着不可或缺的角色,从我们日常使用的软件中的“撤销”功能,到支撑现代编程语言运行的函数调用,背后都有栈的身影。本文将系统地剖析栈的定义、核心特性与操作,并手把手带你用数组链表两种方式实现一个功能完备的栈,深入对比它们的优劣与适用场景,为你彻底掌握栈结构打下坚实的基础。

一、什么是栈 (Stack)?

在进入代码实现之前,我们必须先理解栈的核心思想。它非常直观,贴近生活。

1.1 栈的定义与特性

栈 (Stack) 是一种特殊的线性表,它只允许在固定的一端进行插入和删除元素的操作。进行数据插入和删除操作的一端称为栈顶 (Top),另一端称为栈底 (Bottom)

栈最大的特性就是后进先出 (Last-In, First-Out),简称 LIFO。这个原则可以用生活中的例子来形象地理解:

  • 一摞盘子:想象一下你在洗一摞盘子,你总是把洗好的盘子放在最上面,而需要用盘子时,也总是从最上面拿。最后放上去的盘子,最先被取走。
  • 羽毛球筒:往羽毛球筒里放球时,第一个放进去的球会掉到最底下,最后一个放进去的球在最顶上。取球时,也只能从顶部一个一个地取。

在栈中,数据元素的插入操作称为入栈 (Push),删除操作称为出栈 (Pop)

1.2 栈的抽象数据类型 (ADT)

从功能的角度,一个栈结构通常需要支持以下几种核心操作:

  • push(element): 将一个元素压入栈顶。
  • pop(): 从栈顶弹出一个元素,并返回该元素。
  • peek(): 查看栈顶元素,但不弹出。
  • isEmpty(): 判断栈是否为空。
  • size(): 返回栈中元素的个数。

这些操作共同构成了栈的抽象数据类型(Abstract Data Type),定义了栈“应该做什么”,而不用关心其“内部如何实现”。

1.3 栈的应用场景

栈虽然结构简单,但应用极其广泛,是许多复杂算法和系统的基石:

  • 函数调用栈:程序在调用函数时,会将调用信息(如返回地址、参数、局部变量)压入一个栈中,函数返回时再从栈中弹出。这是递归得以实现的基础。
  • 表达式求值:编译器使用栈来转换和计算中缀表达式(如 3 + 4 * 2)为后缀表达式(逆波兰表达式 3 4 2 * +),并求值。
  • 括号匹配校验:在代码编辑器中,判断括号((), [], {})是否成对出现,可以用栈轻松实现。
  • 浏览器的前进后退:浏览器的历史记录后退功能,就像一个栈,每访问一个新页面就入栈,点击后退按钮就出栈。
  • 软件的撤销(Undo)操作:在文本编辑器或绘图软件中,每执行一个操作,就将该操作的状态入栈,点击撤销时,就从栈顶弹出一个状态进行恢复。

我们将在下一篇文章中详细探讨这些应用。今天,我们的重点是把栈的两种主要实现方式学透。

二、基于数组实现栈 (静态栈)

使用数组实现栈是最直观的方式之一。其核心是利用数组的连续内存空间来存储数据,并用一个指针(或索引)来时刻指向栈顶。

2.1 设计思路

  1. 底层容器:使用一个固定大小的数组来存放栈内元素。
  2. 栈顶指针:定义一个变量,比如 top 或者 size,来跟踪栈顶的位置。这里我们采用 size 变量,它既能表示栈中元素的数量,也能通过 size - 1 定位到栈顶元素的索引。
  3. 容量限制:由于数组长度固定,这种实现的栈是有容量限制的,我们称之为“静态栈”。当栈满时,无法再进行 push 操作(称为“上溢”);当栈空时,无法进行 pop 操作(称为“下溢”)。

2.2 核心操作实现 (以 Java 为例)

下面我们用 Java 语言实现一个基于数组的泛型栈。

// 使用泛型 E,可以存储任意类型的元素
public class ArrayStack<E> {private E[] data;    // 底层数组private int size;    // 栈中元素的数量,也指向下一个待插入位置的索引/*** 构造函数,传入栈的容量* @param capacity 栈的初始容量*/public ArrayStack(int capacity) {// 由于 Java 不支持直接 new E[],需要一个类型转换data = (E[]) new Object[capacity];size = 0;}/*** 获取栈中元素的个数*/public int getSize() {return size;}/*** 判断栈是否为空*/public boolean isEmpty() {return size == 0;}/*** 获取栈的容量*/public int getCapacity() {return data.length;}/*** 入栈操作* @param e 待入栈的元素*/public void push(E e) {// 检查栈是否已满if (size == data.length) {throw new IllegalArgumentException("Push failed. Stack is full.");}// 在 size 指向的位置放入新元素data[size] = e;// 维护 sizesize++;}/*** 出栈操作* @return 栈顶元素*/public E pop() {// 检查栈是否为空if (isEmpty()) {throw new IllegalArgumentException("Pop failed. Stack is empty.");}// 取出栈顶元素(索引为 size - 1)E ret = data[size - 1];// 维护 sizesize--;// [可选优化] 将出栈位置置为null,帮助垃圾回收 (Loitering objects)data[size] = null; return ret;}/*** 查看栈顶元素* @return 栈顶元素*/public E peek() {if (isEmpty()) {throw new IllegalArgumentException("Peek failed. Stack is empty.");}return data[size - 1];}
}

2.2.1 时间复杂度分析

  • push(e): O(1)O(1)O(1)。仅涉及一次数组赋值和一次整数自增。
  • pop(): O(1)O(1)O(1)。仅涉及一次数组访问和一次整数自减。
  • peek(): O(1)O(1)O(1)。仅涉及一次数组访问。
  • isEmpty() / getSize(): O(1)O(1)O(1)

可见,基于数组实现的栈,其核心操作效率非常高。

2.3 优缺点分析

特性优点缺点
存储方式内存连续,数据紧凑容量固定,无法动态扩展(除非实现复杂的动态扩容机制)
性能缓存友好性好(Cache Locality),访问速度快若实现动态扩容,单次 push 操作可能导致 O(n)O(n)O(n) 的时间复杂度
空间无需额外指针开销,空间利用率高可能造成空间浪费(分配了很大容量但实际使用很少)
实现逻辑简单,易于实现需要处理栈满(上溢)的边界情况

三、基于链表实现栈 (动态栈)

为了克服数组实现栈的固定容量问题,我们可以使用链表作为底层结构。链表天然支持动态增删,非常适合实现一个容量可变的“动态栈”。

3.1 设计思路

  1. 底层容器:使用一个单向链表。
  2. 栈顶:将链表的头结点作为栈顶。这是关键设计点,因为对链表头部的增删操作时间复杂度都是 O(1)O(1)O(1)。如果选择链表尾部作为栈顶,push 操作需要遍历整个链表找到尾部,时间复杂度为 O(n)O(n)O(n),效率低下。
  3. 动态容量:链表在内存中按需分配节点,因此没有固定的容量限制,只要内存足够,就可以一直 push

下面是一个链表栈操作的示意图:

再执行 pop()
执行 push(D)
初始状态 (Stack: A -> B -> C)
A
栈顶
B
C
null
弹出 D
D
栈顶
A
B
C
null
A
栈顶
B
C
null

3.2 核心操作实现 (以 Java 为例)

我们同样用 Java 实现一个基于链表的泛型栈。

public class LinkedListStack<E> {// 内部私有节点类private class Node {public E e;public Node next;public Node(E e, Node next) {this.e = e;this.next = next;}public Node(E e) {this(e, null);}}private Node head;  // 链表头结点,即为栈顶private int size;   // 栈中元素的数量public LinkedListStack() {head = null;size = 0;}/*** 获取栈中元素的个数*/public int getSize() {return size;}/*** 判断栈是否为空*/public boolean isEmpty() {return size == 0;}/*** 入栈操作* @param e 待入栈的元素*/public void push(E e) {// 创建一个新节点,让它的 next 指向当前的 headNode newNode = new Node(e, head);// 更新 head 为这个新节点head = newNode;// 等价于下面两行代码:// Node node = new Node(e);// node.next = head;// head = node;size++;}/*** 出栈操作* @return 栈顶元素*/public E pop() {if (isEmpty()) {throw new IllegalArgumentException("Pop failed. Stack is empty.");}// 保存待删除的头结点Node retNode = head;// head 指向下一个节点head = head.next;// 断开原头结点的链接(便于垃圾回收)retNode.next = null;size--;return retNode.e;}/*** 查看栈顶元素* @return 栈顶元素*/public E peek() {if (isEmpty()) {throw new IllegalArgumentException("Peek failed. Stack is empty.");}return head.e;}
}

3.2.1 时间复杂度分析

  • push(e): O(1)O(1)O(1)。仅涉及一次新节点的创建和一次引用的改变。
  • pop(): O(1)O(1)O(1)。仅涉及一次引用的改变。
  • peek(): O(1)O(1)O(1)。仅涉及一次 head 节点的访问。
  • isEmpty() / getSize(): O(1)O(1)O(1)

可以看到,基于链表实现的栈,核心操作效率同样非常高。

3.3 优缺点分析

特性优点缺点
存储方式内存非连续,动态分配每个元素都需要额外的指针开销
性能增删操作稳定在 O(1)O(1)O(1)缓存不友好,理论上访问速度可能略慢于数组
空间按需分配,没有容量限制相比数组,每个元素占用的总内存稍大
实现无需处理栈满的情况涉及指针操作,实现逻辑相对数组稍复杂

四、两种实现方式的对比

现在,让我们将这两种实现方式放在一起进行一个全面的对比,以便在实际开发中做出正确的选择。

对比项基于数组的栈 (ArrayStack)基于链表的栈 (LinkedStack)
底层结构连续内存的数组非连续内存的链表节点
容量固定,需预先指定动态,无容量限制(受限于内存)
核心操作
(push, pop, peek)
均为 O(1)O(1)O(1)均为 O(1)O(1)O(1)
内存使用元素本身占用空间。可能因预分配过大而浪费每个元素 + 一个指针的开销
缓存性能 (Cache-friendly),因为数据连续,因为节点在内存中可能分散
适用场景栈的最大容量已知可预估时;对性能和内存局部性有极致要求的场景栈的容量未知变化剧烈时;不希望处理扩容逻辑的场景
主要问题栈上溢 (Stack Overflow)指针的额外开销

选择建议
在大多数高级编程语言中,内置的 Stack 类(如 Java 的 java.util.Stack,虽然已不推荐使用,推荐用 Deque)底层往往是基于动态数组(如 VectorArrayList)实现的。这是一种折中方案:兼具数组的缓存优势,同时通过动态扩容机制解决了固定容量的问题,尽管扩容时会有性能抖动。

对于我们自己实现而言:

  • 如果能确定数据规模上限,且对性能要求苛刻,数组栈是更好的选择。
  • 如果不确定数据规模,或希望实现更灵活、平滑的内存增长,链表栈是更省心的选择。

五、总结

今天,我们系统地学习了栈这一重要的数据结构,现在对全文核心内容进行梳理:

  1. 栈的核心:栈是一种遵循后进先出 (LIFO) 原则的受限线性表,所有操作都在栈顶进行。
  2. 核心操作:栈的 ADT 主要包括 push(入栈)、pop(出栈)、peek(查看栈顶)、isEmpty(判空)和 size(大小),它们的时间复杂度都应为 O(1)O(1)O(1)
  3. 数组实现:基于数组的静态栈实现简单,缓存友好,但容量固定。适用于数据规模可预知的场景。
  4. 链表实现:基于链表的动态栈容量灵活,无上溢风险,但有额外的指针开销。适用于数据规模不确定的场景。
  5. 技术权衡:在选择实现方式时,需在空间效率时间效率实现复杂度之间做出权衡。

掌握了栈的底层原理和实现方法后,你就拥有了一把解决许多经典算法问题的利器。在下一篇文章中,我们将进入实战环节,运用今天所学的栈来解决括号匹配、表达式求值等经典问题,敬请期待!


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

相关文章:

  • 奔图P2500NW打印机加碳粉方法
  • 《Transformer黑魔法Mask与Softmax、Attention的关系:一个-∞符号如何让AI学会“选择性失明“》
  • 深入理解 qRegisterMetaType<T>()
  • DAY32打卡
  • 字符输入流—read方法
  • Kotlin Native调用C curl
  • 内部类详解:Java中的嵌套艺术
  • WebView 中控制光标
  • Diamond基础1:认识Lattice器件
  • 数据结构 二叉树(1)二叉树简单了解
  • Linux学习-数据结构(栈和队列)
  • 8.6学习总结
  • Selenium在Pyhton应用
  • Java 大视界 -- Java 大数据机器学习模型在电商用户生命周期价值评估与客户关系精细化管理中的应用(383)
  • 应急响应排查(windows版)
  • Vue计算属性详解2
  • Python Pandas.lreshape函数解析与实战教程
  • 机器学习模型调优实战指南
  • 关于应急响应的那些事
  • 第14届蓝桥杯Scratch选拔赛初级及中级(STEMA)真题2023年3月12日
  • 人工智能-python-机器学习实战:特征降维、PCA与KNN的核心价值解析
  • Linux: NFS 服务部署与autofs自动挂载的配置
  • 分隔串处理方法
  • SQL注入SQLi-LABS 靶场less51-57详细通关攻略
  • 【2026版】JVM面试题
  • K8S的POD数量限制
  • 敏捷协作平台推荐:Jira、PingCode、Tapd等15款
  • C++ - 仿 RabbitMQ 实现消息队列--网络通信协议设计
  • 力扣-42.接雨水
  • 云平台托管集群:EKS、GKE、AKS 深度解析与选型指南-第二章