菜单

栈和队列

相关源文件

本文档提供了 Hello Algo 项目中实现的栈和队列数据结构的概述。这些基本线性数据结构以特定的访问模式管理元素集合。有关作为底层实现的数组和链表的信息,请参阅数组和链表

介绍

栈和队列是抽象数据类型(ADT),它们限制了元素的访问方式,为不同场景提供了专门的行为。

  • :一种后进先出(LIFO)结构,元素在同一端添加和移除。
  • 队列:一种先进先出(FIFO)结构,元素在一端添加,在另一端移除。
  • 双端队列:一种允许在两端进行插入和移除操作的双端队列。

这些结构可以使用数组或链表实现,在内存使用和时间复杂度方面各有权衡。

来源:codes/c/chapter_stack_and_queue/array_stack.c codes/c/chapter_stack_and_queue/linkedlist_queue.c codes/python/chapter_stack_and_queue/linkedlist_deque.py

栈数据结构

栈使用后进先出(LIFO)原则来限制元素的访问。就像一叠盘子一样,你只能从顶部添加或移除。

栈操作

栈的关键操作包括:

操作描述时间复杂度
推送向顶部添加元素O(1)
弹栈(Pop)移除并返回顶部元素O(1)
查看/顶部返回顶部元素而不移除它O(1)
isEmpty检查栈是否为空O(1)
size返回元素数量O(1)

来源:codes/python/chapter_stack_and_queue/array_stack.py11-37 codes/c/chapter_stack_and_queue/array_stack.c33-66

栈实现

栈可以用数组或链表实现,各有优缺点。

基于数组的栈

数组实现通过一个大小变量(作为数组索引)来跟踪栈顶。

在 C 语言中,ArrayStack 结构定义为:

基于数组的栈的关键考虑因素:

  • 简单的内存布局
  • 操作速度快,时间复杂度为 O(1)
  • 如果数组已满,可能需要调整大小
  • 内存是预先分配的(如果未被使用,可能会造成浪费)

来源:codes/c/chapter_stack_and_queue/array_stack.c11-66 codes/python/chapter_stack_and_queue/array_stack.py8-42

基于链表的栈

链表实现使用一个指针来跟踪栈顶节点,新元素被添加为链表的新的头部。

在 C 语言中,LinkedListStack 结构定义为:

基于链表的栈的关键考虑因素:

  • 动态内存分配
  • 无需调整大小
  • 指针有额外的内存开销
  • 由于指针间接寻址,速度稍慢

来源:codes/c/chapter_stack_and_queue/linkedlist_stack.c9-70 codes/python/chapter_stack_and_queue/linkedlist_stack.py14-48

队列数据结构

队列使用先进先出(FIFO)原则来限制元素的访问。就像排队的人一样,元素在队尾加入,在队头离开。

队列操作

队列的关键操作包括:

操作描述时间复杂度
入队/Push添加到队尾O(1)
出队/Pop移除并返回队头元素O(1)
查看/队头返回队头元素而不移除它O(1)
isEmpty检查队列是否为空O(1)
size返回元素数量O(1)

来源:codes/python/chapter_stack_and_queue/array_queue.py8-52 codes/c/chapter_stack_and_queue/array_queue.c33-75

队列实现

队列可以使用数组或链表实现。

基于数组的队列:循环数组实现

基于数组的队列使用**循环数组**设计来高效利用内存。这需要同时跟踪队头位置和大小(或队尾位置)。

在 C 语言中,ArrayQueue 结构定义为:

循环数组实现的关键方面:

  • 通过循环实现高效的内存利用
  • 避免了在出队操作中移动元素
  • 使用模运算处理循环:(front + size) % capacity
  • 所有操作均保持 O(1) 的时间复杂度

来源:codes/c/chapter_stack_and_queue/array_queue.c9-75 codes/python/chapter_stack_and_queue/array_queue.py8-61 codes/javascript/chapter_stack_and_queue/array_queue.js8-69

基于链表的队列

队列的链表实现同时维护了链表的前后指针,允许在 O(1) 时间内完成入队和出队操作。

在 C 语言中,LinkedListQueue 结构定义为:

基于链表的队列的关键考虑因素:

  • 动态内存分配
  • 无需进行循环计算
  • 分离的前后指针使得在两端进行 O(1) 操作成为可能
  • 指针有额外的内存开销

