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

栈(Stack)

栈(Stack)详解

基本概念与特性

栈是一种遵循后进先出(LIFO,Last In First Out)原则的抽象数据类型,属于线性数据结构。其操作方式类似于现实生活中的一叠盘子:

  • 入栈(Push):将新元素添加到栈顶,相当于在最上面放一个新盘子
  • 出栈(Pop):移除并返回栈顶元素,相当于从最上面取走一个盘子
  • 栈顶(Top):指向最新插入的元素位置,总是指向当前可操作的元素
  • 栈底(Bottom):存放最早插入的元素,通常作为基准位置

栈的所有基本操作(push、pop、peek等)的时间复杂度均为O(1),这种极高的效率使其成为算法设计中重要的基础数据结构。

实现方式

数组实现(顺序栈)

实现要点

顺序栈使用连续的内存空间(数组)存储元素:

  • 预分配固定长度的数组作为存储空间
  • 通过整型变量top指示当前栈顶位置(初始值为-1表示空栈)
  • 栈满条件:top == 数组长度-1(数组索引从0开始)
  • 栈空条件:top == -1(无元素状态)
示例代码(C语言)
#define MAX_SIZE 100  // 预定义栈的最大容量typedef struct {int data[MAX_SIZE];  // 存储栈元素的数组int top;             // 栈顶指针
} ArrayStack;// 入栈操作
void push(ArrayStack *s, int value) {if (s->top == MAX_SIZE - 1) {  // 检查栈是否已满printf("Stack Overflow\n");return;}s->data[++s->top] = value;  // 先移动指针再存储值
}// 出栈操作
int pop(ArrayStack *s) {if (s->top == -1) {  // 检查栈是否为空printf("Stack Underflow\n");return INT_MIN;  // 返回最小值表示错误}return s->data[s->top--];  // 返回当前值再移动指针
}
优缺点分析
  • 优点:
    • 实现简单直观,易于理解
    • 内存连续分配,缓存命中率高(空间局部性好)
    • 直接索引访问,操作速度快
  • 缺点:
    • 需要预先分配固定大小的空间
    • 扩容时需要重新分配内存并复制元素,代价高昂
    • 当存储需求远小于分配空间时,内存利用率低

链表实现(链式栈)

实现要点

链式栈使用动态分配的节点存储元素:

  • 每个节点包含数据域和指向下一个节点的指针
  • 栈顶即链表头节点(插入和删除都在链表头部进行)
  • 不需要预先分配固定空间,可以动态增长
  • 栈空条件:top指针为NULL
示例代码(Java)
// 节点类定义
class Node {int data;       // 存储数据Node next;      // 指向下一个节点的指针public Node(int data) {this.data = data;this.next = null;}
}// 链栈实现
class LinkedStack {private Node top;  // 栈顶指针// 入栈操作public void push(int value) {Node newNode = new Node(value);  // 创建新节点newNode.next = top;              // 新节点指向原栈顶top = newNode;                   // 更新栈顶指针}// 出栈操作public int pop() {if (top == null) {throw new EmptyStackException();  // 栈空异常}int value = top.data;   // 保存栈顶数据top = top.next;         // 移动栈顶指针return value;}// 查看栈顶元素public int peek() {if (top == null) {throw new EmptyStackException();}return top.data;}
}
优缺点分析
  • 优点:
    • 动态内存分配,不需要预先设置大小
    • 理论上只要内存足够就不会出现栈满情况
    • 内存使用效率高(按需分配)
  • 缺点:
    • 每个节点需要额外存储指针,空间开销较大
    • 内存不连续,缓存性能可能较差
    • 访问非栈顶元素需要遍历,效率低下
    • 频繁的内存分配/释放可能影响性能

典型应用场景

括号匹配

完整算法流程
  1. 初始化一个空栈
  2. 从左到右遍历输入字符串:
    • 遇到任意左括号(‘(’、‘[’、‘{’)则将其压入栈中
    • 遇到右括号时:
      a. 首先检查栈是否为空(栈空则说明右括号无匹配)
      b. 弹出栈顶元素并检查是否与当前右括号匹配
      c. 不匹配则直接返回错误
  3. 字符串遍历完成后:
    • 检查栈是否为空(栈非空说明有未匹配的左括号)
    • 栈空则匹配成功,否则匹配失败
详细示例分析

以字符串"{([])}"为例:

  1. 遇到’{’ → 压入栈 → 栈:[‘{’]
  2. 遇到’(’ → 压入栈 → 栈:[‘{’, ‘(’]
  3. 遇到’[’ → 压入栈 → 栈:[‘{’, ‘(’, ‘[’]
  4. 遇到’]’ → 弹出’[’ → 检查匹配(‘[‘和’]‘匹配) → 栈:[’{’, ‘(’]
  5. 遇到’)’ → 弹出’(’ → 检查匹配(‘(‘和’)‘匹配) → 栈:[’{’]
  6. 遇到’}’ → 弹出’{’ → 检查匹配('{‘和’}'匹配) → 栈:[]
  7. 字符串遍历结束且栈空 → 匹配成功

表达式求值

中缀转后缀详细算法(逆波兰表示法)
  1. 初始化一个输出队列和一个操作符栈
  2. 从左到右扫描中缀表达式:
    • 遇到操作数:直接加入输出队列
    • 遇到左括号:压入操作符栈
    • 遇到右括号:
      a. 不断弹出栈顶元素加入输出队列
      b. 直到遇见左括号(左括号弹出但不输出)
    • 遇到运算符:
      a. 比较与栈顶运算符的优先级
      b. 弹出栈顶优先级≥当前运算符的元素
      c. 将当前运算符压入栈
  3. 表达式处理后弹出栈中剩余运算符
后缀表达式求值步骤
  1. 初始化操作数栈
  2. 从左到右扫描后缀表达式:
    • 遇到数字:压入操作数栈
    • 遇到运算符:
      a. 弹出栈顶两个操作数(注意顺序)
      b. 执行运算(次顶元素 op 栈顶元素)
      c. 将结果压回栈中
  3. 最终栈中剩余的唯一元素即为表达式结果
完整计算示例

中缀表达式:3 + 4 × (2 - 1)

  1. 转换为后缀表达式:

    • 3 → 输出
      • → 压栈
    • 4 → 输出 → 队列:[3,4]
    • × → 优先级>+,压栈 → 栈:[‘+’,‘×’]
    • ( → 压栈 → 栈:[‘+’,‘×’,‘(’]
    • 2 → 输出 → 队列:[3,4,2]
      • → 压栈 → 栈:[‘+’,‘×’,‘(’,‘-’]
    • 1 → 输出 → 队列:[3,4,2,1]
    • ) → 弹出直到’(’ → 队列:[3,4,2,1,-] → 栈:[‘+’,‘×’]
    • 结束 → 弹出剩余 → 队列:[3,4,2,1,-,×,+]
  2. 后缀表达式求值:

    • 初始栈:[]
    • 3 → [3]
    • 4 → [3,4]
    • 2 → [3,4,2]
    • 1 → [3,4,2,1]
      • → 弹出1,2 → 2-1=1 → [3,4,1]
    • × → 弹出1,4 → 4×1=4 → [3,4]
      • → 弹出4,3 → 3+4=7 → [7]
  3. 最终结果:7

其他重要应用

  1. 函数调用栈

    • 保存函数调用的返回地址、参数、局部变量等上下文信息
    • 递归调用本质上是栈的应用,递归深度受限于栈大小
    • 栈溢出常见于递归未设置终止条件或递归过深
  2. 浏览器历史记录管理

    • 访问新页面时URL入栈(主栈)
    • 点击后退时当前页出栈并进入辅助栈
    • 前进时从辅助栈取回URL
    • 新访问时清空辅助栈
  3. 文本编辑器的撤销(Undo)机制

    • 每个编辑操作作为命令对象入栈
    • 执行撤销时弹出栈顶命令并执行逆操作
    • 通常配合重做(Redo)栈实现完整的撤销/重做功能
  4. 深度优先搜索(DFS)

    • 用栈保存待访问节点
    • 每次处理栈顶节点并将其邻接节点入栈
    • 避免递归带来的栈溢出风险

变体与扩展

  1. 最小栈

    • 在O(1)时间内获取当前栈中最小值
    • 实现方式:
      a. 使用辅助栈同步记录最小值
      b. 节点额外存储当前最小值
    • 示例:压入[3,5,1]时,主栈[3,5,1],辅助栈[3,3,1]
  2. 双栈队列

    • 用两个栈模拟队列的FIFO特性
    • 入队栈负责接收新元素
    • 出队栈为空时将入队栈元素全部转移
    • 出队操作始终从出队栈弹出
  3. 共享栈

    • 两个栈共享同一存储空间
    • 一个栈从数组起始位置向末端增长
    • 另一个栈从数组末端向起始位置增长
    • 空间利用率高,适合两栈空间需求相反的场景

常见面试题

  1. 最小栈实现

    • 设计支持push、pop、top和getMin操作的栈
    • getMin需在O(1)时间内完成
  2. 用栈实现队列

    • 使用两个栈模拟队列的先进先出特性
    • 重点在于元素转移的时机控制
  3. 有效的括号

    • 检查字符串中的括号是否匹配
    • 需处理三种括号类型:()、[]、{}
  4. 逆波兰表达式求值

    • 根据后缀表达式计算结果
    • 注意操作数顺序和除法处理
  5. 栈的压入、弹出序列

    • 判断给定弹出序列是否可能是压入序列的合法弹出顺序

性能考量

  1. 栈溢出防护

    • 递归算法必须有明确的终止条件
    • 深度较大时考虑改用迭代算法
    • 必要时调整系统栈大小
  2. 缓存优化

    • 数组实现比链表实现具有更好的缓存局部性
    • 频繁操作时数组栈性能通常更优
  3. 线程安全

    • 多线程环境下需要同步控制
    • 常见解决方案:
      a. 使用锁机制
      b. 采用线程局部存储
      c. 使用并发栈实现
  4. 内存管理

    • 链表实现需注意及时释放出栈节点
    • 防止内存泄漏和野指针问题
    • 可考虑对象池技术优化频繁分配释放
http://www.dtcms.com/a/398747.html

相关文章:

  • 整体设计 逻辑全链 之8 受控的自然语言-字面拼凑:正则表达式 之2
  • 攻防世界-Web-simple_php
  • 【Linux我做主】进程程序替换和exec函数族
  • 清华最新发布 | 大型推理模型的强化学习综述
  • C++异常处理的根本缺陷:隐式传播的性能陷阱与控制流断裂
  • 【东枫】USRP X310 母版 PCB
  • 山东锦华建设集团有限公司网站嘉瑞建设有限公司网站
  • 食品品牌网站策划美容行业培训网站建设
  • Amazon Timestream新用户实时分析:从零到上手完整指南
  • 淘宝联盟个人网站怎么做电商平台入驻
  • 在 Oracle SQL 中实现 `IF-ELSE` 逻辑 SQL 错误 [12704] [72000]: ORA-12704: 字符集不匹配
  • 勒索软件专攻数据库弱点:Oracle 服务器安全防线告急
  • 常用的表空间维护语句
  • MySQL笔记---数据库基础
  • 【数据迁移】:oracle 大数据上线失败复盘:【大表定义变更】不一致导致生产数据灌入失败及解决方案
  • InnoDB一致性读与锁定读全解析
  • Oracle归档及数据库存储空间查询
  • 怎么用wordpress建外贸网站华丽的网站模板
  • 如何在Linux系统里将新添加磁盘进行分区挂载
  • 公司网站案例免费域名建站
  • 抓包解析MCP协议:基于JSON-RPC的MCP host与MCP server的交互
  • 一“网”跨协议,万“设”皆可通!耐达讯自动化Modbus TCP转Profibus ,让控制无界,让能源有道。
  • 江门网站优化公司衡水seo网站建设优化排名
  • [2025CVPR-域泛化方向]:通过改进损失景观实现更好的域泛化
  • 网站开发商怎么关闭图片显示公司网站费怎么做分录
  • ABAC权限模型实战:实现“上班才能访问财务系统”!
  • 深入解析:使用递归计算整数各位数字之和的C语言实现
  • 第1章:初识Linux系统——第4节:文件操作命令2
  • 众云网联做的网站效果好吗深圳网站设计公司排名榜
  • wordpress修改教程网站优化制作公司代理