BFS遍历树需用切片模拟队列,每次循环前记录当前队列长度作为本层节点数,通过queue[0]取头、queue=queue[1:]删头安全操作,严格跳过nil节点以防panic。

用 bfs 函数配合队列切片遍历树节点
Go 没有内置队列类型,但用切片模拟足够轻量、够用。关键不是“造轮子”,而是控制好入队出队顺序和层级标记。
常见错误是把 queue = append(queue[1:], node.Left) 这类操作写成覆盖式赋值,导致漏节点或 panic;还有人用 for range queue 遍历过程中修改 queue,结果迭代行为不可控。
- 每次循环前记下当前队列长度,代表本层节点数,只处理这么多
- 用
queue[0]取头,queue = queue[1:]删头,比append(queue[1:], ...)更安全 - 树节点为空(
nil)必须跳过,否则解引用 panic
示例片段:
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
if node == nil { continue }
// 处理 node.Val
queue = append(queue, node.Left, node.Right)
}
}
图结构 BFS 必须用 map[interface{}]bool 记录已访问节点
树天然无环,图不行。不加访问标记,BFS 会反复绕圈、爆内存、最终 goroutine stack overflow。
立即学习“go语言免费学习笔记(深入)”;
注意:键类型得跟图中节点标识一致。用 int 当节点 ID 就别用 string 做 map 键;如果节点是结构体指针,直接当键会报错,得换 ID 字段或用 uintptr(unsafe.Pointer(node))(慎用)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 初始化时把起点设为已访问,再入队,避免起点被重复加入
- 每次从队列取节点后,先检查是否已访问,再处理邻居 —— 顺序不能反
- 用
make(map[int]bool)而非map[int]bool{},后者在并发写时 panic
想按层返回二维切片?别在循环里 append 大切片
很多人写 result = append(result, levelNodes) 看似没问题,但若 levelNodes 是局部切片且底层数组被复用,后续层修改会影响前面结果 —— Go 切片共享底层数组的特性在这里是坑。
典型现象:所有层都变成最后一层的数据。这不是 bug,是预期行为,只是你没意识到。
- 每次新层都用
make([]int, 0)显式创建空切片 - 或用
append([]int(nil), levelNodes...)强制分配新底层数组 - 如果层级深度固定且不大,预分配
result := make([][]int, maxDepth)更稳
性能敏感场景慎用 container/list
container/list 是双向链表,插入删除 O(1),但每次操作都要堆上分配节点,缓存不友好。实测在万级节点 BFS 中,纯切片比 list.List 快 2–3 倍,内存少一半。
它唯一优势是支持从任意位置增删 —— BFS 不需要这个。真要频繁中间插入,说明你可能选错了算法,不是 BFS 的活。
- 别为了“看起来像队列”而用
list.PushBack/list.Remove(list.Front()) - 切片
append和[1:]在小数据下几乎零开销,大数组时可考虑sync.Pool复用切片 - 如果图非常稀疏且单次 BFS 极短,链表毛刺更小;但这种情况极少
真正难的从来不是写对逻辑,而是记住:切片截取不复制数据、map 键必须可比较、nil 指针随时准备 panic —— 这些点不盯紧,BFS 写得再“标准”,跑两轮就裂开。

















