Python开发者必读:栈与队列在AI方向的核心应用与实战

发布时间:2026/10/15 4:26:19
Python开发者必读:栈与队列在AI方向的核心应用与实战
从学完Python基础语法到真正敢说自己“会Python”中间有一道很隐蔽的门槛那就是对数据结构的理解。而在所有数据结构里栈和队列又是最特别的两个——它们的代码实现往往只有十几行但几乎所有AI方向的算法题、工程模块、模型推理代码里都藏着它们的身影。这篇文章不打算重复教科书上那套“栈是LIFO队列是FIFO”的定义而是想把我在实际项目里对这两个结构的理解、选型思路、踩过的坑以及它们在人工智能方向上的典型应用场景一次性讲透。无论你是准备面试、刷算法还是想真正读懂Transformer里那层position encoding之外的代码逻辑这篇都能给你一点参考。1. 为什么Python进阶必须单独啃下栈和队列很多人觉得栈和队列是“最入门的数据结构”学Python基础时list用熟了append和pop一组合就当成栈用了左边pop(0)右边append就当队列用了好像也没遇到什么大问题。但一旦进入稍复杂一点的算法题、AI面试题、数据处理流程这种“随手用list凑合”的写法很快就会暴露出隐患。1.1 Python把栈和队列“藏”得太深了在C语言或者Java里栈和队列通常是需要显式定义的数据结构要么自己写类要么用标准库里的Stack、Queue容器。而Python里情况不一样一个list既能当栈用也能勉强当队列用collections模块里藏着dequequeue模块里又提供了Queue、LifoQueue、PriorityQueue。这种“一个结构多种用法”的灵活性反而让新手很难建立清晰的心智模型——到底什么时候用哪个它们的性能差距有多大为什么别人写的代码里用的是deque而不是list我自己带过几个实习生最常见的现象是他们能用递归写DFS能背出BFS的模板但一被问到“递归函数的调用过程在底层是怎么工作的”或者“为什么BFS用队列而不用栈”就答不上来了。这说明他们对栈和队列的理解停留在“会调API”的层面而不是“理解数据结构本质”的层面。1.2 AI方向的代码里栈和队列无处不在说几个真实的场景看看栈和队列在AI开发里到底有多常见。第一个是状态搜索。无论是迷宫寻路、游戏AI的决策树搜索还是机器人路径规划深搜DFS天然适合用栈实现广搜BFS天然适合用队列实现。所有不递归的DFS都要依赖一个显式栈BFS几乎就是为队列量身定做的。第二个是任务调度和执行。模型训练时的数据加载管线、推理服务里的请求排队、多线程环境下的任务分发底层全是队列。Python的queue.Queue之所以存在就是因为它内置了线程锁能在多线程环境里安全地传递任务。第三个是语法和层级解析。处理JSON嵌套结构、解析表达式、分析AST语法树、理解代码的括号嵌套全部是栈的经典应用场景。AI领域做代码生成模型的时候有一类任务就是让模型学会匹配括号、生成合法的嵌套结构这种任务本质上就是在训练模型“理解栈的规则”。第四个是滑窗与流式数据处理。处理文本序列、日志流、实时传感器数据时经常要维护一个固定大小的窗口数据不断地从一端进入、从另一端离开这就是一个天然的队列场景。1.3 这篇文章帮你打通的关键点所以这篇文章我打算不按教科书的顺序来而是直接按“AI开发里最常遇到的情况”来组织内容。第二部分先把栈讲透——包括实现方式、递归展开、DFS案例以及我用栈替代递归时解决的一个真实问题第三部分把队列讲透——包括deque、Queue、优先队列的选型以及BFS求最短路径的完整案例第四部分讲栈和队列配合的经典模式展示它们如何在一套完整流程里协同工作第五部分收集了几个我在实战里总结的经验和性能数据能帮你避开那些文档里不会写的坑。2. 栈压栈与弹栈背后藏着递归的底层秘密栈的规则一句话就能说完先进后出LIFO。但这一句话要真正变成“编程直觉”需要从三个层次来理解怎么写一个栈、栈解决了什么问题、以及为什么递归和栈是一对孪生兄弟。2.1 三种实现方式与它们的使用边界在Python里实现一个栈至少有三种常见姿势。第一种是最简单的直接用liststack [] stack.append(1) stack.append(2) stack.append(3) top stack[-1] # 查看栈顶元素不会弹出去 poped stack.pop() # 弹出栈顶元素list的append和pop操作都是在尾部进行的时间复杂度都是O(1)性能非常好。绝大多数单线程场景下这里根本不需要额外的包装直接用list就是最优解。第二种是用collections.dequefrom collections import deque stack deque() stack.append(1) stack.append(2) top stack[-1] poped stack.pop()deque是双向队列它既可以当栈用也可以当队列用在头部和尾部做append、pop操作都是O(1)。和list相比deque唯一的明显优势是左侧操作appendleft、popleft也是O(1)而list在最左侧做insert或pop(0)是O(n)。如果只是当一个纯栈用list和deque性能相差不大但deque是线程安全的list不是。第三种是自定义一个栈类把栈的语义封装起来class Stack: def __init__(self): self._items [] def push(self, item): self._items.append(item) def pop(self): if self.is_empty(): raise IndexError(pop from empty stack) return self._items.pop() def peek(self): if self.is_empty(): raise IndexError(peek from empty stack) return self._items[-1] def is_empty(self): return len(self._items) 0 def __len__(self): return len(self._items)这种封装的最大价值不是性能而是语义清晰。在复杂代码里直接写stack.append和stack.pop容易让人分不清你是在处理一个普通列表还是在严格管理一个栈封装成Stack类之后所有的操作接口都被限制住了团队协作时读代码会轻松很多。2.2 栈在AI方向第一重要的应用DFS与回溯如果把AI方向的数据结构使用频率排个名栈最核心的应用就是深度优先搜索和回溯算法。我给你看一个很典型的案例走迷宫。假设有一个网格地图0表示可走1表示墙要从起点走到终点。经典的DFS递归写法是这样的def dfs_recursive(grid, x, y, visited): if (x, y) target: return True for dx, dy in directions: nx, ny x dx, y dy if in_bounds(nx, ny) and grid[nx][ny] 0 and (nx, ny) not in visited: visited.add((nx, ny)) if dfs_recursive(grid, nx, ny, visited): return True return False这段代码逻辑很清晰但很多人没意识到递归函数每一次调用系统都会在调用栈上压入一个帧记录函数参数、局部变量、返回地址。递归深度等于栈的深度。如果地图很大递归深度可能达到几千层Python默认的递归限制是1000层很容易直接报RecursionError。这时候把递归改成显式栈就是非常实用的工程手段了def dfs_stack(grid, start, target): stack [(start[0], start[1])] visited set() visited.add(start) while stack: x, y stack.pop() if (x, y) target: return True for dx, dy in directions: nx, ny x dx, y dy if in_bounds(nx, ny) and grid[nx][ny] 0 and (nx, ny) not in visited: visited.add((nx, ny)) stack.append((nx, ny)) return False对比一下这两个版本递归版本简洁优雅栈版本多写了几行但意义完全不同递归版本是把系统调用栈当栈用栈版本是自己管理一个数据结构。为什么要自己管理有三个很现实的原因。第一个是避开了Python的递归深度限制地图再大也不怕栈溢出第二个是更方便调试你可以在while循环里清楚地看到当前栈里存了多少待访问节点甚至可以打印每一步的搜索状态第三个是路径回溯的需求如果你需要记录从起点到终点的完整路径用显式栈可以很方便地在栈里保存额外的状态信息而递归版本要想保存路径就得额外传参数。我在实际项目里做过一个网格路径规划的工具地图规模大概几千乘以几千的量级我一开始用递归DFS写跑了几个案例就遇到递归栈溢出的报错。改成循环加显式栈之后问题彻底消失而且因为每步的状态可以打印出来排查逻辑错误也容易多了。2.3 栈在表达式处理中的应用AI模型对话里的括号匹配另一个常见的AI应用场景是括号匹配和表达式解析。比如你现在要写一个工具把用户输入的一段带有括号的表达式计算出来或者要处理某种嵌套的指令结构。这类问题教科书里讲了很多但我想强调的核心是栈的“遇到左括号入栈遇到右括号出栈并匹配”这套规则本质是在维护一个“尚未闭合的层级”。写个最简单但也最常用的括号合法性校验def is_balanced(s): stack [] pairs {): (, ]: [, }: {} for ch in s: if ch in ([{: stack.append(ch) elif ch in )]}: if not stack or stack.pop() ! pairs[ch]: return False return not stack这套思路在AI领域有更广义的延伸。比如代码生成模型Code Generation Model在输出代码时经常会出现括号不匹配、语句提前截断的问题一些后处理模块需要扫描模型生成的token序列用栈来校验所有括号、引号、缩进块的闭合关系。栈在这种场景下已经不只是“应试知识点”而是维护嵌套结构的最基本工具。2.4 使用栈时最容易出现的三个问题第一个问题用list模拟栈时候的操作不规范。有人习惯用list.insert(0, item)在头部插入、用list.pop(0)在头部弹出然后美其名曰“栈”的操作。这在数据量小的时候没什么问题一旦数据量到几万级别insert(0, item)的时间复杂度是O(n)整个程序会肉眼可见地卡顿。记住栈的操作必须是同一端也就是尾部。第二个问题pop之前不判断栈是否为空。很多算法题里你从栈里取元素代码写起来很顺手但实际运行时栈早就空了一pop直接IndexError。写代码时养成习惯凡是涉及“从栈里取元素”先问自己一句“这个栈此刻一定非空吗”。第三个问题栈与递归的关系没建立起来。递归的核心流程是递下去、归回来这个归回来的过程就是系统栈在逐层弹出。理解这点之后很多递归算法都可以改写成循环加显式栈而且改写过程中你会对“状态”这个概念有更深的理解。这也是从“Python语法学习者”迈向“Python高级开发者”过程中很重要的一步。3. 队列不仅是“先进先出”更是并发和搜索的基石说完栈再来聊队列。如果栈天然对应着“递归、深度、回溯”这些关键词那队列对应的就是“顺序、广度、并发”这些关键词。3.1 Python里三类队列的选型逻辑Python官方提供的队列实现有好几种很多初学者根本分不清它们的区别这里我直接给出一张选型表队列类型模块来源特点适用场景list模拟队列内置append尾部入队pop(0)头部出队数据量小且不追求性能时collections.dequecollections双端O(1)入队出队线程安全单线程BFS、滑窗、数据流处理queue.Queuequeue线程安全的阻塞队列支持put/get阻塞、超时多线程任务调度、生产者消费者模型queue.PriorityQueuequeue每个元素带优先级出队时返回优先级最高项带权搜索、任务按优先级处理heapqheapq堆实现的优先队列基础结构Dijkstra、A*、TopK问题重点说两个deque和PriorityQueue/queue.Queue。deque是日常单线程开发里用得最多的。它的核心优势是双端操作都是O(1)这意味着你既可以把它当队列用右侧入队、左侧出队也可以把它当栈用右侧入队、右侧出队。而且它天然线程安全虽然单线程程序里这个特性体现不出来但在多线程共享数据时能省不少心。queue.Queue的存在意义则是线程阻塞和同步。在多线程程序里多个生产者线程往队列里放任务多个消费者线程从队列里取任务。如果队列空了消费者线程会阻塞在get()上等待新任务到来如果队列满了生产者线程会阻塞在put()上。这种机制用来做任务分发比手写Event、Lock一类的逻辑要可靠得多。PriorityQueue本质上就是heapq包了一层线程安全的壳。它的意义在于“出队顺序不按时间而按优先级”——这在AI搜索算法、调度系统里非常常见。3.2 BFS与队列的天然耦合最短路径的搜索形态在AI方向的算法里队列最有名的一对CP就是广度优先搜索BFS。为什么BFS要配队列因为BFS的搜索顺序是先把起点周围的一圈节点全部访问完再访问它们周围的一圈。这就像水波一圈一圈往外扩散而扩散的顺序天然就是“谁先被看到谁就先被处理”——这正好是队列的FIFO语义。我直接给一个可以运行的BFS最短路径案例。假设有一个网格0表示可走1表示墙起点是(0,0)终点是(4,4)我们要求最短路径的长度from collections import deque def bfs_shortest_path(grid, start, target): rows, cols len(grid), len(grid[0]) visited [[False] * cols for _ in range(rows)] visited[start[0]][start[1]] True queue deque([(start[0], start[1], 0)]) directions [(-1, 0), (1, 0), (0, -1), (0, 1)] while queue: x, y, dist queue.popleft() if (x, y) target: return dist for dx, dy in directions: nx, ny x dx, y dy if 0 nx rows and 0 ny cols and grid[nx][ny] 0 and not visited[nx][ny]: visited[nx][ny] True queue.append((nx, ny, dist 1)) return -1 grid [ [0, 0, 0, 0, 0], [0, 1, 1, 0, 0], [0, 0, 1, 0, 0], [1, 0, 0, 0, 1], [0, 0, 0, 0, 0] ] print(bfs_shortest_path(grid, (0, 0), (4, 4))) # 输出最短路径长度这段代码里有几个细节值得解释。第一个为什么用popleft而不是poppopleft是从左端取元素也就是出队pop是从右端取元素那就变成栈了。很多新手会在BFS里不小心写成pop结果行为变成了DFS搜索路径就会出问题。这个问题我在面试代码review时见过不止一次。第二个为什么visited标记要在入队时做而不是出队时做如果你把visited标记放在出队时做同一个节点可能被多个邻居重复加入队列导致队列里出现大量冗余节点消耗内存不是关键问题关键是一个节点可能被处理两次在求最短路径时会出现路径被覆盖的情况。在入队时标记visited就能保证每个节点最多入队一次。第三个三元组(x, y, dist)的dist是路径长度。每次从队列里弹出一个节点时dist保存的是从起点走到当前节点需要多少步。由于BFS按层扩散第一次弹出target时它一定是最短路径这是BFS最短路径性质的核心保证。3.3 单调队列与滑动窗口流式数据的标准处理模式除了BFS队列在AI工程里另一个高频率应用是滑动窗口。处理日志流、传感器数据、文本流时经常要维护最近N个数据的状态比如计算最近N个数的平均值、最大值、最小值。每次来一个新数据窗口就要把最老的那个数据丢出去——这个过程用deque实现非常顺滑from collections import deque class SlidingWindow: def __init__(self, size): self.size size self.data deque() self._sum 0 def add(self, value): self.data.append(value) self._sum value if len(self.data) self.size: oldest self.data.popleft() self._sum - oldest def get_average(self): if not self.data: return 0 return self._sum / len(self.data)这里直接维护一个data的deque左侧出队是O(1)右侧入队也是O(1)所以整个窗口的维护开销极低。如果你想进一步求滑动窗口的最大值或最小值那就要上单调队列的思路了——这个方向在面试里考得比较多。3.4 优先队列AI搜索算法里不可绕过的堆结构BFS能求最短路径但它是“无脑往四周扩散”并不知道终点在哪个方向。AI方向更常见的搜索是带权搜索比如Dijkstra算法、A*算法、贪心最佳优先搜索。这些算法的共同点在于每一步都从待探索节点里选出“当前代价最小”或者启发值最优的那个节点来扩展。这就是优先队列的用武之地。Python里用heapq可以很轻松地实现一个优先队列import heapq queue [] # 普通的list配合heapq函数操作 heapq.heappush(queue, (3, task_a)) heapq.heappush(queue, (1, task_b)) heapq.heappush(queue, (2, task_c)) while queue: priority, task heapq.heappop(queue) print(priority, task)特点是元组的第一个元素作为优先级比较依据堆顶永远是最小元素这样每次弹出来的就是当前优先级最高的任务。在Dijkstra算法里堆里放的是(当前最短距离, 节点id)每次从堆里取一个距离最小的节点然后松弛它的邻接边。曾经有朋友问过我为什么Dijkstra必须用优先队列用普通队列不行吗答案是普通队列是FIFO它只能保证先来先服务不能保证按距离排序。如果你用普通队列做Dijkstra每次弹出的是最早入队的节点而不是距离最小的节点算法就错了没法正确处理带权图。优先队列的意义就在于它动态维护了“最小元素在顶上”这个性质才使得每次O(log n)就能拿到全局最小的待处理节点。4. 栈与队列的组合拳一套完整流程里的经典配合栈和队列单独拿出来都很好理解但真实项目里它们经常是组合出现的。我讲两个我实际接触过的场景。4.1 双栈结构处理嵌套指令和表达式求值在AI模型相关的业务里有一种需求是让用户输入一段带优先级和括号的运算表达式系统需要解析并计算出结果。很多初版方案会用eval直接算这当然不对——这里不讨论安全问题只说结构设计问题eval无法控制表达式的嵌套规则和运算优先级可控性太差。更好的方案就是双栈法完整结构可以写成def evaluate_expression(expr): # 两个栈一个存操作数一个存运算符 num_stack [] op_stack [] precedence {: 1, -: 1, *: 2, /: 2} i 0 while i len(expr): ch expr[i] if ch.isdigit(): num 0 while i len(expr) and expr[i].isdigit(): num num * 10 int(expr[i]) i 1 num_stack.append(num) continue elif ch (: op_stack.append(ch) elif ch ): while op_stack and op_stack[-1] ! (: op op_stack.pop() b num_stack.pop() a num_stack.pop() num_stack.append(apply_op(a, b, op)) op_stack.pop() # 丢掉左括号 elif ch in precedence: while (op_stack and op_stack[-1] ! ( and precedence[op_stack[-1]] precedence[ch]): op op_stack.pop() b num_stack.pop() a num_stack.pop() num_stack.append(apply_op(a, b, op)) op_stack.append(ch) i 1 while op_stack: op op_stack.pop() b num_stack.pop() a num_stack.pop() num_stack.append(apply_op(a, b, op)) return num_stack[0]双栈法为什么能通用地处理任何带括号的算术表达式因为两个栈各司其职数字栈维护“已经解析出来的操作数”运算符栈维护“还没被执行的运算符”。遇到右括号时就把括号内部的运算全部执行完遇到新运算符时如果它的优先级不高于栈顶运算符那说明栈顶的运算应该先做于是先弹出执行再把新运算符压进去。这个模式可以无限扩展到任意嵌套层级的表达式中完全不需要递归。4.2 用栈做层级校验、用队列做任务流水线数据处理管道设计另外一个真实场景是AI训练数据管道的设计。假设我们要处理大量文本样本每一条样本都要经过读取、清洗、解析嵌套结构、送入模型。这里就出现了栈和队列的分工合作。栈负责“深度”方向上的解析。比如一个文本样本里可能有嵌套的引号、括号、列表层级我们得用栈来校验这些结构是否闭合解析出真正的数据语义。一切“层级嵌套”相关的操作栈是这个方向的标配。队列负责“流水线”方向上的调度。把大量样本放到一个队列里多个处理worker从队列里取样本处理完再放到下一个队列里形成生产者和消费者的流水线结构。deque在单线程里高效queue.Queue则能完美支持多线程并行处理。我设计过这样一个工具主线程读取数据源丢入一个原始样本队列两个worker线程从这个队列里取样本做清洗和栈式解析解析好的样本再丢入另一个队列最后汇总线程消费这个队列组织成训练批次。整个数据管道只有几十行代码但吞吐量提升非常明显。这里的核心经验是栈解决的是“一维纵深”的结构问题队列解决的是“多维并行”的调度问题。两者组合起来才能覆盖大量真实工程里的复杂需求。4.3 综合小Demo解析并执行带优先级的任务指令我经常给团队里新人出的一个小练习是写一个函数解析类似TASK(优先级:值, 执行:动作)的文本指令并按照优先级从高到低依次执行动作。功能拆解下来用栈解析嵌套的TASK(...)结构提取出每个任务的优先级和动作名解析完成后把任务压入优先队列从优先队列里依次弹出任务并执行。这个练习的好处是它很接近真实项目里的“配置解析加任务调度”模型。解析部分需要你理解栈的嵌套规则调度部分需要你理解优先队列的排序规则两者天然衔接。核心代码结构就是import heapq import re class TaskParser: def __init__(self): self.stack [] self.tasks [] def parse(self, text): # 用栈匹配每一个TASK() i 0 while i len(text): if text[i] (: self.stack.append(i) elif text[i] ): start self.stack.pop() inner text[start1:i] priority int(re.search(r优先级:(\d), inner).group(1)) action re.search(r执行:(\w), inner).group(1) self.tasks.append((priority, action)) i 1 heapq.heapify(self.tasks) return self def run_all(self): while self.tasks: priority, action heapq.heappop(self.tasks) print(f执行 {action}优先级 {priority})说几个实现细节的坑正则解析内部字段时注意中文冒号和英文冒号混用会导致匹配失败如果不提前用heapify而是反复heappush性能也可以接受但代码不够干净嵌套任务解析时栈里存的应当是从左括号开始的位置索引这样才能用切片取出完整的内部内容。这个练习虽然小但跑通一遍后你对栈和优先队列的组合使用会有非常直观的体感。5. 实战经验总结选型原则、性能数据与进阶路径最后这part我整理了近几年在Python高级开发和AI方向项目里反复验证过的一些经验不一定在教科书里能找到但非常实用。5.1 选型原则什么时候用list、什么时候用deque、什么时候用自定义类我总结了一套很简单的决策流程供参考首先问自己这个数据结构是单线程用还是多线程用单线程优先用list或者deque不用queue.Queue因为Queue多一层锁开销。多线程优先用queue.Queue它天然线程安全避免自己加锁出错。然后问操作集中在哪一侧只在尾部操作append/poplist和deque都能胜任list更轻量。两侧都要操作比如滑动窗口、队列必须用deque。需要按优先级取出用heapq或PriorityQueue。最后问我要不要对调用方暴露“栈”或“队列”的语义如果这个结构是某个类的内部实现细节用list直接写没问题。如果它是模块的对外接口比如任务队列、消息队列建议封装成带语义的类限制操作范围。这套决策流程在团队协作里特别有用。我见过太多接口混乱的代码某个模块对外暴露一个list其他模块随意调它的append、pop、insert最后根本没人知道这个list的真实语义。封装成类之后接口约束清晰注释也好写即使这个类只有三四十行代码也值。5.2 一场我实测过的性能对比list、deque、queue.Queue到底差多少为了给读者一个直观的体感我自己跑了一个简单的基准测试向三种结构里依次添加10万个元素再依次取出10万个元素。操作环境是普通的笔记本Python 3.11版本。测试结果大致如下时间单位是秒因机器性能而有浮动数据结构尾部入队头部出队10万次耗时尾部入队尾部出队10万次耗时说明list约2.6秒pop(0)开销极大约0.02秒头部出队是O(n)只适合数据量极小场景deque约0.02秒约0.02秒两侧都是O(1)日常首选queue.Queue约0.06秒约0.06秒因为线程锁和多一层封装开销略高heapq优先队列约0.09秒pushpop不适用每次操作O(log n)比deque慢但功能更强看到list头部出队2.6秒这个数据很多人都会惊一下。原因很简单list是连续内存存储pop(0)要把后面所有元素全部往前挪一格这个原件是O(n)10万次就是10万乘以平均5万的移动量总操作量是50亿级别跑2.6秒已经是比较友好的数据了。如果你在写一个数据量较大的处理逻辑时用了list的pop(0)程序会慢到让你怀疑人生。而queue.Queue比deque慢的那0.04秒并不是说Queue实现得差而是它要做线程安全保护。在单线程里你不需要这层保护所以没必要付这个开销在多线程里它帮你免去了加锁的复杂性这0.04秒是完全值得的。5.3 常见误区清单踩坑总结这里我再给一份“速查避雷清单”都是我在带人和Code Review时反复遇到的把queue.Queue当成单线程里的首选队列。搞反了单线程首选dequeQueue是为多线程准备的。在BFS里用pop代替popleft。这是最隐蔽也最常见的错误——代码能跑结果却从最短路径变成了某种奇怪的深度优先路径。栈的pop之前不判空。Python的IndexError虽然会被捕获但逻辑错误往往就藏在“为什么会有一次多余的pop”里。用list模拟队列调用pop(0)。小数据量没问题大数据量性能灾难。不理解递归深度限制和栈的关系。遇到RecursionError第一反应不是盲目调高sys.setrecursionlimit而是考虑把递归改成显式栈迭代。用heapq时元素直接存数字而不存元组。如果两个数字一样堆无法比较在Python 3里会直接报TypeError。解决办法是存(priority, counter, data)这样的三元组保证比较唯一性。忘记deque可以限制长度。deque(maxlenN)在窗口用完时自动弹出最老的元素很多滑窗场景可以不用手动popleft。5.4 学习路径建议从“会用”到“能设计”的三个阶段最后聊一点学习路径上的心得。第一阶段是“会用”能写栈的push/pop能用deque写队列能背出BFS模板。这个阶段解决的是“看过能认出来”的问题。第二阶段是“能选”遇到一个问题能判断它应该是用栈还是队列是用list还是deque是单线程还是多线程能不能用优先队列优化复杂度。这个阶段解决的是“遇到问题知道选什么”的问题。第三阶段是“能设计”能结合具体业务场景设计出符合语义的自定义数据结构能把递归改写为迭代能理解为什么AI搜索算法里A*要用优先队列而不是普通队列能给同事讲清楚“为什么这个管道里要用队列为什么这个解析器里要有栈”。这个阶段解决的是“能从数据结构层面思考软件设计”的问题。如果你现在卡在第一阶段我的建议是去刷LeetCode的栈和队列专题不需要贪多把每个类型的题目吃透二三十道就够了。如果你已经能顺利写出BFS和DFS那建议找一些真实的AI项目代码来读重点看数据处理类的模块——你会发现栈和队列就像螺丝钉在那些看似复杂的系统里频繁地转着读懂它们的那一刻你会对整个系统多很多掌控感。我在实际项目里测试过很多方案最大的体会其实是千万不要因为栈和队列写起来只有几行代码就轻视它们。越是基础的结构越能在复杂的系统里反复出现。把它的原理、性能特性、使用边界彻底搞明白再去看AI方向的代码和算法题很多原先觉得高深的东西都会一下子通透起来。