二叉树算法题
二叉树是算法面试中的重要考点,掌握树的遍历和递归思想是解决树问题的关键。
二叉树的基本结构
javascript
function TreeNode(val, left, right) {
this.val = val === undefined ? 0 : val;
this.left = left === undefined ? null : left;
this.right = right === undefined ? null : right;
}
高频面试题
1. 二叉树的遍历方式
问题:如何实现二叉树的前序、中序、后序和层序遍历?
解答:
前序遍历(根-左-右):
javascript
// 递归方式
function preorderTraversal(root) {
const result = [];
function preorder(node) {
if (!node) return;
result.push(node.val); // 访问根节点
preorder(node.left); // 遍历左子树
preorder(node.right); // 遍历右子树
}
preorder(root);
return result;
}
// 迭代方式
function preorderTraversal(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop();
result.push(node.val);
// 先右后左,保证左子树先被访问
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return result;
}
中序遍历(左-根-右):
javascript
// 递归方式
function inorderTraversal(root) {
const result = [];
function inorder(node) {
if (!node) return;
inorder(node.left); // 遍历左子树
result.push(node.val); // 访问根节点
inorder(node.right); // 遍历右子树
}
inorder(root);
return result;
}
// 迭代方式
function inorderTraversal(root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length) {
// 将所有左子节点入栈
while (current) {
stack.push(current);
current = current.left;
}
// 弹出栈顶元素并访问
current = stack.pop();
result.push(current.val);
// 处理右子树
current = current.right;
}
return result;
}
后序遍历(左-右-根):
javascript
// 递归方式
function postorderTraversal(root) {
const result = [];
function postorder(node) {
if (!node) return;
postorder(node.left); // 遍历左子树
postorder(node.right); // 遍历右子树
result.push(node.val); // 访问根节点
}
postorder(root);
return result;
}
// 迭代方式
function postorderTraversal(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop();
// 将结果插入到数组头部
result.unshift(node.val);
// 先左后右,结合unshift操作,实现左-右-根
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return result;
}
层序遍历:
javascript
function levelOrder(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length) {
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;
}
相关面试题:
- "你能说明前序、中序、后序遍历的应用场景吗?"
- "如何仅使用前序和中序遍历结果构建二叉树?"
- "如何不使用递归实现树的遍历?各有什么优缺点?"
2. 二叉树的最大深度
问题:计算二叉树的最大深度。
解答:
javascript
// 递归解法
function maxDepth(root) {
if (!root) return 0;
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
// BFS解法
function maxDepth(root) {
if (!root) return 0;
const queue = [root];
let depth = 0;
while (queue.length) {
const size = queue.length;
depth++;
for (let i = 0; i < size; i++) {
const node = queue.shift();
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return depth;
}
相关面试题:
- "如何判断一棵二叉树是否是平衡二叉树?"
- "深度优先搜索和广度优先搜索在计算树深度时各有什么优缺点?"
3. 对称二叉树
问题:判断一棵二叉树是否是镜像对称的。
解答:
javascript
function isSymmetric(root) {
if (!root) return true;
function isMirror(left, right) {
if (!left && !right) return true;
if (!left || !right) return false;
return (left.val === right.val) &&
isMirror(left.left, right.right) &&
isMirror(left.right, right.left);
}
return isMirror(root.left, root.right);
}
### 4. 二叉搜索树的操作
**问题**:实现二叉搜索树的基本操作(插入、删除、查找)。
```javascript
// 插入操作
function insert(root, val) {
if (!root) return new TreeNode(val);
if (val < root.val) {
root.left = insert(root.left, val);
} else if (val > root.val) {
root.right = insert(root.right, val);
}
return root;
}
// 查找操作
function search(root, val) {
if (!root || root.val === val) return root;
if (val < root.val) {
return search(root.left, val);
} else {
return search(root.right, val);
}
}
// 删除操作
function deleteNode(root, key) {
if (!root) return null;
if (key < root.val) {
root.left = deleteNode(root.left, key);
} else if (key > root.val) {
root.right = deleteNode(root.right, key);
} else {
// 找到要删除的节点
// 情况1:叶子节点
if (!root.left && !root.right) {
return null;
}
// 情况2:只有一个子节点
if (!root.left) return root.right;
if (!root.right) return root.left;
// 情况3:有两个子节点
let minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteNode(root.right, minNode.val);
}
return root;
}
function findMin(node) {
while (node.left) {
node = node.left;
}
return node;
}
5. 平衡二叉树(AVL树)
问题:实现一个平衡二叉树,并实现自平衡操作。
javascript
class AVLNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
this.height = 1;
}
}
class AVLTree {
getHeight(node) {
return node ? node.height : 0;
}
getBalance(node) {
return node ? this.getHeight(node.left) - this.getHeight(node.right) : 0;
}
rightRotate(y) {
const x = y.left;
const T2 = x.right;
x.right = y;
y.left = T2;
y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1;
x.height = Math.max(this.getHeight(x.left), this.getHeight(x.right)) + 1;
return x;
}
leftRotate(x) {
const y = x.right;
const T2 = y.left;
y.left = x;
x.right = T2;
x.height = Math.max(this.getHeight(x.left), this.getHeight(x.right)) + 1;
y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1;
return y;
}
insert(root, val) {
if (!root) return new AVLNode(val);
if (val < root.val) {
root.left = this.insert(root.left, val);
} else if (val > root.val) {
root.right = this.insert(root.right, val);
} else {
return root;
}
root.height = Math.max(this.getHeight(root.left), this.getHeight(root.right)) + 1;
const balance = this.getBalance(root);
// LL情况
if (balance > 1 && val < root.left.val) {
return this.rightRotate(root);
}
// RR情况
if (balance < -1 && val > root.right.val) {
return this.leftRotate(root);
}
// LR情况
if (balance > 1 && val > root.left.val) {
root.left = this.leftRotate(root.left);
return this.rightRotate(root);
}
// RL情况
if (balance < -1 && val < root.right.val) {
root.right = this.rightRotate(root.right);
return this.leftRotate(root);
}
return root;
}
}
6. 红黑树的基本概念
红黑树是一种自平衡的二叉搜索树,具有以下性质:
- 节点是红色或黑色
- 根节点是黑色
- 所有叶子节点都是黑色
- 红色节点的子节点都是黑色
- 从任一节点到其所有后代叶子节点的路径上包含相同数量的黑节点
javascript
class RBNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
this.color = 'RED'; // 新插入的节点默认为红色
this.parent = null;
}
}
实际应用场景
文件系统:
- 目录结构的表示
- 文件系统的组织
语法分析:
- 编译器的语法树
- 表达式求值
数据库:
- B树和B+树索引
- 数据库的查询优化
面试要点
1. 树的基本概念
树的特点:
- 层次结构
- 父子关系
- 路径和深度
不同类型的树:
- 二叉树
- 二叉搜索树
- 平衡二叉树
- 红黑树
2. 常见面试问题
树的遍历方式的选择:
- 什么时候用前序遍历
- 什么时候用中序遍历
- 什么时候用后序遍历
- 什么时候用层序遍历
平衡树的选择:
- AVL树和红黑树的比较
- 不同场景下的选择
- 实现的复杂度
递归和迭代的选择:
- 递归的优缺点
- 迭代的优缺点
- 如何转换
总结
树是一个非常重要的数据结构,面试中需要注意:
基础知识:
- 理解树的基本概念
- 掌握不同类型的树
- 熟练各种遍历方式
实现技巧:
- 递归和迭代的转换
- 平衡树的自平衡
- 高效的数据结构
应用场景:
- 理解实际应用
- 选择合适的数据结构
- 性能优化
代码质量:
- 代码的可读性
- 边界情况的处理
- 错误处理 if (!left && !right) return true; // 其中一个节点为空 if (!left || !right) return false;
// 值相等且子树互为镜像 return ( left.val === right.val && isMirror(left.left, right.right) && isMirror(left.right, right.left) ); }
return isMirror(root.left, root.right); }
**相关面试题**:
1. "如何使用迭代而不是递归解决这个问题?"
2. "如果树的节点数非常多,你会如何优化算法?"
### 4. 路径总和
**问题**:给定一棵二叉树和一个目标和,判断是否存在从根节点到叶子节点的路径,使得路径上所有节点值相加等于目标和。
**解答**:
```javascript
function hasPathSum(root, targetSum) {
if (!root) return false;
// 如果是叶子节点,检查值是否等于目标和
if (!root.left && !root.right) {
return root.val === targetSum;
}
// 递归检查左右子树
return (
hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val)
);
}
相关面试题:
- "如何找到所有满足条件的路径,而不仅仅是判断是否存在?"
- "如果路径不一定从根节点开始,也不一定在叶子节点结束,如何解决?"
5. 二叉树的最近公共祖先
问题:给定一棵二叉树, 找到该树中两个指定节点的最近公共祖先。
解答:
javascript
function lowestCommonAncestor(root, p, q) {
// 基本情况
if (!root || root === p || root === q) {
return root;
}
// 在左右子树中查找p和q
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
// 如果p和q分别在左右子树中,则root就是LCA
if (left && right) {
return root;
}
// 否则,LCA在左子树或右子树中
return left ? left : right;
}
相关面试题:
- "如果树中可能不包含 p 或 q,如何修改算法?"
- "如果是二叉搜索树,如何优化此算法?"
6. 二叉树的序列化与反序列化
问题:设计一个算法,将二叉树序列化为字符串,并能将该字符串反序列化为原二叉树。
解答:
javascript
// 序列化
function serialize(root) {
if (!root) return "null";
// 前序遍历序列化
const left = serialize(root.left);
const right = serialize(root.right);
return `${root.val},${left},${right}`;
}
// 反序列化
function deserialize(data) {
const list = data.split(",");
function buildTree() {
const val = list.shift();
if (val === "null") {
return null;
}
const node = new TreeNode(parseInt(val));
node.left = buildTree();
node.right = buildTree();
return node;
}
return buildTree();
}
相关面试题:
- "除了前序遍历,还可以使用什么遍历方式进行序列化?它们有什么区别?"
- "如何优化序列化的空间效率?"
解题技巧总结
- 递归思想:树的问题通常可以通过递归优雅解决
- 层次遍历:使用队列实现,解决层级相关问题
- 前/中/后序遍历:使用栈实现非递归版本
- 自顶向下 vs 自底向上:根据问题特点选择合适的方向
- 树的构造:通常需要前序+中序或中序+后序
复杂度分析
- 时间复杂度:通常为 O(n),其中 n 是树的节点数
- 空间复杂度:
- 递归:最坏情况下 O(h),h 为树高,不平衡树可能达到 O(n)
- 迭代:通常为 O(w),w 为树的最大宽度
面试常见陷阱
- 忽略空树或只有一个节点的情况
- 没有正确处理树不平衡的情况
- 递归过程中没有合理传递或更新状态