来源:codes/c/chapter_stack_and_queue/linkedlist_queue.c9-77 codes/python/chapter_stack_and_queue/linkedlist_queue.py14-58

双端队列 (Deque)

双端队列(发音为“deck”)是一种允许在队头和队尾进行插入和移除操作的队列。

双端队列操作

双端队列的关键操作包括:

操作描述时间复杂度
pushFirst添加到队头O(1)
pushLast添加到队尾O(1)
popFirst移除并返回队头元素O(1)
popLast移除并返回队尾元素O(1)
peekFirst返回队头元素而不移除它O(1)
peekLast返回队尾元素而不移除它O(1)
isEmpty检查双端队列是否为空O(1)
size返回元素数量O(1)

来源:codes/python/chapter_stack_and_queue/linkedlist_deque.py18-107 codes/c/chapter_stack_and_queue/array_deque.c30-124 codes/python/chapter_stack_and_queue/array_deque.py8-86

双端队列的实现

双端队列可以使用数组或链表实现,各有考虑。

基于数组的双端队列

与循环数组队列类似,基于数组的双端队列使用模运算来处理循环。

在 C 语言中,ArrayDeque 结构定义为:

基于数组的双端队列的关键考虑因素:

  • 使用循环数组概念,允许在两端高效进行操作
  • 需要使用模运算进行仔细的索引计算
  • 比简单的队列实现更复杂
  • 固定容量,满时需要调整大小

来源:codes/c/chapter_stack_and_queue/array_deque.c9-124 codes/python/chapter_stack_and_queue/array_deque.py8-86

基于链表的双端队列

对于双端队列,双向链表特别有用,因为它允许在两端高效进行操作。C 语言的实现使用了以下结构:

其中 DoublyListNode 包含前向和后向指针。

基于链表的双端队列的关键考虑因素:

  • 使用双向链表,允许在两端高效进行操作
  • 没有容量限制
  • 更复杂的节点结构,包含 prev 和 next 指针
  • 每个元素有更高的内存开销

来源: codes/c/chapter_stack_and_queue/linkedlist_deque.c9-152 codes/python/chapter_stack_and_queue/linkedlist_deque.py8-107

实际应用

栈的应用

  1. 函数调用管理:编程语言运行时使用栈来管理函数调用和返回。
  2. 表达式求值:栈用于求值算术表达式,特别是带括号的表达式。
  3. 撤销/重做操作:应用程序使用栈来跟踪状态更改,以实现撤销/重做功能。
  4. 回溯算法:栈在回溯算法中很有用,例如迷宫求解和深度优先搜索。
  5. 语法解析:编译器使用栈进行语法解析和检查括号匹配。

队列的应用

  1. 作业调度:操作系统使用队列进行进程调度、打印作业管理等。
  2. 广度优先搜索:队列在图遍历的 BFS 算法中至关重要。
  3. 请求缓冲: Web 服务器使用队列来处理传入的请求。
  4. 消息缓冲区:消息队列支持系统组件之间的异步通信。
  5. 缓存实现:一些缓存替换算法使用队列。

双端队列的应用

  1. 任务调度:一些调度算法(如工作窃取)使用双端队列。
  2. 滑动窗口问题:双端队列对于滑动窗口最大/最小值问题非常有效。
  3. A-Steal 调度算法:用于并行处理中的负载均衡。
  4. 浏览器历史记录:浏览器中的前进和后退导航。
  5. 回文检查:使用双端队列可以检查字符串是否是回文。

实现选择指南

考量因素数组实现链表实现
内存效率已知大小更佳未知大小更佳
内存开销每个元素的开销较低较高(指针)
访问模式更好的缓存局部性内存中更分散
插入/删除可能需要调整大小使用适当的指针始终为 O(1)
实现复杂度栈更简单,循环队列更复杂队列更简单,双端队列更复杂

根据您的具体要求进行选择

  • 当以下情况时,使用基于数组的实现

    • 已知最大大小或可以合理估计
    • 内存效率至关重要
    • 缓存性能很重要
  • 当以下情况时,使用基于链表的实现

    • 大小可能发生显著变化
    • 频繁调整大小效率低下
    • 指针开销可接受

来源:stack_and_queue 目录中的所有文件