dfs递归必须用visited标记避免无限循环,树可省略但图必加;标记需在函数首行完成,引用传参邻接表,终止条件和边界检查须严格匹配数据范围。

DFS递归实现必须处理访问标记
不加访问标记的 dfs 在图中会无限循环,哪怕只是无向图的两个节点互相连通也会栈溢出。树结构可以省略,但只要输入可能是图(比如邻接表存的网格、社交关系),就必须用 visited 数组或集合。
常见错误现象:std::stack_overflow 或程序卡死;调试时发现同一节点被反复进入 dfs 函数多次。
-
visited类型选std::vector<bool></bool>(下标连续)或std::unordered_set<int></int>(节点编号稀疏) - 标记时机必须在进入函数**第一行**就设为
true,不能放在循环里或递归调用后 - 回溯场景(如求所有路径)需要在递归返回后重置
visited[i] = false;单纯遍历则不需要
邻接表传参别用值传递
把 std::vector<:vector>></:vector> 邻接表以值方式传进 dfs,每次递归都拷贝整个图结构,时间和内存直接爆炸。C++ 默认按值传递对象,这点和 Python 不同,容易误踩。
使用场景:图规模稍大(边数 > 1e4)就会明显变慢,Clang/GCC 甚至可能触发编译器优化警告。
立即学习“C++免费学习笔记(深入)”;
- 统一用
const std::vector<:vector>>& graph</:vector>引用传参 - 如果函数内要修改图(极少见),改用
std::vector<:vector>>&</:vector>,但需明确文档说明副作用 - 不要为了“看起来安全”而用
std::shared_ptr包一层——增加间接访问开销,且无必要
递归终止条件不止是“空节点”
写成 if (!node) return; 只适用于指针树;对索引建模的图或数组树,终止条件得看数据范围和边界检查。
参数差异:节点编号从 0 还是 1 开始?图大小是 n 还是动态算出来的?这些直接影响 if (u = n) 这类判断是否漏边。
- 用
std::vector存节点时,下标合法范围是[0, graph.size()),不是[1, graph.size()] - 网格类 DFS(如岛屿数量)要同时检查
x和y是否越界,别只判一个维度 - 遇到
std::out_of_range错误,大概率是这里没兜住非法索引
避免隐式类型转换导致的栈爆炸
把 int 节点 ID 传给期望 size_t 的容器下标操作,一旦传入负值(比如初始化错写成 -1),会被转成极大正数,触发越界访问或无限递归。
性能影响:这类 bug 往往不报错,而是静默读到野内存,行为不可预测;ASan 可捕获,但线上环境难复现。
- 统一用有符号整型(
int)处理节点 ID,除非你明确需要 >2G 的节点数 - 访问
vector前先断言:assert(u >= 0 && u - 禁用
-Wsign-conversion编译选项是掩耳盗铃,不如写清楚类型意图
最麻烦的不是写错逻辑,而是图结构本身带环、不连通、含孤立点——这些情况不会报错,但会让 DFS 结果不全。动手前先用小样例手画调用栈,比加十个断点更管用。










