二叉树的递归/层序遍历
了解了 二叉树基本概念和几种特殊的二叉树,本文来讲解如何遍历和访问二叉树的节点。
递归遍历(DFS)
二叉树的遍历框架:
// 基本的二叉树节点
class TreeNode {
int val;
TreeNode left, right;
}
// 二叉树的遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
traverse(root.left);
traverse(root.right);
}
// 基本的二叉树节点
class TreeNode {
public:
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 二叉树的遍历框架
void traverse(TreeNode* root) {
if (root == nullptr) {
return;
}
traverse(root->left);
traverse(root->right);
}
# 基本的二叉树节点
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# 二叉树的遍历框架
def traverse(root: TreeNode):
if root is None:
return
traverse(root.left)
traverse(root.right)
// 基本的二叉树节点
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 二叉树的遍历框架
func traverse(root *TreeNode) {
if root == nil {
return
}
traverse(root.Left)
traverse(root.Right)
}
// 基本的二叉树节点
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 二叉树的遍历框架
var traverse = function(root) {
if (root === null) {
return;
}
traverse(root.left);
traverse(root.right);
}
这段短小精干的代码为什么能遍历二叉树?又是以什么顺序遍历二叉树的?
对于 traverse
这样的递归遍历函数,你就可以把它理解成一个在二叉树结构上游走的指针,下面我用一个可视化就能直观地给你展现这个函数是如何遍历二叉树的。
点开这个可视化面板,右侧 root
指针的位置就是当前正在访问的节点,即 traverse
函数当前遍历到的位置,橙黄色的部分展示了 traverse
函数的堆栈。你可以点击左上角的播放按钮,观察 root
指针的位置变化:
traverse
函数的遍历顺序就是一直往左子节点走,直到遇到空指针不能再走了,才尝试往右子节点走一步;然后再一直尝试往左子节点走,如此循环;如果左右子树都走完了,则返回上一层父节点。其实看代码也能看出来,先递归调用的 root.left
,然后才递归调用的 root.right
嘛。
这个递归遍历节点顺序是固定的,务必记住这个顺序,否则你肯定玩不转二叉树结构。
那么有一些数据结构基础的读者肯定要问了:不对呀,只要上过大学的数据结构课程都知道,二叉树有前/中/后序三种遍历,会得到三种不同顺序的结果。为啥你这里说递归遍历节点的顺序是固定的呢?
这个问题很好,我来解答一下。
重要
递归遍历的顺序,即 traverse
函数访问节点的顺序确实是固定的。正如上面那个可视化面板,root
指针在树上移动的顺序是固定的。
但是你在 traverse
函数中不同位置写代码,效果是可以不一样的。前中后序遍历的结果不同,原因是因为你把代码写在了不同位置,所以产生了不同的效果。
比方说,刚进入一个节点的时候,你还对它的子节点一无所知,而当你要离开一个节点的时候,它的所有子节点你都遍历过了。那么在这两种情况下写的代码,肯定是可以有不同的效果的。
你所谓的前中后序遍历,其实就是在上述二叉树遍历框架的不同位置写代码:
// 二叉树的遍历框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
// 二叉树的遍历框架
void traverse(TreeNode* root) {
if (root == nullptr) {
return;
}
// 前序位置
traverse(root->left);
// 中序位置
traverse(root->right);
// 后序位置
}
# 二叉树的遍历框架
def traverse(root):
if root is None:
return
# 前序位置
traverse(root.left)
# 中序位置
traverse(root.right)
# 后序位置
// 二叉树的遍历框架
func traverse(root *TreeNode) {
if root == nil {
return
}
// 前序位置
traverse(root.Left)
// 中序位置
traverse(root.Right)
// 后序位置
}
// 二叉树的遍历框架
var traverse = function(root) {
if (root === null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
};
前序位置的代码会在进入节点时执行;中序位置的代码会在左子树遍历完成后,遍历右子树之前执行;后序位置的代码会在左右子树遍历完成后执行:
二叉树的前中后序位置非常重要,我会在 二叉树算法思想(纲领篇) 和习题中深入探讨二叉树的前中后序遍历的区别和联系,以及如何运用到回溯算法、动态规划算法中,这里不展开。
那还有读者可能会问,为这个 traverse
函数要先递归遍历 root.left
后递归遍历 root.right
呢?能不能反过来?
当然可以,但一般所说的前中后序是默认先左后右的,所以这是个约定俗成的习惯。如果就你非要先右后左,那么你的前中后序遍历也和一般人理解的不一样,这不是徒增沟通成本?所以,除非做题的的时候有特殊的需要,否则就按照先左后右的顺序写代码即可。
BST 的中序遍历结构有序
这里需要强调的是,二叉搜索树(BST) 的中序遍历结果是有序的,这是 BST 的一个重要性质。
可以看这个可视化面板,点击其中 res.push(root.val);
这一行代码,就可以看到中序遍历访问节点的顺序:
在后续 BST 相关习题集 中,会有一些题目利用到这个特性。
层序遍历(BFS)
上面的递归遍历是依赖函数堆栈递归遍历二叉树的,如果把递归函数 traverse
看做一个指针,那么这个指针在二叉树上游走的顺序大概是从最左侧开始,一列一列走到最右侧。
二叉树的层序遍历,顾名思义,就是一层一层地遍历二叉树。这个遍历方式需要借助队列来实现,而且根据不同的需求,主要有三种不同的写法,下面一一列举。
写法一
这是最简单的写法,代码如下:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
// 访问 cur 节点
System.out.println(cur.val);
// 把 cur 的左右子节点加入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* cur = q.front();
q.pop();
// 访问 cur 节点
std::cout << cur->val << std::endl;
// 把 cur 的左右子节点加入队列
if (cur->left != nullptr) {
q.push(cur->left);
}
if (cur->right != nullptr) {
q.push(cur->right);
}
}
}
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
while q:
cur = q.popleft()
# 访问 cur 节点
print(cur.val)
# 把 cur 的左右子节点加入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []*TreeNode{}
q = append(q, root)
for len(q) > 0 {
cur := q[0]
q = q[1:]
// 访问 cur 节点
fmt.Println(cur.Val)
// 把 cur 的左右子节点加入队列
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点
console.log(cur.val);
// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
}
这种写法的优缺点
这种写法最大的优势就是简单。每次把队头元素拿出来,然后把它的左右子节点加入队列,就完事了。
但是这种写法的缺点是,无法知道当前节点在第几层。知道节点的层数是个常见的需求,比方说让你收集每一层的节点,或者计算二叉树的最小深度等等。
所以这种写法虽然简单,但用的不多,下面介绍的写法会更常见一些。
写法二
对上面的解法稍加改造,就得出了下面这种写法:
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 访问 cur 节点,同时知道它所在的层数
System.out.println("depth = " + depth + ", val = " + cur.val);
// 把 cur 的左右子节点加入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
depth++;
}
}
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
queue<TreeNode*> q;
q.push(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
int depth = 1;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
TreeNode* cur = q.front();
q.pop();
// 访问 cur 节点,同时知道它所在的层数
cout << "depth = " << depth << ", val = " << cur->val << endl;
// 把 cur 的左右子节点加入队列
if (cur->left != nullptr) {
q.push(cur->left);
}
if (cur->right != nullptr) {
q.push(cur->right);
}
}
depth++;
}
}
from collections import deque
def levelOrderTraverse(root):
if root is None:
return
q = deque()
q.append(root)
# 记录当前遍历到的层数(根节点视为第 1 层)
depth = 1
while q:
sz = len(q)
for i in range(sz):
cur = q.popleft()
# 访问 cur 节点,同时知道它所在的层数
print(f"depth = {depth}, val = {cur.val}")
# 把 cur 的左右子节点加入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
depth += 1
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []*TreeNode{root}
// 记录当前遍历到的层数(根节点视为第 1 层)
depth := 1
for len(q) > 0 {
// 获取当前队列长度
sz := len(q)
for i := 0; i < sz; i++ {
// 弹出队列头
cur := q[0]
q = q[1:]
// 访问 cur 节点,同时知道它所在的层数
fmt.Printf("depth = %d, val = %d\n", depth, cur.Val)
// 把 cur 的左右子节点加入队列
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
depth++
}
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
var q = [];
q.push(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
var depth = 1;
while (q.length !== 0) {
var sz = q.length;
for (var i = 0; i < sz; i++) {
var cur = q.shift();
// 访问 cur 节点,同时知道它所在的层数
console.log("depth = " + depth + ", val = " + cur.val);
// 把 cur 的左右子节点加入队列
if (cur.left !== null) {
q.push(cur.left);
}
if (cur.right !== null) {
q.push(cur.right);
}
}
depth++;
}
};
注意代码中的内层 for 循环:
int sz = q.size();
for (int i = 0; i < sz; i++) {
...
}
这个变量 i
记录的是节点 cur
是当前层的第几个,大部分算法题中都不会用到这个变量,所以你完全可以改用下面的写法:
int sz = q.size();
while (sz-- > 0) {
...
}
这个属于细节问题,按照自己的喜好来就行。
但是注意队列的长度 sz
一定要在循环开始前保存下来,因为在循环过程中队列的长度是会变化的,不能直接用 q.size()
作为循环条件。
这种写法就可以记录下来每个节点所在的层数,可以解决诸如二叉树最小深度这样的问题,是我们最常用的层序遍历写法。
写法三
既然写法二是最常见的,为啥还有个写法三呢?因为要给后面的进阶内容做铺垫。
现在我们只是在探讨二叉树的层序遍历,但是二叉树的层序遍历可以衍生出 多叉树的层序遍历,图的 BFS 遍历,以及经典的 BFS 暴力穷举算法框架,所以这里要拓展延伸一下。
回顾写法二,我们每向下遍历一层,就给 depth
加 1,可以理解为每条树枝的权重是 1,二叉树中每个节点的深度,其实就是从根节点到这个节点的路径权重和,且同一层的所有节点,路径权重和都是相同的。
那么假设,如果每条树枝的权重和可以是任意值,现在让你层序遍历整棵树,打印每个节点的路径权重和,你会怎么做?
这样的话,同一层节点的路径权重和就不一定相同了,写法二这样只维护一个 depth
变量就无法满足需求了。
写法三就是为了解决这个问题,在写法一的基础上添加一个 State
类,让每个节点自己负责维护自己的路径权重和,代码如下:
class State {
TreeNode node;
int depth;
State(TreeNode node, int depth) {
this.node = node;
this.depth = depth;
}
}
void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<State> q = new LinkedList<>();
// 根节点的路径权重和是 1
q.offer(new State(root, 1));
while (!q.isEmpty()) {
State cur = q.poll();
// 访问 cur 节点,同时知道它的路径权重和
System.out.println("depth = " + cur.depth + ", val = " + cur.node.val);
// 把 cur 的左右子节点加入队列
if (cur.node.left != null) {
q.offer(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right != null) {
q.offer(new State(cur.node.right, cur.depth + 1));
}
}
}
class State {
public:
TreeNode* node;
int depth;
State(TreeNode* node, int depth) : node(node), depth(depth) {}
};
void levelOrderTraverse(TreeNode* root) {
if (root == nullptr) {
return;
}
queue<State> q;
// 根节点的路径权重和是 1
q.push(State(root, 1));
while (!q.empty()) {
State cur = q.front();
q.pop();
// 访问 cur 节点,同时知道它的路径权重和
cout << "depth = " << cur.depth << ", val = " << cur.node->val << endl;
// 把 cur 的左右子节点加入队列
if (cur.node->left != nullptr) {
q.push(State(cur.node->left, cur.depth + 1));
}
if (cur.node->right != nullptr) {
q.push(State(cur.node->right, cur.depth + 1));
}
}
}
class State:
def __init__(self, node, depth):
self.node = node
self.depth = depth
def levelOrderTraverse(root):
if root is None:
return
q = deque()
# 根节点的路径权重和是 1
q.append(State(root, 1))
while q:
cur = q.popleft()
# 访问 cur 节点,同时知道它的路径权重和
print(f"depth = {cur.depth}, val = {cur.node.val}")
# 把 cur 的左右子节点加入队列
if cur.node.left is not None:
q.append(State(cur.node.left, cur.depth + 1))
if cur.node.right is not None:
q.append(State(cur.node.right, cur.depth + 1))
type State struct {
node *TreeNode
depth int
}
func levelOrderTraverse(root *TreeNode) {
if root == nil {
return
}
q := []State{{root, 1}}
for len(q) > 0 {
cur := q[0]
q = q[1:]
// 访问 cur 节点,同时知道它的路径权重和
fmt.Printf("depth = %d, val = %d\n", cur.depth, cur.node.Val)
// 把 cur 的左右子节点加入队列
if cur.node.Left != nil {
q = append(q, State{cur.node.Left, cur.depth + 1})
}
if cur.node.Right != nil {
q = append(q, State{cur.node.Right, cur.depth + 1})
}
}
}
function State(node, depth) {
this.node = node;
this.depth = depth;
}
var levelOrderTraverse = function(root) {
if (root === null) {
return;
}
// @visualize bfs
var q = [];
// 根节点的路径权重和是 1
q.push(new State(root, 1));
while (q.length !== 0) {
var cur = q.shift();
// 访问 cur 节点,同时知道它的路径权重和
console.log("depth = " + cur.depth + ", val = " + cur.node.val);
// 把 cur 的左右子节点加入队列
if (cur.node.left !== null) {
q.push(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right !== null) {
q.push(new State(cur.node.right, cur.depth + 1));
}
}
};
这样每个节点都有了自己的 depth
变量,是最灵活的,可以满足所有 BFS 算法的需求。但是由于要额外定义一个 State
类比较麻烦,所以非必要的话,用写法二就够了。
其实你很快就会学到,这种边带有权重的场景属于图结构算法,在之后的 BFS 算法习题集 和 dijkstra 算法 中,才会用到这种写法。
其他遍历?
二叉树的遍历方式只有上面两种,也许有其他的写法,但都是表现形式上的差异,本质上不可能跳出上面两种遍历方式。
比方说,你可能看到用栈来迭代遍历二叉树的代码。但这本质还是是递归遍历,只不过他手动维护栈模拟递归调用罢了。
再比如,你还可能看到递归地一层层遍历二叉树的代码。但这本质还是层序遍历,只不过他把层序遍历代码中的 for 循环用递归的形式展现了。
总之,不要被表象迷惑,二叉树的遍历方式就上面两种,结合后面的教程和习题,你把这两种遍历方式玩明白,一切暴力穷举算法都小菜一碟。
为什么 BFS 常用来寻找最短路径
用可视化面板结合一道例题,你立刻就能明白了。
来看力扣第 111 题「二叉树的最小深度」:
111. 二叉树的最小深度 | 力扣 | LeetCode |
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6] 输出:5
提示:
- 树中节点数的范围在
[0, 105]
内 -1000 <= Node.val <= 1000
二叉树的最小深度即「根节点到最近的叶子节点的距离」,所以这道题本质上就是让你求最短距离。
DFS 递归遍历和 BFS 层序遍历都可以解决这道题,先看 DFS 递归遍历的解法:
class Solution {
// 记录最小深度(根节点到最近的叶子节点的距离)
private int minDepth = Integer.MAX_VALUE;
// 记录当前遍历到的节点深度
private int currentDepth = 0;
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 从根节点开始 DFS 遍历
traverse(root);
return minDepth;
}
private void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置进入节点时增加当前深度
currentDepth++;
// 如果当前节点是叶子节点,更新最小深度
if (root.left == null && root.right == null) {
minDepth = Math.min(minDepth, currentDepth);
}
traverse(root.left);
traverse(root.right);
// 后序位置离开节点时减少当前深度
currentDepth--;
}
}
class Solution {
private:
// 记录最小深度(根节点到最近的叶子节点的距离)
int minDepthValue = INT_MAX;
// 记录当前遍历到的节点深度
int currentDepth = 0;
public:
int minDepth(TreeNode* root) {
if (root == nullptr) {
return 0;
}
// 从根节点开始 DFS 遍历
traverse(root);
return minDepthValue;
}
void traverse(TreeNode* root) {
if (root == nullptr) {
return;
}
// 前序位置进入节点时增加当前深度
currentDepth++;
// 如果当前节点是叶子节点,更新最小深度
if (root->left == nullptr && root->right == nullptr) {
minDepthValue = min(minDepthValue, currentDepth);
}
traverse(root->left);
traverse(root->right);
// 后序位置离开节点时减少当前深度
currentDepth--;
}
};
class Solution:
def __init__(self):
# 记录最小深度(根节点到最近的叶子节点的距离)
self.minDepthValue = float('inf')
# 记录当前遍历到的节点深度
self.currentDepth = 0
def minDepth(self, root: TreeNode) -> int:
if root is None:
return 0
# 从根节点开始 DFS 遍历
self.traverse(root)
return self.minDepthValue
def traverse(self, root: TreeNode) -> None:
if root is None:
return
# 前序位置进入节点时增加当前深度
self.currentDepth += 1
# 如果当前节点是叶子节点,更新最小深度
if root.left is None and root.right is None:
self.minDepthValue = min(self.minDepthValue, self.currentDepth)
self.traverse(root.left)
self.traverse(root.right)
# 后序位置离开节点时减少当前深度
self.currentDepth -= 1
func minDepth(root *TreeNode) int {
if root == nil {
return 0
}
// 记录最小深度(根节点到最近的叶子节点的距离)
minDepthValue := math.MaxInt32
// 记录当前遍历到的节点深度
currentDepth := 0
var traverse func(*TreeNode)
traverse = func(root *TreeNode) {
if root == nil {
return
}
// 前序位置进入节点时增加当前深度
currentDepth++
// 如果当前节点是叶子节点,更新最小深度
if root.Left == nil && root.Right == nil {
minDepthValue = min(minDepthValue, currentDepth)
}
traverse(root.Left)
traverse(root.Right)
// 后序位置离开节点时减少当前深度
currentDepth--
}
// 从根节点开始 DFS 遍历
traverse(root)
return minDepthValue
}
var minDepth = function(root) {
if (root === null) {
return 0;
}
// 记录最小深度(根节点到最近的叶子节点的距离)
let minDepthValue = Infinity;
// 记录当前遍历到的节点深度
let currentDepth = 0;
const traverse = function(root) {
if (root === null) {
return;
}
// 前序位置进入节点时增加当前深度
currentDepth++;
// 如果当前节点是叶子节点,更新最小深度
if (root.left === null && root.right === null) {
minDepthValue = Math.min(minDepthValue, currentDepth);
}
traverse(root.left);
traverse(root.right);
// 后序位置离开节点时减少当前深度
currentDepth--;
};
// 从根节点开始 DFS 遍历
traverse(root);
return minDepthValue;
};
每当遍历到一条树枝的叶子节点,就会更新最小深度,当遍历完整棵树后,就能算出整棵树的最小深度。
你能不能在不遍历完整棵树的情况下,提前结束算法?不可以,因为你必须确切的知道每条树枝的深度(根节点到叶子节点的距离),才能找到最小的那个。
下面来看 BFS 层序遍历的解法。按照 BFS 从上到下逐层遍历二叉树的特点,当遍历到第一个叶子节点时,就能得到最小深度:
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// root 本身就是一层,depth 初始化为 1
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
// 遍历当前层的节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 判断是否到达叶子结点
if (cur.left == null && cur.right == null)
return depth;
// 将下一层节点加入队列
if (cur.left != null)
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
// 这里增加步数
depth++;
}
return depth;
}
}
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == nullptr) return 0;
queue<TreeNode*> q;
q.push(root);
// root 本身就是一层,depth 初始化为 1
int depth = 1;
while (!q.empty()) {
int sz = q.size();
// 遍历当前层的节点
for (int i = 0; i < sz; i++) {
TreeNode* cur = q.front();
q.pop();
// 判断是否到达叶子结点
if (cur->left == nullptr && cur->right == nullptr)
return depth;
// 将下一层节点加入队列
if (cur->left != nullptr)
q.push(cur->left);
if (cur->right != nullptr)
q.push(cur->right);
}
// 这里增加步数
depth++;
}
return depth;
}
};
class Solution:
def minDepth(self, root: TreeNode) -> int:
if root is None:
return 0
q = deque([root])
# root 本身就是一层,depth 初始化为 1
depth = 1
while q:
sz = len(q)
# 遍历当前层的节点
for _ in range(sz):
cur = q.popleft()
# 判断是否到达叶子结点
if cur.left is None and cur.right is None:
return depth
# 将下一层节点加入队列
if cur.left is not None:
q.append(cur.left)
if cur.right is not None:
q.append(cur.right)
# 这里增加步数
depth += 1
return depth
func minDepth(root *TreeNode) int {
if root == nil {
return 0
}
q := []*TreeNode{root}
// root 本身就是一层,depth 初始化为 1
depth := 1
for len(q) > 0 {
sz := len(q)
// 遍历当前层的节点
for i := 0; i < sz; i++ {
cur := q[0]
q = q[1:]
// 判断是否到达叶子结点
if cur.Left == nil && cur.Right == nil {
return depth
}
// 将下一层节点加入队列
if cur.Left != nil {
q = append(q, cur.Left)
}
if cur.Right != nil {
q = append(q, cur.Right)
}
}
// 这里增加步数
depth++
}
return depth
}
var minDepth = function(root) {
if (root === null) return 0;
let q = [root];
// root 本身就是一层,depth 初始化为 1
let depth = 1;
while (q.length > 0) {
let sz = q.length;
// 遍历当前层的节点
for (let i = 0; i < sz; i++) {
let cur = q.shift();
// 判断是否到达叶子结点
if (cur.left === null && cur.right === null)
return depth;
// 将下一层节点加入队列
if (cur.left !== null)
q.push(cur.left);
if (cur.right !== null)
q.push(cur.right);
}
// 这里增加步数
depth++;
}
return depth;
};
当它遍历到第二行的时候,就遇到第一个叶子节点了,这个叶子节点就是距离根节点最近的叶子节点,所以此时算法就结束了。
点击 let result =
这段代码,观察二叉树上节点的颜色,即可直观地看到 BFS 算法并没有遍历整棵树就找到了最小深度。
综上,你应该能理解为啥 BFS 算法经常用来寻找最短路径了:
由于 BFS 逐层遍历的逻辑,第一次遇到目标节点时,所经过的路径就是最短路径,算法可能并不需要遍历完所有节点就能提前结束。
DFS 遍历当然也可以用来寻找最短路径,但必须遍历完所有节点才能得到最短路径。
从时间复杂度的角度来看,两种算法在最坏情况下都会遍历所有节点,时间复杂度都是 ,但在一般情况下,显然 BFS 算法的实际效率会更高。所以在寻找最短路径的问题中,BFS 算法是首选。
为什么 DFS 常用来寻找所有路径
在寻找所有路径的问题中,你会发现 DFS 算法用的比较多,BFS 算法似乎用的不多。
理论上两种遍历算法都是可以的,只不过 BFS 算法寻找所有路径的代码比较复杂,DFS 算法代码比较简洁。
你想啊,就以二叉树为例,如果要用 BFS 算法来寻找所有路径(根节点到每个叶子节点都是一条路径),队列里面就不能只放节点了,而需要使用第三种写法,新建一个 State
类,把当前节点以及到达当前节点的路径都存进去,这样才能正确维护每个节点的路径,最终计算出所有路径。
而使用 DFS 算法就简单了,它本就是一条树枝一条树枝从左往右遍历的,每条树枝就是一条路径,所以 DFS 算法天然适合寻找所有路径。
结合可视化面板中的 JavaScript 代码,可以明显感觉 BFS 算法代码要复杂。
综上,DFS 算法在寻找所有路径的问题中更常用,而 BFS 算法在寻找最短路径的问题中更常用。