1. 二叉树基础与JavaScript实现
二叉树是每个节点最多有两个子节点的树结构,在算法题中极为常见。JavaScript中我们通常用对象来表示节点:
javascript复制function TreeNode(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? null : left);
this.right = (right === undefined ? null : right);
}
这个构造函数创建了一个典型的二叉树节点,包含值属性和左右子节点指针。在实际使用时,我们需要注意:
在JavaScript中,未赋值的属性默认是undefined,所以这里用null显式表示空节点,这是与许多其他语言不同的地方。
1.1 二叉树的三种基本遍历
前序遍历(根-左-右)
前序遍历先访问根节点,然后左子树,最后右子树。递归实现直观易懂:
javascript复制function preorderTraversal(root) {
const res = [];
const traverse = (node) => {
if (!node) return;
res.push(node.val); // 先访问根节点
traverse(node.left);
traverse(node.right);
};
traverse(root);
return res;
}
但在实际面试中,面试官往往要求迭代实现以避免递归的栈溢出风险:
javascript复制function preorderTraversal(root) {
if (!root) return [];
const stack = [root], res = [];
while (stack.length) {
const node = stack.pop();
res.push(node.val);
// 注意右子节点先入栈,保证左子节点先处理
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return res;
}
中序遍历(左-根-右)
中序遍历的递归实现只需调整访问顺序:
javascript复制function inorderTraversal(root) {
const res = [];
const traverse = (node) => {
if (!node) return;
traverse(node.left);
res.push(node.val); // 中间访问根节点
traverse(node.right);
};
traverse(root);
return res;
}
迭代实现需要借助指针和栈:
javascript复制function inorderTraversal(root) {
const stack = [], res = [];
let curr = root;
while (curr || stack.length) {
while (curr) {
stack.push(curr);
curr = curr.left; // 先走到最左节点
}
curr = stack.pop();
res.push(curr.val);
curr = curr.right;
}
return res;
}
后序遍历(左-右-根)
后序遍历递归实现:
javascript复制function postorderTraversal(root) {
const res = [];
const traverse = (node) => {
if (!node) return;
traverse(node.left);
traverse(node.right);
res.push(node.val); // 最后访问根节点
};
traverse(root);
return res;
}
迭代实现较为复杂,可以采用反转前序遍历的方式:
javascript复制function postorderTraversal(root) {
if (!root) return [];
const stack = [root], res = [];
while (stack.length) {
const node = stack.pop();
res.push(node.val);
// 与前序遍历相反,先左后右
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return res.reverse(); // 反转得到后序结果
}
2. 二叉树经典题目解析
2.1 二叉树的最大深度(LeetCode 104)
求二叉树的最大深度是基础但重要的问题,有两种主流解法。
递归解法(DFS)
递归解法简洁优雅,体现了分治思想:
javascript复制function maxDepth(root) {
if (!root) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
这个解法的时间复杂度是O(n),因为需要访问每个节点一次。空间复杂度在最坏情况下(树退化为链表)是O(n),平均为O(log n)。
层序遍历解法(BFS)
使用队列实现广度优先搜索:
javascript复制function maxDepth(root) {
if (!root) return 0;
const queue = [root];
let depth = 0;
while (queue.length) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
depth++;
}
return depth;
}
注意这里使用了levelSize来区分不同层级的节点,这是BFS解决层级相关问题的关键技巧。
2.2 对称二叉树(LeetCode 101)
判断二叉树是否对称,实际上是判断左右子树是否镜像对称。
递归解法
递归比较左右子树:
javascript复制function isSymmetric(root) {
if (!root) return true;
const compare = (left, right) => {
if (!left && !right) return true;
if (!left || !right || left.val !== right.val) return false;
return compare(left.left, right.right) &&
compare(left.right, right.left);
};
return compare(root.left, root.right);
}
迭代解法
使用队列实现迭代:
javascript复制function isSymmetric(root) {
if (!root) return true;
const queue = [root.left, root.right];
while (queue.length) {
const left = queue.shift();
const right = queue.shift();
if (!left && !right) continue;
if (!left || !right || left.val !== right.val) return false;
queue.push(left.left, right.right);
queue.push(left.right, right.left);
}
return true;
}
在迭代解法中,我们成对地压入和弹出节点进行比较,模拟了递归的过程。
2.3 路径总和(LeetCode 112)
判断是否存在从根到叶子的路径,使得路径上节点值之和等于给定值。
递归回溯解法
javascript复制function hasPathSum(root, targetSum) {
if (!root) return false;
if (!root.left && !root.right) return targetSum === root.val;
return hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
这个解法通过不断减去当前节点值来更新目标和,直到叶子节点时判断剩余值是否等于叶子节点值。
迭代解法
使用栈实现DFS:
javascript复制function hasPathSum(root, targetSum) {
if (!root) return false;
const stack = [[root, targetSum - root.val]];
while (stack.length) {
const [node, currSum] = stack.pop();
if (!node.left && !node.right && currSum === 0) return true;
if (node.right) stack.push([node.right, currSum - node.right.val]);
if (node.left) stack.push([node.left, currSum - node.left.val]);
}
return false;
}
3. 二叉树进阶题目
3.1 二叉树的最近公共祖先(LeetCode 236)
寻找二叉树中两个节点的最近公共祖先(LCA)。
递归解法
javascript复制function lowestCommonAncestor(root, p, q) {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root; // p和q分布在两侧
return left || right; // 返回非空的一侧
}
这个解法利用了后序遍历的特性,时间复杂度O(n),空间复杂度O(h),h为树高。
迭代解法
使用父指针和回溯法:
javascript复制function lowestCommonAncestor(root, p, q) {
const stack = [root];
const parent = new Map();
parent.set(root, null);
// 构建父指针映射
while (!parent.has(p) || !parent.has(q)) {
const node = stack.pop();
if (node.left) {
parent.set(node.left, node);
stack.push(node.left);
}
if (node.right) {
parent.set(node.right, node);
stack.push(node.right);
}
}
// 收集p的所有祖先
const ancestors = new Set();
while (p) {
ancestors.add(p);
p = parent.get(p);
}
// 查找q的祖先中第一个也是p的祖先的节点
while (!ancestors.has(q)) {
q = parent.get(q);
}
return q;
}
3.2 二叉树的序列化与反序列化(LeetCode 297)
将二叉树转换为字符串,并能从字符串重建二叉树。
BFS实现
javascript复制function serialize(root) {
if (!root) return '[]';
const queue = [root], res = [];
while (queue.length) {
const node = queue.shift();
if (node) {
res.push(node.val);
queue.push(node.left, node.right);
} else {
res.push(null);
}
}
// 去除末尾多余的null
while (res[res.length - 1] === null) res.pop();
return JSON.stringify(res);
}
function deserialize(data) {
const arr = JSON.parse(data);
if (!arr.length) return null;
const root = new TreeNode(arr[0]);
const queue = [root];
let i = 1;
while (queue.length && i < arr.length) {
const node = queue.shift();
if (arr[i] !== null) {
node.left = new TreeNode(arr[i]);
queue.push(node.left);
}
i++;
if (i < arr.length && arr[i] !== null) {
node.right = new TreeNode(arr[i]);
queue.push(node.right);
}
i++;
}
return root;
}
序列化时使用BFS层级遍历,null表示空节点。反序列化时同样使用队列重建树结构。
4. 二叉树解题技巧与注意事项
4.1 递归与迭代的选择
- 递归:代码简洁,但存在栈溢出风险(JavaScript引擎通常有调用栈限制)
- 迭代:更安全,适合大型树,但代码复杂度较高
在实际面试中,建议先给出递归解法,然后应要求给出迭代解法。
4.2 JavaScript特定注意事项
- 空值处理:JavaScript中null和undefined的区别很重要,二叉树中通常用null表示空节点
- 引用比较:判断节点是否相同时,JavaScript比较的是引用而非值
- 队列性能:数组的shift()操作是O(n)复杂度,对于大型树可以考虑使用链表实现队列
4.3 常见错误与调试技巧
- 无限递归:忘记写递归终止条件或条件错误
- 指针丢失:在迭代法中错误地修改了节点指针
- 顺序错误:在迭代法中入栈/入队顺序错误
调试时可以:
- 在关键位置打印节点值
- 使用小规模的树进行测试
- 逐步跟踪代码执行流程
4.4 时间复杂度分析
大多数二叉树问题的时间复杂度都是O(n),因为需要访问每个节点一次。空间复杂度取决于解法:
- 递归:O(h),h为树高
- 迭代:通常O(n),取决于使用的数据结构
对于平衡二叉树,h=log n,空间复杂度可以优化为O(log n)。
5. 二叉树问题扩展练习
除了热题100中的题目,以下题目也值得练习:
- 验证二叉搜索树(LeetCode 98):利用中序遍历特性
- 二叉树的右视图(LeetCode 199):BFS的变种
- 二叉树展开为链表(LeetCode 114):前序遍历的应用
- 二叉树的直径(LeetCode 543):深度计算的变种
- 二叉树的层平均值(LeetCode 637):BFS的应用
在解决这些问题时,建议先自己思考解法,再对比优秀题解,理解不同解法的优缺点。