LeetCode简单题------翻转二叉树,这道题看似简单,却能帮我们快速掌握二叉树的递归和迭代遍历思路,非常适合入门练习。今天就带大家一步步拆解题目,详解两种常用解法,附上完整代码和易错点提醒,新手也能轻松看懂!
一、题目解读
题目很直白:给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
什么是翻转二叉树?其实就是把每一个节点的「左子树」和「右子树」互换。举个简单例子:如果根节点有左孩子3、右孩子9,翻转后就变成左孩子9、右孩子3;同理,每个子节点也需要做同样的操作,直到所有节点都完成左右互换。
先看题目给出的TreeNode类定义(TypeScript版本),这个是解题的基础,必须先理解:
typescript
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val) // 节点值,默认0
this.left = (left === undefined ? null : left) // 左子节点,默认null
this.right = (right === undefined ? null : right) // 右子节点,默认null
}
}
二、核心思路
翻转二叉树的核心逻辑很简单:遍历每一个节点,将该节点的左右子节点互换。
关键在于「如何遍历所有节点」------ 二叉树的遍历主要有两种方式:递归遍历(深度优先DFS)和迭代遍历(广度优先BFS,常用队列实现)。对应这两种遍历方式,我们可以写出两种解法,下面分别详细讲解。
三、解法一:递归实现(深度优先DFS)
1. 思路拆解
递归的核心是「自顶向下」处理节点,遵循「终止条件→处理当前节点→递归处理子节点」的逻辑:
-
终止条件:如果当前节点为null(空树或叶子节点的子节点),直接返回null,无需翻转;
-
处理当前节点:用一个临时变量temp,保存当前节点的左子节点,然后将左子节点替换为右子节点,右子节点替换为temp(完成左右互换);
-
递归处理:分别递归翻转当前节点的左子树和右子树;
-
返回结果:返回处理后的当前节点(作为父节点的子节点)。
2. 完整代码
typescript
function invertTree_1(root: TreeNode | null): TreeNode | null {
// 终止条件:当前节点为空,直接返回null
if (root === null) {
return null;
}
// 互换当前节点的左右子节点
const temp = root.left;
root.left = root.right;
root.right = temp;
// 递归翻转左子树和右子树
invertTree_1(root.left);
invertTree_1(root.right);
// 返回翻转后的当前节点
return root;
};
3. 关键解析
为什么递归能生效?因为递归会逐层深入到二叉树的最底层(叶子节点),然后逐层回溯,确保每一个节点的左右子节点都被互换。
举个例子:一棵三层二叉树(根1,左2、右3;2的左4、右5),递归过程是:
-
处理根1,互换左2和右3 → 根1的左是3、右是2;
-
递归处理根1的左子节点3(3是叶子节点,互换后还是3);
-
递归处理根1的右子节点2,互换2的左4和右5 → 2的左是5、右是4;
-
递归处理2的左子节点5(叶子节点)、右子节点4(叶子节点);
-
回溯返回,最终得到翻转后的二叉树。
时间复杂度:O(n),n是二叉树的节点数,每个节点只被访问一次;
空间复杂度:O(h),h是二叉树的高度,递归调用栈的深度等于树的高度(最坏情况是斜树,h=n,空间复杂度O(n))。
四、解法二:迭代实现(广度优先BFS)
递归虽然简洁,但如果树的高度很高,可能会出现栈溢出的问题(比如斜树,递归深度达到n)。这时可以用迭代的方式(广度优先),用队列来存储待处理的节点,避免递归栈溢出。
1. 思路拆解
广度优先的核心是「逐层处理」,用队列实现"先进先出",确保每一层的节点都被处理完再处理下一层:
-
终止条件(边界处理):如果根节点为null,直接返回null;
-
初始化队列:将根节点加入队列,队列中存储的是「待互换左右子节点的节点」;
-
循环处理:只要队列不为空,就取出队列头部的节点,互换其左右子节点;
-
入队操作:将互换后的左子节点、右子节点(如果不为null)加入队列,等待后续处理;
-
循环结束后,返回根节点(此时整棵树已翻转完成)。
2. 完整代码
typescript
function invertTree_2(root: TreeNode | null): TreeNode | null {
// 边界处理:根节点为空,直接返回null
if (root === null) {
return null;
}
// 初始化队列,将根节点加入队列
const queue: TreeNode[] = [root];
// 队列不为空,继续处理
while (queue.length > 0) {
// 取出队列头部的节点(先进先出)
const node = queue.shift();
// 防止queue.shift()返回undefined(实际不会触发,因队列长度>0时才取)
if (node === undefined) {
return null;
}
// 互换当前节点的左右子节点
const temp = node.left;
node.left = node.right;
node.right = temp;
// 左子节点不为null,加入队列,等待处理
if (node.left !== null) {
queue.push(node.left);
}
// 右子节点不为null,加入队列,等待处理
if (node.right !== null) {
queue.push(node.right);
}
}
// 所有节点处理完毕,返回翻转后的根节点
return root;
};
3. 关键解析
队列的作用是"缓存"待处理的节点,确保我们能逐层处理每一个节点。还是用上面的三层二叉树举例,迭代过程是:
-
队列初始化:[1],取出1,互换左右→1的左3、右2;将3、2加入队列→队列变为[3,2];
-
取出3,互换左右(3是叶子节点,无变化);3的左右都为null,不加入队列→队列变为[2];
-
取出2,互换左右→2的左5、右4;将5、4加入队列→队列变为[5,4];
-
取出5,互换左右(无变化);不加入队列→队列变为[4];
-
取出4,互换左右(无变化);不加入队列→队列变为空,循环结束;
-
返回根节点1,翻转完成。
时间复杂度:O(n),每个节点只被取出队列一次,处理一次;
空间复杂度:O(n),队列中最多存储一层的节点数,最坏情况是完全二叉树的最后一层,节点数约为n/2,所以空间复杂度O(n)。
五、两种解法对比
| 解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 递归(DFS) | 自顶向下,递归遍历子树 | O(n) | O(h)(h为树高) | 树的高度较低,代码简洁易写 |
| 迭代(BFS) | 逐层处理,队列缓存节点 | O(n) | O(n) | 树的高度很高(避免递归栈溢出) |
六、易错点提醒
-
边界处理:一定要先判断root是否为null,否则会出现"访问null的left/right"的错误;
-
临时变量:互换左右子节点时,必须用临时变量保存,不能直接写root.left = root.right、root.right = root.left(这样会导致右子节点被覆盖,最终左右子节点相同);
-
队列操作:迭代解法中,queue.shift()会取出队列头部元素,若队列长度为0,shift()返回undefined,所以加了一层判断(实际不会触发,但能避免语法报错);
-
递归终止:递归解法中,处理完当前节点后,必须递归调用左右子树,否则只有根节点的左右子节点被互换,子树不会翻转。
七、总结
翻转二叉树是二叉树操作的入门题,核心就是「遍历节点+左右互换」。两种解法各有优势:递归写法简洁,适合日常刷题;迭代写法更稳定,适合处理极端场景(高树)。
通过这道题,我们可以巩固二叉树的递归和广度优先遍历思路,后续遇到二叉树的修改、遍历类题目,都可以沿用类似的逻辑。