一、递归基础模板
二叉树的核心解题思路是递归。掌握递归的基本模式是解决所有二叉树问题的基础。
递归函数的基本结构
function dfs(node) {
// 1. 终止条件(基本情况)
if (!node) return;
// 2. 处理当前节点(前序遍历位置)
// do something...
// 3. 递归处理左右子树
dfs(node.left);
dfs(node.right);
// 4. 后续处理(后序遍历位置)
// do something...
}
三种遍历方式
// 前序遍历:根 -> 左 -> 右
function preorder(node) {
if (!node) return;
console.log(node.val); // 访问根
preorder(node.left); // 遍历左
preorder(node.right); // 遍历右
}
// 中序遍历:左 -> 根 -> 右
function inorder(node) {
if (!node) return;
inorder(node.left); // 遍历左
console.log(node.val); // 访问根
inorder(node.right); // 遍历右
}
// 后序遍历:左 -> 右 -> 根
function postorder(node) {
if (!node) return;
postorder(node.left); // 遍历左
postorder(node.right); // 遍历右
console.log(node.val); // 访问根
}
二、解题思维总结
1. 递归问题的思考框架
1. 确定递归函数的返回值和参数
- 返回值:需要子树提供什么信息?
- 参数:需要父节点传递什么信息?
2. 确定终止条件
- 空节点怎么处理?
3. 确定单层递归逻辑
- 当前节点做什么处理?
- 如何调用递归函数?
- 如何利用递归返回值?
2. 常见返回值设计
| 问题类型 | 返回值设计 |
|---|---|
| 深度/高度 | number |
| 是否满足条件 | boolean |
| 需要多个信息 | [boolean, number] 或对象 |
| 路径/节点 | TreeNode 或数组 |
3. 时间复杂度与空间复杂度
- 时间复杂度:通常为 O(n),每个节点访问一次
- 空间复杂度:
- 递归栈空间:O(h),h 为树高,递归调用本身会使用系统栈来保存每一层调用的上下文,栈的深度等于递归的深度,即树的高度
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
三、高频解题技巧
技巧一:自顶向下递归(Top-Down)
适用场景:需要从根节点向下传递信息(如边界值、路径等)
核心要点:
- 父节点通过参数向子节点传递信息
- 每个节点根据传入的参数做判断
典型例题:验证二叉搜索树
var isValidBST = function(root) {
function dfs(node, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
if (!node) return true;
// 当前节点必须满足边界条件
if (node.val <= min || node.val >= max) return false;
// 递归左右子树,更新边界
return dfs(node.left, min, node.val) && dfs(node.right, node.val, max);
}
return dfs(root);
};
技巧二:自底向上递归(Bottom-Up)
适用场景:需要从子树获取信息返回给父节点(如高度、直径、路径和等)
核心要点:
- 子树向父节点返回信息
- 使用全局变量记录需要的结果
- 返回值的定义要明确(如高度、是否平衡等)
典型例题1:二叉树的最大深度
var maxDepth = function(root) {
if (!root) return 0;
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
};
典型例题2:二叉树的直径
var diameterOfBinaryTree = function(root) {
let maxDiameter = 0;
function getHeight(node) {
if (!node) return 0;
const left = getHeight(node.left);
const right = getHeight(node.right);
// 当前节点的直径 = 左子树高度 + 右子树高度
maxDiameter = Math.max(maxDiameter, left + right);
// 返回当前子树的高度
return Math.max(left, right) + 1;
}
getHeight(root);
return maxDiameter;
};
技巧三:返回多个值的递归
适用场景:需要同时返回多个信息(如是否平衡 + 高度)
核心要点:
- 使用数组或对象返回多个值
- 递归函数同时承担计算和判断的职责
典型例题:平衡二叉树
var isBalanced = function(root) {
return check(root)[0];
};
function check(node) {
if (!node) return [true, 0]; // [是否平衡, 高度]
const [leftBalanced, leftHeight] = check(node.left);
const [rightBalanced, rightHeight] = check(node.right);
const balanced = leftBalanced && rightBalanced
&& Math.abs(leftHeight - rightHeight) <= 1;
const height = Math.max(leftHeight, rightHeight) + 1;
return [balanced, height];
}
技巧四:层序遍历(BFS)
适用场景:按层处理问题(如层序遍历、右视图等)
核心要点:
- 使用队列实现
- 每次处理一层的所有节点
- 通过
levelSize控制每层的遍历次数
模板代码:二叉树的层序遍历
var levelOrder = function(root) {
if (!root) return [];
const queue = [root];
const result = [];
while (queue.length > 0) {
const levelSize = queue.length; // 当前层的节点数
const currentLevel = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
currentLevel.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(currentLevel);
}
return result;
};
典型例题:二叉树的右视图
var rightSideView = function(root) {
if (!root) return [];
const queue = [root];
const result = [];
while (queue.length > 0) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
// 当前层的最后一个节点就是右视图节点
if (i === levelSize - 1) {
result.push(node.val);
}
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return result;
};
技巧五:二叉搜索树(BST)特性利用
BST 核心性质:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 中序遍历结果是有序的
核心要点:
- 利用 BST 的有序性简化问题
- 中序遍历 = 升序遍历
典型例题1:二叉搜索树的最近公共祖先
var lowestCommonAncestor = function(root, p, q) {
// 两个节点都在左子树
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}
// 两个节点都在右子树
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}
// 当前节点就是最近公共祖先
return root;
};
典型例题2:二叉搜索树中第K小的元素
var kthSmallest = function(root, k) {
let count = 0;
let result = null;
function inorder(node) {
if (!node || result !== null) return;
inorder(node.left);
count++;
if (count === k) {
result = node.val;
return;
}
inorder(node.right);
}
inorder(root);
return result;
};
技巧六:构建父节点映射
适用场景:需要向上遍历(访问父节点)的情况
核心要点:
- 先用 DFS 构建父节点映射
- 然后可以像图一样进行多方向遍历
典型例题:二叉树中所有距离为 K 的结点
var distanceK = function(root, target, k) {
const parentMap = new Map();
// 第一步:构建父节点映射
function buildParentMap(node, parent) {
if (!node) return;
parentMap.set(node, parent);
buildParentMap(node.left, node);
buildParentMap(node.right, node);
}
buildParentMap(root, null);
// 第二步:从 target 开始 BFS
const result = [];
const visited = new Set();
function find(node, distance) {
if (!node || visited.has(node)) return;
visited.add(node);
if (distance === k) {
result.push(node.val);
return;
}
find(node.left, distance + 1);
find(node.right, distance + 1);
find(parentMap.get(node), distance + 1); // 向上遍历
}
find(target, 0);
return result;
};
技巧七:分治构建二叉树
适用场景:根据遍历序列重建二叉树
核心要点:
- 前序的第一个元素是根
- 中序中根的位置划分左右子树
- 用哈希表优化查找,避免 O(n) 的 indexOf
典型例题:从前序与中序遍历序列构造二叉树
var buildTree = function(preorder, inorder) {
// 用哈希表优化查找
const inorderMap = new Map();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
function build(preStart, inStart, inEnd) {
if (inStart > inEnd) return null;
const rootVal = preorder[preStart];
const root = new TreeNode(rootVal);
const index = inorderMap.get(rootVal);
const leftSize = index - inStart;
root.left = build(preStart + 1, inStart, index - 1);
root.right = build(preStart + 1 + leftSize, index + 1, inEnd);
return root;
}
return build(0, 0, inorder.length - 1);
};
技巧八:序列化与反序列化
典型例题:二叉树的序列化与反序列化
// 序列化:前序遍历
var serialize = function(root) {
if (!root) return 'null';
return root.val + ',' + serialize(root.left) + ',' + serialize(root.right);
};
// 反序列化
var deserialize = function(data) {
const values = data.split(',');
function build() {
const val = values.shift();
if (val === 'null') return null;
const node = new TreeNode(parseInt(val));
node.left = build();
node.right = build();
return node;
}
return build();
};
四、易错点提醒
- 递归终止条件:不要忘记处理空节点
- 全局变量更新:在自底向上递归中,注意全局变量的更新时机
- 负数处理:路径和问题中,负数可以置为 0 表示不选
const leftMax = Math.max(dfs(node.left), 0); - BST 边界:验证 BST 时注意边界值要用
Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER - 索引计算:分治构建树时,注意索引的计算不要出错