前言
二叉树是算法面试高频考点,而「翻转二叉树」是入门必刷的基础递归题。当年 Homebrew 作者面试谷歌,因不会写这道题被拒的段子更是让这道题火出圈。本文结合 LeetCode 原题,用 JavaScript 实现递归解法,逐行拆解思路,同时补充迭代拓展方案,看完彻底吃透二叉树递归分治思想。
一、题目信息
题目描述
给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。翻转二叉树的本质:树上每一个节点的左右子节点互相交换。
样例演示
示例 1
输入数组:root = [4,2,7,1,3,6,9]原树结构:
plaintext
markdown
4
/ \
2 7
/ \ / \
1 3 6 9
翻转后:
plaintext
markdown
4
/ \
7 2
/ \ / \
9 6 3 1
输出数组:[4,7,2,9,6,3,1]
示例 2
输入:root = [2,1,3]原树:
plaintext
2
/ \
1 3
翻转后:
plaintext
2
/ \
3 1
输出:[2,3,1]
示例 3
输入空树 root = [],直接输出空树 []
标签分类
难度:简单考点:二叉树、深度优先搜索(DFS)、递归
二、递归解题思路
核心分治思想
我们可以把整棵树拆分成「根节点 + 左子树 + 右子树」三部分:
- 先把左子树完整翻转
- 再把右子树完整翻转
- 交换当前根节点的左右指针
- 递归终止条件:当前节点为
null,没有子树,直接返回
通俗理解:自底向上处理,先把下层所有子树翻转完毕,再交换当前节点的左右分支。
完整 AC 代码(和截图完全对应)
javascript
运行
ini
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
// 递归出口:空节点无需翻转
if (!root) return root;
// 递归翻转左右子树
const left = invertTree(root.left);
const right = invertTree(root.right);
// 交换当前节点左右子节点
root.left = right;
root.right = left;
return root;
};
三、逐行代码解析
if (!root) return root;递归边界:如果遍历到空节点,不存在左右孩子,直接终止递归并返回自身。对应测试用例 3 空树的场景。const left = invertTree(root.left);递归进入左子树,拿到完全翻转完成后的左子树根节点。const right = invertTree(root.right);递归进入右子树,拿到完全翻转完成后的右子树根节点。
javascript
运行
ini
root.left = right;
root.right = left;
核心操作:交换当前节点的左右指针,完成当前层节点的翻转。
return root;把翻转完成的当前节点返回给上层父节点,供上层交换使用。
流程举例(示例 2 小树推演)
输入树 2 -> 左1、右3
-
执行
invertTree(2),先递归invertTree(1)- 节点 1 左右都是 null,left=null、right=null,交换后还是 1,返回 1
-
再递归
invertTree(3)- 节点 3 左右都是 null,交换后还是 3,返回 3
-
交换节点 2:left=3,right=1,返回 2,得到翻转后的树
四、复杂度分析
-
**时间复杂度 O (n)**n 为二叉树总节点数量,每个节点只会被递归访问、交换 1 次。
-
**空间复杂度 O (h)**h 代表二叉树高度,空间由递归调用栈占用:
- 平衡二叉树:树高 h = logn,空间复杂度 O (logn)
- 单边倾斜树(链式树):树高 h = n,空间复杂度 O (n)
五、拓展:BFS 层序迭代解法(规避递归栈溢出)
递归依赖调用栈,如果二叉树深度极大,会出现栈溢出报错。可以用队列实现广度优先迭代,逐层交换节点:
javascript
运行
ini
var invertTree = function(root) {
if (!root) return root;
// 队列存储待处理节点
const queue = [root];
while (queue.length) {
const node = queue.shift();
// 交换当前节点左右
[node.left, node.right] = [node.right, node.left];
// 子节点入队,等待下一轮处理
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
return root;
};
思路:借助队列逐层遍历每一个节点,只要取出节点就交换左右,再把左右子节点放进队列循环处理。
六、补充:前序递归写法(先交换再递归)
上面代码是后序遍历(先处理子树,再交换当前节点),我们也可以先交换当前节点,再递归子树,逻辑等价:
javascript
运行
scss
var invertTree = function(root) {
if (!root) return root;
// 先交换当前节点左右
[root.left, root.right] = [root.right, root.left];
// 再递归处理子树
invertTree(root.left);
invertTree(root.right);
return root;
};
七、总结
-
翻转二叉树核心逻辑:每个节点交换左右子节点,递归分治处理整棵树;
-
递归固定模板三步:判空出口 → 递归处理左右子树 → 交换指针;
-
两种实现对比:
- DFS 递归:代码极简,行数少,面试手写首选;
- BFS 队列迭代:无调用栈溢出风险,适合深度极大的树;
-
这道题是二叉树递归的敲门砖,掌握交换左右节点的思路后,对称二叉树、二叉树镜像等同类题目都可以复用这套模板。