引言
参照的是左程云的课程:https://space.bilibili.com/8888480/lists/3509640?type=series
本笔记包括了栈和队列的基础实现、相互转换以及最小栈等核心内容,涵盖了链表实现、数组实现、循环队列等多种实现方式,包括了class013 -> class016的内容。
013【入门】队列和栈-链表、数组实现
队列的基本概念
队列(Queue)是一种先进先出(FIFO, First In First Out)的线性数据结构。元素从队尾(rear)插入,从队首(front)删除。
队列的基本操作
- enqueue/offer: 入队,将元素添加到队尾
- dequeue/poll: 出队,从队首移除元素
- front/peek: 查看队首元素,但不移除
- isEmpty: 判断队列是否为空
- size: 获取队列中元素个数
队列的实现方式
方式一:基于双端队列(deque)实现
Python内置的collections.deque提供了高效的双端操作,但常数时间较慢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| from collections import deque
class Queue1: """ 基于Python内置deque实现的队列 内部使用双向链表,常数操作较慢但使用简单 """ def __init__(self): self.queue = deque()
def is_empty(self): """判断队列是否为空""" return not self.queue
def offer(self, num): """向队列中加入元素,加到队尾""" self.queue.append(num)
def poll(self): """从队列头部弹出元素""" return self.queue.popleft()
def peek(self): """返回队列头的元素但不弹出""" return self.queue[0]
def size(self): """返回队列中元素个数""" return len(self.queue)
|
方式二:基于固定数组实现
在已知操作次数上限的情况下,使用固定数组实现具有更好的常数时间性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class Queue2: """ 基于固定数组实现的队列 适用于已知加入操作总次数上限的场景 常数时间性能更好,是实际刷题中最常用的方式 """ def __init__(self, n): """ 初始化队列 参数: n - 加入操作的总次数上限 """ self.queue = [0] * n self.l = 0 self.r = 0
def is_empty(self): """判断队列是否为空""" return self.l == self.r
def offer(self, num): """入队操作""" self.queue[self.r] = num self.r += 1
def poll(self): """出队操作""" num = self.queue[self.l] self.l += 1 return num
def head(self): """返回队首元素""" return self.queue[self.l]
def tail(self): """返回队尾元素""" return self.queue[self.r - 1]
def size(self): """队列当前元素个数,区间[l, r)""" return self.r - self.l
|
栈的基本概念
栈(Stack)是一种后进先出(LIFO, Last In First Out)的线性数据结构。元素只能从栈顶插入和删除。
栈的基本操作
- push: 压栈,将元素添加到栈顶
- pop: 弹栈,从栈顶移除元素
- peek/top: 查看栈顶元素,但不移除
- isEmpty: 判断栈是否为空
- size: 获取栈中元素个数
栈的实现方式
方式一:基于动态数组实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Stack1: """ 基于Python内置列表实现的栈 使用动态数组,常数时间不是最优但使用简单 """ def __init__(self): self.stack = []
def is_empty(self): """判断栈是否为空""" return len(self.stack) == 0
def push(self, num): """压栈操作""" self.stack.append(num)
def pop(self): """弹栈操作""" return self.stack.pop()
def peek(self): """返回栈顶元素但不弹出""" return self.stack[-1]
def size(self): """返回栈的大小""" return len(self.stack)
|
方式二:基于固定数组实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Stack2: """ 基于固定数组实现的栈 适用于已知同时在栈里元素个数上限的场景 常数时间性能更好,空间可以复用 """ def __init__(self, n): """ 初始化栈 参数: n - 同时在栈里的元素个数上限 """ self.stack = [0] * n self.size_ = 0
def is_empty(self): """判断栈是否为空""" return self.size_ == 0
def push(self, num): """压栈操作""" self.stack[self.size_] = num self.size_ += 1
def pop(self): """弹栈操作""" self.size_ -= 1 return self.stack[self.size_]
def peek(self): """返回栈顶元素但不弹出""" return self.stack[self.size_ - 1]
def size(self): """返回栈大小""" return self.size_
|
循环队列
循环队列是队列的一种特殊实现,通过循环使用固定大小的数组来避免空间浪费。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| class MyCircularQueue: """ 循环队列实现 测试链接: https://leetcode.cn/problems/design-circular-queue/ """ def __init__(self, k): """ 初始化循环队列 参数: k - 队列容量上限 """ self.queue = [0] * k self.l = 0 self.r = 0 self.size = 0 self.limit = k
def enQueue(self, value): """ 入队操作 返回: 成功返回True,队列满返回False """ if self.isFull(): return False else: self.queue[self.r] = value self.r = 0 if self.r == self.limit - 1 else self.r + 1 self.size += 1 return True
def deQueue(self): """ 出队操作 返回: 成功返回True,队列空返回False """ if self.isEmpty(): return False else: self.l = 0 if self.l == self.limit - 1 else self.l + 1 self.size -= 1 return True
def Front(self): """返回队首元素,队列为空返回-1""" if self.isEmpty(): return -1 else: return self.queue[self.l]
def Rear(self): """返回队尾元素,队列为空返回-1""" if self.isEmpty(): return -1 else: last = self.limit - 1 if self.r == 0 else self.r - 1 return self.queue[last]
def isEmpty(self): """判断队列是否为空""" return self.size == 0
def isFull(self): """判断队列是否已满""" return self.size == self.limit
|
014【入门】队列和栈入门题目-栈和队列相互实现
用栈实现队列
队列是先进先出(FIFO),而栈是后进先出(LIFO)。要用栈实现队列,需要使用两个栈来模拟队列的行为。
算法思想
使用两个栈:
- 输入栈(in_stack):负责接收新元素的push操作
- 输出栈(out_stack):负责输出元素的pop和peek操作
关键规则
- 倒数据条件:只有当输出栈为空时,才能从输入栈倒数据
- 倒数据原则:如果要倒数据,必须将输入栈的数据全部倒完
- 时间复杂度:虽然单次操作可能是O(n),但均摊时间复杂度是O(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class MyQueue: """ 用栈实现队列 测试链接: https://leetcode.cn/problems/implement-queue-using-stacks/ 时间复杂度: 均摊O(1) """ def __init__(self): self.in_stack = [] self.out_stack = []
def inToOut(self): """ 倒数据操作:从输入栈将数据倒入输出栈 核心规则: 1) 输出栈空了,才能倒数据 2) 如果倒数据,输入栈必须倒完 """ if not self.out_stack: while self.in_stack: self.out_stack.append(self.in_stack.pop())
def push(self, x: int): """入队操作:新元素加入输入栈""" self.in_stack.append(x) self.inToOut()
def pop(self) -> int: """出队操作:从输出栈弹出元素""" self.inToOut() return self.out_stack.pop()
def peek(self) -> int: """查看队首元素:不移除,只查看""" self.inToOut() return self.out_stack[-1]
def empty(self) -> bool: """判断队列是否为空""" return not self.in_stack and not self.out_stack
|
时间复杂度分析
虽然inToOut()操作在最坏情况下需要O(n)时间,但通过均摊分析:
- 每个元素最多被移动两次(输入栈→输出栈→出队列)
- n次操作的总时间复杂度为O(n)
- 均摊时间复杂度为O(1)
用队列实现栈
栈是后进先出(LIFO),队列是先进先出(FIFO)。要用队列实现栈,需要在每次push操作后重新排列队列中的元素。
算法思想
使用一个双端队列(deque),在每次push新元素后,将队列中原有的元素依次移动到新元素后面,确保新元素总是在队首位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from collections import deque
class MyStack: """ 用双端队列实现栈 测试链接: https://leetcode.cn/problems/implement-stack-using-queues/ """ def __init__(self): self.queue = deque()
def push(self, x: int): """ 压栈操作 时间复杂度: O(n) 核心思想: 新元素入队后,将前面所有元素重新排列到新元素后面 """ n = len(self.queue) self.queue.append(x) for _ in range(n): self.queue.append(self.queue.popleft())
def pop(self) -> int: """弹栈操作:弹出队首元素,即栈顶元素""" return self.queue.popleft()
def top(self) -> int: """查看栈顶元素:返回队首元素""" return self.queue[0]
def empty(self) -> bool: """判断栈是否为空""" return not self.queue
|
操作示例
假设依次push元素1, 2, 3:
1 2 3 4 5 6 7
| 初始状态: []
push(1): [1]
push(2): [2] -> [2,1] (将1移动到2后面)
push(3): [3,2,1] -> [3,2,1] (将2,1依次移动到3后面)
|
最终队列状态为[3,2,1],队首元素3就是栈顶元素,符合LIFO特性。
015【入门】栈的入门题目-最小栈
最小栈问题
最小栈要求实现一个栈,除了基本的栈操作外,还要能够在O(1)时间内获取栈中的最小元素。
问题分析
核心挑战是如何在保持基本栈操作O(1)时间复杂度的同时,追踪当前栈中的最小值。当栈顶元素(恰好是最小值)被弹出时,需要快速知道剩余元素中的最小值。
解决方案:辅助栈法
使用两个栈:
- 数据栈(data):存储实际数据
- 最小值栈(min):存储对应位置的最小值
实现方法一:基于列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class MinStack1: """ 最小栈实现方法一:使用Python列表 测试链接: https://leetcode.cn/problems/min-stack/ 时间复杂度: 所有操作均为O(1) """ def __init__(self): self.data = [] self.min = []
def push(self, val): """ 压栈操作 核心思想: 每次压栈时,同时在最小值栈中记录当前的最小值 """ self.data.append(val) if not self.min or val <= self.min[-1]: self.min.append(val) else: self.min.append(self.min[-1])
def pop(self): """弹栈操作:同时弹出两个栈的栈顶元素""" self.data.pop() self.min.pop()
def top(self): """获取栈顶元素""" return self.data[-1]
def getMin(self): """获取栈中最小元素""" return self.min[-1]
|
工作原理示例
假设依次压入元素:5, 2, 7, 1, 3
| 操作 |
数据栈 |
最小栈 |
说明 |
| push(5) |
[5] |
[5] |
5是第一个元素,也是最小值 |
| push(2) |
[5,2] |
[5,2] |
2比5小,成为新的最小值 |
| push(7) |
[5,2,7] |
[5,2,2] |
7比2大,最小值仍是2 |
| push(1) |
[5,2,7,1] |
[5,2,2,1] |
1比2小,成为新的最小值 |
| push(3) |
[5,2,7,1,3] |
[5,2,2,1,1] |
3比1大,最小值仍是1 |
实现方法二:基于固定数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class MinStack2: """ 最小栈实现方法二:使用固定大小数组 适用于已知最大容量的场景,常数时间性能更好 """ def __init__(self): self.MAXN = 8001
self.data = [0] * self.MAXN self.min = [0] * self.MAXN self.size = 0
def push(self, val): """压栈操作""" self.data[self.size] = val if self.size == 0 or val <= self.min[self.size - 1]: self.min[self.size] = val else: self.min[self.size] = self.min[self.size - 1] self.size += 1
def pop(self): """弹栈操作:只需将size减1,不需要实际删除数据""" self.size -= 1
def top(self): """获取栈顶元素""" return self.data[self.size - 1]
def getMin(self): """获取栈中最小元素""" return self.min[self.size - 1]
|
复杂度分析
时间复杂度
- push操作:O(1) - 只需要常数次比较和赋值
- pop操作:O(1) - 只需要移动指针或减少计数
- top操作:O(1) - 直接访问数组元素
- getMin操作:O(1) - 直接访问最小值栈顶
空间复杂度
- 总空间复杂度:O(n) - 需要两个栈存储数据
- 额外空间:O(n) - 最小值栈的空间开销
优化思考
虽然辅助栈法简单易懂,但存在空间冗余。可以考虑以下优化:
- 稀疏存储:最小值栈只在最小值更新时才压入新值
- 差值存储:存储与最小值的差值而非绝对值
- 链表实现:在节点中直接存储当前最小值
数据结构选择指南
性能对比
| 实现方式 |
时间复杂度 |
空间复杂度 |
常数因子 |
适用场景 |
| Python内置容器 |
O(1)均摊 |
O(n) |
较大 |
快速原型,不追求极致性能 |
| 固定数组 |
O(1) |
O(n) |
较小 |
已知容量上限,追求性能 |
| 双栈/双队列 |
O(1)均摊 |
O(n) |
中等 |
功能转换,教学示例 |
016【入门】双端队列-双链表和固定数组实现
双端队列的基本概念
双端队列(Deque,Double-ended Queue)是一种特殊的线性数据结构,允许在队列的两端进行插入和删除操作。与普通队列只能在一端插入、另一端删除不同,双端队列提供了更大的灵活性。
双端队列的基本操作
- insertFront: 在队首插入元素
- insertLast: 在队尾插入元素
- deleteFront: 删除队首元素
- deleteLast: 删除队尾元素
- getFront: 获取队首元素
- getRear: 获取队尾元素
- isEmpty: 判断队列是否为空
- isFull: 判断队列是否已满
循环双端队列的实现
循环双端队列是双端队列的一种特殊实现,使用固定大小的数组并通过循环索引来管理队列的边界。
实现方式一:基于Python列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| class MyCircularDeque1: """ 基于Python列表实现的循环双端队列 内部使用动态数组,操作简单但常数时间较慢 测试链接: https://leetcode.cn/problems/design-circular-deque/ """ def __init__(self, k): """ 初始化循环双端队列 参数: k - 队列容量上限 """ self.deque = [] self.size = 0 self.limit = k
def insertFront(self, value): """ 在队首插入元素 时间复杂度: O(n) - 需要移动所有现有元素 """ if self.isFull(): return False else: self.deque.insert(0, value) self.size += 1 return True
def insertLast(self, value): """ 在队尾插入元素 时间复杂度: O(1)均摊 """ if self.isFull(): return False else: self.deque.append(value) self.size += 1 return True
def deleteFront(self): """ 删除队首元素 时间复杂度: O(n) - 需要移动所有剩余元素 """ if self.isEmpty(): return False else: self.size -= 1 self.deque.pop(0) return True
def deleteLast(self): """ 删除队尾元素 时间复杂度: O(1) """ if self.isEmpty(): return False else: self.size -= 1 self.deque.pop() return True
def getFront(self): """获取队首元素,队列为空返回-1""" if self.isEmpty(): return -1 else: return self.deque[0]
def getRear(self): """获取队尾元素,队列为空返回-1""" if self.isEmpty(): return -1 else: return self.deque[-1]
def isEmpty(self): """判断队列是否为空""" return self.size == 0
def isFull(self): """判断队列是否已满""" return self.size == self.limit
|
实现方式二:基于固定数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| class MyCircularDeque2: """ 基于固定数组实现的循环双端队列 使用循环索引管理队列边界,常数时间性能更好 适用于已知容量上限的场景 """ def __init__(self, k): """ 初始化循环双端队列 参数: k - 队列容量上限 """ self.deque = [0] * k self.l = 0 self.r = 0 self.size = 0 self.limit = k
def insertFront(self, value): """ 在队首插入元素 时间复杂度: O(1) """ if self.isFull(): return False else: if self.isEmpty(): self.l = self.r = 0 self.deque[0] = value else: self.l = self.l - 1 if self.l != 0 else self.limit - 1 self.deque[self.l] = value self.size += 1 return True
def insertLast(self, value): """ 在队尾插入元素 时间复杂度: O(1) """ if self.isFull(): return False else: if self.isEmpty(): self.l = self.r = 0 self.deque[0] = value else: self.r = 0 if self.r == self.limit - 1 else self.r + 1 self.deque[self.r] = value self.size += 1 return True
def deleteFront(self): """ 删除队首元素 时间复杂度: O(1) """ if self.isEmpty(): return False else: if self.size == 1: pass self.l = 0 if self.l == self.limit - 1 else self.l + 1 self.size -= 1 return True
def deleteLast(self): """ 删除队尾元素 时间复杂度: O(1) """ if self.isEmpty(): return False else: if self.size == 1: pass self.r = self.limit - 1 if self.r == 0 else self.r - 1 self.size -= 1 return True
def getFront(self): """获取队首元素""" if self.isEmpty(): return -1 else: return self.deque[self.l]
def getRear(self): """获取队尾元素""" if self.isEmpty(): return -1 else: return self.deque[self.r]
def isEmpty(self): """判断队列是否为空""" return self.size == 0
def isFull(self): """判断队列是否已满""" return self.size == self.limit
|
循环索引的关键理解
指针移动规律
在固定数组实现中,关键是理解循环索引的移动:
1 2 3 4 5
| next_index = 0 if current_index == limit - 1 else current_index + 1
prev_index = limit - 1 if current_index == 0 else current_index - 1
|
边界情况处理
- 空队列插入:第一个元素插入时,队首和队尾指针都指向同一位置
- 单元素删除:删除唯一元素后队列变空,但指针位置不需要重置
- 满队列检测:通过size变量而非指针位置来判断队列是否已满
性能分析
时间复杂度对比
| 操作 |
列表实现 |
数组实现 |
说明 |
| insertFront |
O(n) |
O(1) |
列表需要移动所有元素 |
| insertLast |
O(1)均摊 |
O(1) |
列表可能需要扩容 |
| deleteFront |
O(n) |
O(1) |
列表需要移动所有元素 |
| deleteLast |
O(1) |
O(1) |
两种实现都是常数时间 |
| getFront/getRear |
O(1) |
O(1) |
直接索引访问 |
空间复杂度
- 列表实现:O(k),但可能因为动态扩容导致额外开销
- 数组实现:O(k),固定空间,无额外开销
应用场景
双端队列的典型应用
- 滑动窗口问题:需要在窗口两端进行操作
- 回文检测:从两端向中间检查字符
- 撤销/重做功能:需要在两端添加和删除操作记录
- 广度优先搜索变种:某些图算法需要双向扩展
与其他数据结构的对比
| 数据结构 |
队首操作 |
队尾操作 |
适用场景 |
| 普通队列 |
删除O(1) |
插入O(1) |
FIFO场景 |
| 栈 |
插入/删除O(1) |
无操作 |
LIFO场景 |
| 双端队列 |
插入/删除O(1) |
插入/删除O(1) |
需要两端操作 |
| 动态数组 |
插入O(n),删除O(n) |
插入O(1)均摊,删除O(1) |
随机访问 |
双端队列提供了比普通队列和栈更大的灵活性,在需要两端操作的算法中具有重要作用。固定数组的循环实现虽然代码复杂度稍高,但提供了最优的时间和空间性能。刷题中优先选择固定数组实现,性能更好,实际工程中如果容量不确定,可以考虑列表实现或者动态扩容的数组;此外,固定数据能使得内存使用更可控。