栈&队列&数组
栈、队列与数组
目录
-
- 栈(Stack)
-
- 队列(Queue)
-
- 双端队列与优先队列(Deque & Priority Queue)
-
- 数组(Array)
-
- 常见题型与解题模板
-
- 核心考点速记
1. 栈(Stack)
1.1 定义与特性
- 栈是后进先出(LIFO,Last In First Out)的线性表。
- 典型操作:push(入栈)、pop(出栈)、top/peek(栈顶元素)、isEmpty、size。
- 常见实现:顺序栈(基于数组)与链式栈(基于链表)。
1.2 实现要点
- 顺序栈:
- 需要一个连续的数组/向量来存放元素。
- 注意上溢(栈满)与下溢(栈空)的边界判断。
- 动态数组实现更常见,可以在容量不足时扩容。
- 链式栈:
- 通过单向链表实现,出栈/入栈复杂度都是 O(1),不需要预设容量。
- 优点:无容量上限,缺点:指针开销略大。
1.3 时间复杂度
- push、pop、top、isEmpty:均为 O(1)(摊销视实现而定,如动态扩容会有偶发的 O(n) 代价,但总体摊销仍是 O(1) 平均)。
1.4 常见题型与解题思路
- 括号匹配:用栈存放左括号,遇到右括号时与栈顶对对称性检查。
- 表达式求值:中缀表达式转后缀表达式(逆波兰表达式)再求值,或直接用栈实现表达式求值。
- 逆波兰表达式求值:遍历表达式,遇数字入栈,遇运算符弹出需要的操作数进行计算后再入栈。
- 用栈实现队列/双端操作:如用两个栈实现队列,或在特定题型中用栈来回放/反转序列。
- 最小栈(MinStack):在栈中维护辅助栈记录当前最小值,O(1) 获取最小值。
1.5 代码模板(Python)
- 顺序栈(简单版,Python 内置列表即可实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class Stack: def __init__(self): self.data = [] def push(self, x): self.data.append(x) def pop(self): if not self.data: raise IndexError("pop from empty stack") return self.data.pop() def top(self): if not self.data: raise IndexError("top from empty stack") return self.data[-1] def is_empty(self): return len(self.data) == 0 def __len__(self): return len(self.data)
- 最小栈(MinStack)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class MinStack: def __init__(self): self.stack = [] self.min_stack = [] def push(self, x): self.stack.append(x) if not self.min_stack or x <= self.min_stack[-1]: self.min_stack.append(x) def pop(self): v = self.stack.pop() if v == self.min_stack[-1]: self.min_stack.pop() return v def top(self): return self.stack[-1] def get_min(self): return self.min_stack[-1] if self.min_stack else None
- 两个栈实现队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
class StackQueue: def __init__(self): self.in_stack = [] self.out_stack = [] def enqueue(self, x): self.in_stack.append(x) def dequeue(self): if not self.out_stack: while self.in_stack: self.out_stack.append(self.in_stack.pop()) if not self.out_stack: raise IndexError("dequeue from empty queue") return self.out_stack.pop() def is_empty(self): return not self.in_stack and not self.out_stack
1.6 共享栈(Multi-stack in one array)
1.6.1 定义与动机
- 共享栈指在同一个数组中实现多个栈,使多个栈共享同一块物理存储空间。
- 优点:提高空间利用率,避免“固定分区”导致的浪费。
- 常见场景:需要多路栈结构且总容量有限时,或在题目中要求“多个栈共用一个数组”的实现。
1.6.2 实现思路
- 基本思想:通过一组辅助数据结构,将数组的空闲单元按链表形式组织起来,动态分配给任意栈使用;入栈时从自由链表取一个位置,出栈时把该位置回收到自由链表。
- 常用实现有两种变体:
- 变体A:固定数量的栈(K 个栈),使用一个 free 列表管理未使用的索引,以及每个栈的 top 指针和一个 next 指针数组来组织“下一元素”的链表结构。
- 变体B:两栈在一个数组的简化实现(只有两个栈),仍然使用一个中间界限,适合快速理解。
- 下面给出最常用的变体A(K 栈在一个数组)的完整实现思路与代码。
1.6.3 数据结构设计
- arr[n]:存放实际数据
- top[k]:存放每个栈的栈顶在 arr 中的索引,初始为 -1
- next[n]:用于维护每个下标的“下一个索引”指针,初始用于构建自由链表;对某个栈来说,next[index] 指向该索引在该栈中的下一个元素的位置
- free:指向自由链表的头结点,表示当前未使用的数组下标,初始为 0
- 说明:
- push(x, t) 时,从 free 取一个下标 idx,free 指向 next[idx],然后把 arr[idx] 赋值为 x、next[idx] 指向上一顶栈顶、top[t] 更新为 idx
- pop(t) 时,取当前栈顶 idx = top[t],top[t] 指向 next[idx],将 idx 加回自由链表:next[idx] 指向当前 free,free = idx,返回 arr[idx]
1.6.4 初始化
- top = [-1, -1, …, -1](长度为 K)
- next = [i+1 for i in range(n)],最后一个为 -1
- free = 0
1.6.5 操作接口
- push(stack_num, value)
- pop(stack_num)
- peek(stack_num)
- is_empty(stack_num)
- is_full()(当 free == -1 时,表示没有空闲空间)
1.6.6 时间复杂度与注意点
- 单次 push/pop 的时间复杂度为 O(1)(摊常数时间)。
- 空间利用率高于固定分区实现,但实现复杂度比单栈稍高,需要小心边界条件和指针更新。
- 在大量并发或异常情况时,要注意保证异常安全性(如抛错前确保数据结构状态正确)。
1.6.7 Python 代码模板
- 通用的 K 栈在一数组中的实现(使用自由链表管理空闲单元)
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
class KStacksInOneArray:
def __init__(self, k, size):
self.k = k # 栈的数量
self.n = size # 数组容量
self.arr = [0] * size # 存放实际数据
self.top = [-1] * k # 每个栈的栈顶在 arr 中的索引
self.next = list(range(1, size)) + [-1] # 下一个索引,用作自由链表和各栈中元素的“下一个”指针
self.free = 0 # 自由链表的头指针
def is_full(self):
return self.free == -1
def is_empty_stack(self, sn):
return self.top[sn] == -1
def push(self, sn, value):
"""
sn: 栈号,范围 [0, k-1]
value: 要入栈的值
"""
if sn < 0 or sn >= self.k:
raise IndexError("Invalid stack number")
if self.is_full():
raise IndexError("Stack Overflow")
insert_at = self.free # 从空闲链表中取一个位置
self.free = self.next[insert_at] # 更新 free 指向下一个空闲位置
self.next[insert_at] = self.top[sn] # 将新节点与栈 sn 的当前栈顶连接
self.top[sn] = insert_at # 更新栈顶为新节点
self.arr[insert_at] = value # 存放值
def pop(self, sn):
if sn < 0 or sn >= self.k:
raise IndexError("Invalid stack number")
if self.is_empty_stack(sn):
raise IndexError("Stack Underflow")
current = self.top[sn] # 栈顶位置
self.top[sn] = self.next[current] # 栈顶向下一个节点移动
# 将当前索引加入自由链表
self.next[current] = self.free
self.free = current
return self.arr[current]
def peek(self, sn):
if sn < 0 or sn >= self.k:
raise IndexError("Invalid stack number")
if self.is_empty_stack(sn):
raise IndexError("Stack is empty")
return self.arr[self.top[sn]]
def __str__(self):
return f"arr={self.arr}, top={self.top}, free={self.free}"
- 简单的两栈在同一数组实现(变体B,便于理解)
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
class TwoStacksInOneArray:
def __init__(self, n):
self.n = n
self.arr = [0] * n
self.left_top = -1 # 左栈顶
self.right_top = n # 右栈顶
def push_left(self, x):
if self.left_top + 1 == self.right_top:
raise IndexError("Stack Overflow")
self.left_top += 1
self.arr[self.left_top] = x
def pop_left(self):
if self.left_top == -1:
raise IndexError("Left Stack Underflow")
val = self.arr[self.left_top]
self.left_top -= 1
return val
def push_right(self, x):
if self.right_top - 1 == self.left_top:
raise IndexError("Stack Overflow")
self.right_top -= 1
self.arr[self.right_top] = x
def pop_right(self):
if self.right_top == self.n:
raise IndexError("Right Stack Underflow")
val = self.arr[self.right_top]
self.right_top += 1
return val
使用场景提醒
- 对于需要强大空间利用率的多栈场景,优先考虑 KStacksInOneArray 的实现;
- 如果栈数固定且大小比较小,且实现要简单快速,可先用两栈共享或分区实现,后续再改为通用实现。
1.7 链栈(Linked Stack)
1.7.1 定义与特性
- 链栈通过单向链表实现栈结构,栈顶通常对应链表的头结点。
- 支持的操作:入栈 push、出栈 pop、获取栈顶元素 peek、判空 is_empty、获取大小 size/name。
- 优点:天然无容量上限,动态扩展;缺点:每个节点需要额外的指针域,内存开销较数组栈大,且在连续分配/释放时可能产生碎片。
1.7.2 数据结构设计
- Node/结点结构:包含值和值所属的下一个节点引用。
- top 指针:指向当前栈顶节点。
- size(可选):记录当前栈中元素个数,便于 O(1) 的 size。
- 变体选择:
- 简单版本:不使用哨兵结点,直接通过头指针与 next 指针实现。
- 带哨兵版本(Sentinel/ dummy head):用一个哨兵头结点简化边界条件,入栈/出栈操作更简洁。
1.7.3 初始化
- 简单版本:top = None,size = 0
- 哨兵版本:head 指向哨兵节点,head.next 指向实际栈顶,size = 0
1.7.4 基本操作接口
- push(x): 将 x 入栈
- pop(): 将栈顶元素出栈并返回
- peek(): 返回栈顶元素但不出栈
- is_empty(): 判断栈是否为空
- size()/len(): 返回栈中元素个数
1.7.5 时间复杂度
- push、pop、peek、is_empty、size:均为 O(1)
1.7.6 变体:带哨兵头结点的链栈
- 优点:边界判断统一,代码常量更小,尤其在需要频繁判断空栈时更稳妥。
- 结构要点:哨兵结点不存放实际数据,所有数据结点都位于 head.next 及其后续。
1.7.7 Python 代码模板
- 变体A:简单版本(无哨兵) ```python class Node: def init(self, val, nxt=None): self.val = val self.next = nxt
class LinkedStack: def init(self): self.top = None self._size = 0
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
def push(self, x):
node = Node(x, self.top)
self.top = node
self._size += 1
def pop(self):
if self.top is None:
raise IndexError("pop from empty stack")
val = self.top.val
self.top = self.top.next
self._size -= 1
return val
def peek(self):
if self.top is None:
raise IndexError("peek from empty stack")
return self.top.val
def is_empty(self):
return self.top is None
def size(self):
return self._size
def __len__(self):
return self._size ```
- 变体B:带哨兵头结点的链栈 ```python class Node: def init(self, val=None, nxt=None): self.val = val self.next = nxt
class LinkedStackSentinel: def init(self): self.head = Node() # 哨兵头结点 self._size = 0
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
def push(self, x):
# 新结点插入在 head.next 之前
self.head.next = Node(x, self.head.next)
self._size += 1
def pop(self):
if self.head.next is None:
raise IndexError("pop from empty stack")
val = self.head.next.val
self.head.next = self.head.next.next
self._size -= 1
return val
def peek(self):
if self.head.next is None:
raise IndexError("peek from empty stack")
return self.head.next.val
def is_empty(self):
return self.head.next is None
def size(self):
return self._size
def __len__(self):
return self._size ```
- 使用建议
- 如果题目强调空间使用,需要尽量避免固定容量的分区,链栈是天然选择。
- 如果对性能要求更多,且栈深度可控,数组栈在缓存局部性方面可能更快;两者可按题目需求选用或互换。
1.7.8 与数组栈的对比要点
- 链栈优点
- 无固定容量限制,理论上可无限扩展(受限于内存)。
- 入栈/出栈操作都为 O(1),且边界处理通常更简单,尤其带哨兵时。
- 链栈缺点
- 每个节点需要额外的对象/指针,占用额外内存。
- 访问时可能对 CPU 缓存友好性较差。
- 适用场景
- 题目要求“动态扩容且不预先设定容量”的场景。
- 需要在栈中间频繁插入/删除节点的题型,或需要实现可回溯的状态记录。
1.7.9 常见题型与解题要点
- 括号匹配、表达式求值、逆波兰表达式(RPN)求值等栈相关题型,链栈与数组栈思路一致,区别在数据结构实现方式。
- 回溯/状态记录题:利用栈来保存历史状态,链栈在需要动态扩展的历史记录场景下更自然。
- 注意点
- 边界条件:空栈出栈/查看栈顶等操作需先判空。
- 内存管理:Python 等语言自动管理,但在手写语言(如 C/C++)需关注删除节点以防内存泄漏。
- 线程安全:默认非线程安全,如需多线程,需加锁或使用并发结构。
2. 队列(Queue)
2.1 定义与特性
- 队列是先进先出(FIFO,First In First Out)的线性表。
- 栈反向的角色:push 变为 enqueue,pop 变为 dequeue。
- 常见实现:顺序队列、循环队列、链式队列、以及更高级的双端队列(Deque)和优先队列(Priority Queue)。
2.2 实现要点
- 顺序队列:
- 用数组维护头尾指针,直观但容易在大量出队后产生“假空”,需要移动/记忆头指针。
- 循环队列(环形缓冲区):
- 通过取模运算实现头尾移动,避免数据移动。
- 边界判定:通常通过头尾指针相遇来判空/判满。
- 链式队列:
- 通过链表节点实现,入队以尾部插入,出队以头部删除,避免容量限制。
- 双端队列(Deque):
- 同时支持队头出/队尾入、队尾出/队头入,灵活性更强。
- 优先队列(Priority Queue):
- 根据优先级高低出队,常用堆实现(最大堆或最小堆)。
2.3 时间复杂度
- 入队/出队:通常 O(1);具体取决于实现(循环队列和链式队列为 O(1),数组实现需关注头指针移动)。
2.4 常见题型与解题思路
- 广度优先搜索(BFS):图/树的层级遍历,典型用队列存放待访问节点。
- 队列实现题:用两栈实现队列、用队列实现滑动窗口等。
- 优先队列的应用:最小生成树的某些实现、Dijkstra、A* 等路径算法的核心结构。
- 双端队列在滑动窗口、回文判断等题型中的应用。
2.5 代码模板(Python)
- 循环队列(容量 fixed)
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
class CircularQueue: def __init__(self, k): self.data = [None] * k self.head = 0 self.tail = 0 self.size = 0 self.capacity = k def enqueue(self, x): if self.is_full(): raise IndexError("enqueue on full queue") self.data[self.tail] = x self.tail = (self.tail + 1) % self.capacity self.size += 1 def dequeue(self): if self.is_empty(): raise IndexError("dequeue from empty queue") val = self.data[self.head] self.data[self.head] = None self.head = (self.head + 1) % self.capacity self.size -= 1 return val def is_empty(self): return self.size == 0 def is_full(self): return self.size == self.capacity
- 两栈实现队列(前面已有 MinStack 的思路)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class StackQueue: def __init__(self): self.in_stack = [] self.out_stack = [] def enqueue(self, x): self.in_stack.append(x) def dequeue(self): if not self.out_stack: while self.in_stack: self.out_stack.append(self.in_stack.pop()) if not self.out_stack: raise IndexError("dequeue from empty queue") return self.out_stack.pop()
3. 双端队列与优先队列(Deque & Priority Queue)
3.1 双端队列(Deque)
- 支持在两端进行插入和删除的队列。
- 常见题型:滑动窗口中的维护、回文判定、队列的谓词性题型等。
- 实现:可以用 Python 的 collections.deque,或自实现循环数组/链表。
3.2 优先队列(Priority Queue)
- 元素带有优先级,出队时总是优先级最高的元素。
- 常见实现:二叉堆(MinHeap/MaxHeap)。
- 应用:Dijkstra/Prim 等算法、任务调度等。
代码模板(Python 内置 heapq 的简单示例):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import heapq
class MinPriorityQueue:
def __init__(self):
self.heap = []
def push(self, item, priority):
heapq.heappush(self.heap, (priority, item))
def pop(self):
if not self.heap:
raise IndexError("pop from empty priority queue")
priority, item = heapq.heappop(self.heap)
return item
def is_empty(self):
return not self.heap
4. 数组(Array)
4.1 基本概念
- 静态数组:长度固定,访问常数时间 O(1),缺点是扩容和删除成本较高。
- 动态数组:容量可扩容,通常通过“容量翻倍”策略实现,平均摊销为 O(1) 访问/写入。
4.2 常见操作与复杂度
- 访问/赋值:O(1)
- 插入/删除(在中间位置):O(n)(需要移动后续元素)
- 数组题型核心常用技巧:双指针、滑动窗口、二分查找、前缀和、差分等。
4.3 常见题型与解题思路
4.3.1 双指针法
- 适用场景:有序数组、需要在两端移动的条件等。
- 常见用途:删除重复、两数之和、子序列等。
示例:有序数组去除重复元素(就地去重,返回新长度)
1
2
3
4
5
6
7
8
9
def remove_duplicates(nums):
if not nums:
return 0
i = 0
for j in range(1, len(nums)):
if nums[j] != nums[i]:
i += 1
nums[i] = nums[j]
return i + 1
4.3.2 两数之和(有序数组)
1
2
3
4
5
6
7
8
9
10
11
def two_sum_sorted(nums, target):
i, j = 0, len(nums) - 1
while i < j:
s = nums[i] + nums[j]
if s == target:
return (i, j)
elif s < target:
i += 1
else:
j -= 1
return None
4.3.3 滑动窗口
- 目标:在一个可变窗口内维护和/最大长度/最小值等。
- 常见题型:最长无重复子串、满足条件的子数组长度、最小窗口覆盖等。
示例:最大和不超过 k 的子数组长度(简单版)
1
2
3
4
5
6
7
8
9
10
11
def max_subarray_len_under_k(nums, k):
left = 0
cur_sum = 0
max_len = 0
for right, val in enumerate(nums):
cur_sum += val
while cur_sum > k and left <= right:
cur_sum -= nums[left]
left += 1
max_len = max(max_len, right - left + 1)
return max_len
4.3.4 前缀和(Prefix Sum)
- 将数组转换为前缀和数组,快速求任意区间和。
- 常与二分、哈希结合解决子数组等问题。
示例:前缀和数组
1
2
3
4
5
def prefix_sums(nums):
ps = [0]
for x in nums:
ps.append(ps[-1] + x)
return ps
4.3.5 差分数组(Difference Array)
- 适用于多次区间加法,最后用前缀和得到结果。
- 典型题型:一段区间内统一修改某值,最后输出数组。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
def difference_array(n, ops):
diff = [0] * (n + 1)
for l, r, val in ops:
diff[l] += val
if r + 1 < n:
diff[r + 1] -= val
ans = [0] * n
cur = 0
for i in range(n):
cur += diff[i]
ans[i] = cur
return ans
4.3.6 二分查找(Binary Search)
- 在有序数组中定位目标值的下标,或找到满足特定条件的边界。
- 常见变体:lower_bound、upper_bound、find first/last等。
示例:
1
2
3
4
5
6
7
8
9
10
11
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
4.3.7 Kadane 算法(最大子数组和)
1
2
3
4
5
6
7
8
def max_subarray_sum(arr):
if not arr:
return 0
max_so_far = curr = arr[0]
for x in arr[1:]:
curr = max(x, curr + x)
max_so_far = max(max_so_far, curr)
return max_so_far
5. 常见题型与解题模板
5.1 栈相关
- 括号匹配:使用栈保存左括号,遇到右括号进行匹配。
- 中缀转后缀表达式(Shunting-yard 算法的一部分思想)+ 逆波兰表达式求值。
- 字符串/表达式处理:如只包含数字、运算符、括号的表达式求值。
- 题型变体:需要在栈中同时维护额外信息(如最小值、计数等)。
模板要点:
- 先掌握基本栈操作及边界判断。
- 对复杂题,先把输入分成能被栈处理的子任务(如括号对齐、分块处理等)。
5.2 队列相关
- BFS 实现:用队列按层逐层展开节点,常用于图遍历、树的层序遍历。
- 双端队列在滑动窗口/回文等题中的应用。
- 优先队列在最短路径、调度类题型中的应用。
模板要点:
- 对 BFS 用最小辅助信息(如距离)记忆化。
- 不同题型选用合适的队列实现(单队列、双端队列、优先队列等)。
5.3 数组相关
- 双指针、滑动窗口、二分查找、前缀/差分是最核心的工具。
- Kadane、前缀和+哈希、差分数组等组合常用于区间相关题。
实战建议:
- 切题前,先把题意抽象成“需要在序列上进行的线性操作”,并定位到以下工具之一:栈、队列、滑动窗口、二分查找、前缀和、差分、动态规划的基本数组结构。
- 注意边界条件:空数组、单元素、重复元素、负数等情况。
- 记住常用时间复杂度:大多数线性遍历相关为 O(n),二分查找为 O(log n),堆/优先队列相关为 O(log n)。
6. 核心考点速记
- 栈
- 应用场景:括号匹配、表达式求值、逆波兰表达式、栈模拟(如两栈实现队列)。
- 数据结构要点:简单实现、统一 API、边界判断。
- 队列
- 应用场景:BFS、分层遍历、滑动窗口、队列模拟。
- 数据结构要点:循环队列、双端队列、优先队列的基本应用。
- 数组
- 基本操作:访问、插入删除在中间偏慢,重点练习头尾/中间的巧妙操作。
- 双指针/滑动窗口:解决子串、子数组、区间覆盖等问题的主线。
- 前缀和/差分:快速区间和、批量区间更新。
- 二分查找:定位、边界类型(左边界/右边界)的辨析。
- Kadane:最大子数组和、线性时间找出最优区间。
- 动态规划常与数组配合:常见状态转移需要用数组存放中间结果。
本文由作者按照
CC BY 4.0
进行授权