不能直接用iter()实现图的DFS迭代器,因为图非线性、无天然顺序,iter()仅支持单次正向遍历,而DFS需回溯、状态保存与路径管理;递归写入__iter__会立即执行全部遍历,破坏惰性特性,正确做法是封装栈与局部seen集,每次__next__推进一步。

为什么不能直接用 iter() 实现图的 DFS 迭代器
图不是线性容器,没有天然的“下一个节点”顺序;iter() 默认只支持单次正向遍历,而 DFS 需要回溯、状态保存和路径管理。直接在 __iter__ 里写递归会立即执行全部遍历,无法实现“按需 yield”,也破坏迭代器的惰性特性。
- 常见错误:在
__iter__中调用递归函数并全量yield所有节点 → 第一次next()就跑完整个图,后续调用抛StopIteration - 正确思路:把 DFS 状态(当前栈、已访问集合、是否继续)封装进迭代器对象本身,每次
__next__只推进一步 - 关键约束:必须避免重复访问同一节点,但不能依赖全局
visited集合——因为多个迭代器实例需彼此隔离
用栈模拟递归实现可暂停的 DFS 迭代器
核心是把递归调用栈显式转为 Python 列表,每个元素存 (node, children_iterator) 元组,这样能随时中断、恢复子节点遍历。
-
children_iterator是对当前节点邻接节点的迭代器(例如iter(self.graph[node])),不是列表——否则无法暂停 - 首次 push 根节点时,其
children_iterator应为刚创建的空迭代器,第一次__next__才真正展开它 - 每次
__next__:弹出栈顶,尝试从其children_iterator取下一个子节点;若成功,将子节点及其子迭代器压栈,并返回该子节点;若失败,继续 pop 上层 - 必须维护局部
seen集合(按迭代器实例独有),在 push 前检查,避免环导致无限循环
class GraphDFSIterator:
def __init__(self, graph, start):
self.graph = graph
self.seen = set()
self.stack = [(start, iter([]))] # 占位,首次 next 会替换为真实子迭代器
self.seen.add(start)
<pre class="brush:php;toolbar:false;">def __iter__(self):
return self
def __next__(self):
while self.stack:
node, child_iter = self.stack.pop()
try:
child = next(child_iter)
if child not in self.seen:
self.seen.add(child)
# 把当前 child_iter 放回栈(用于下次继续取兄弟节点)
self.stack.append((node, child_iter))
# 压入 child 及其子迭代器
self.stack.append((child, iter(self.graph.get(child, []))))
return child
except StopIteration:
continue
raise StopIteration
graph 数据结构如何影响迭代器写法
邻接表示方式直接决定 iter(self.graph[node]) 是否可用,以及是否需要额外转换:
立即学习“Python免费学习笔记(深入)”;
- 字典映射(
{node: [neighbor1, neighbor2]}):最直接,iter(graph[node])有效 - 邻接矩阵(二维 list):需先查行索引,再过滤非零值,
iter([i for i, v in enumerate(row) if v != 0]),性能差且不惰性 - 类对象字段(如
node.neighbors):确保neighbors是可迭代对象,而非每次调用才计算的 property - 注意:如果图含自环或多重边,
seen检查必须在 push 前做,否则可能重复入栈
什么时候该用生成器函数替代类迭代器
当不需要多次复用同一个迭代器、也不需中途暂停/恢复时,用 def dfs_iter(graph, start) 生成器更轻量。但它无法支持 for 循环中中途 break 后再次进入同一 DFS 流程——因为生成器状态不可重入。
- 适用场景:一次性遍历、调试打印、配合
itertools.islice取前 N 个节点 - 不适用场景:需要两个独立迭代器同时遍历同一图、或需在循环中根据条件切换遍历逻辑
- 生成器内部仍需用栈,不能用递归(否则无法 yield 中断),所以实际代码长度和类版本接近
- 容易忽略的点:生成器函数中若用闭包变量记录
seen,多个调用会共享该集合——必须每次新建set()
复杂的地方在于状态分离:每个迭代器必须持有自己的 stack 和 seen,且栈中每个元素都要能“冻结”子迭代器的当前进度。这不是语法问题,而是对迭代本质的理解偏差——DFS 迭代器不是“怎么遍历图”,而是“怎么把图遍历过程切片成可中断的步骤”。


















