树是一种非常重要的非线性数据结构,在算法和工程中应用极其广泛。本文将从基础概念、遍历算法到常见题型,全面讲解树的相关知识。
一、树的基本概念
1.1 什么是树?
树(Tree)是由 n(n ≥ 0)个节点组成的有限集合。当 n = 0 时,称为空树。
对于任意一棵非空树,满足:
- 有且仅有一个特定的节点,称为根节点(Root)
- 除根节点外,其余节点可以分为 m(m > 0)个互不相交的有限集合 T1, T2, …, Tm,其中每个集合本身又是一棵树,称为根的子树(Subtree)
1.2 树的相关术语
| 术语 | 说明 |
|---|
| 节点(Node) | 树的基本单位,包含一个数据元素及若干指向子树的指针 |
| 根节点(Root) | 树的顶层节点,没有父节点 |
| 父节点(Parent) | 有子节点的节点,子节点的上一级节点 |
| 子节点(Child) | 父节点的下一级节点 |
| 兄弟节点(Sibling) | 具有相同父节点的节点 |
| 叶子节点(Leaf) | 没有子节点的节点,也叫终端节点 |
| 分支节点(Branch) | 有子节点的节点,也叫非终端节点 |
| 路径(Path) | 从一个节点到另一个节点经过的节点序列 |
| 祖先节点(Ancestor) | 从根到该节点路径上的所有节点 |
| 子孙节点(Descendant) | 该节点的子树中的所有节点 |
1.3 节点的度、树的度
- 节点的度(Degree):一个节点的子节点个数
- 树的度:树中所有节点的度的最大值
- N 叉树:每个节点最多有 N 个子节点的树
1.4 深度、高度、层
| 概念 | 定义 | 示例 |
|---|
| 深度(Depth) | 从根节点到该节点的路径长度(根节点深度为 0 或 1) | 根节点深度 = 0,子节点深度 = 1 |
| 高度(Height) | 从该节点到最远叶子节点的路径长度(叶子节点高度为 0) | 叶子节点高度 = 0,根节点高度最大 |
| 层(Level) | 根为第 1 层,根的孩子为第 2 层,以此类推 | 根在第 1 层 |
注意: 深度和高度的定义在不同教材中可能略有不同,有的根节点深度为 1,做题时需注意题目说明。
1.5 节点的数据结构表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class TreeNode { constructor(value) { this.value = value; this.children = []; } }
class BinaryTreeNode { constructor(value) { this.value = value; this.left = null; this.right = null; } }
|
二、二叉树
二叉树是每个节点最多有两个子节点的树,是最常用的树结构。
2.1 二叉树的性质
- 性质 1:二叉树的第 i 层上最多有 2^(i-1) 个节点
- 性质 2:深度为 k 的二叉树最多有 2^k - 1 个节点
- 性质 3:对于任意一棵二叉树,叶子节点数 = 度为 2 的节点数 + 1
2.2 特殊的二叉树
满二叉树(Full Binary Tree)
除最后一层无任何子节点外,每一层上的所有节点都有两个子节点的二叉树。
1 2 3 4 5
| 1 / \ 2 3 / \ / \ 4 5 6 7
|
- 深度为 k 的满二叉树有 2^k - 1 个节点
- 叶子节点都在最后一层
完全二叉树(Complete Binary Tree)
除最后一层外,其他层的节点数都达到最大值,且最后一层的节点都依次排列在最左边。
1 2 3 4 5
| 1 / \ 2 3 / \ / 4 5 6
|
- 满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
- 完全二叉树可以用数组高效存储(堆就是一种完全二叉树)
完全二叉树的数组存储:
1 2 3 4 5 6
| 索引: 0 1 2 3 4 5 值: 1 2 3 4 5 6
节点 i 的左子节点: 2*i + 1 节点 i 的右子节点: 2*i + 2 节点 i 的父节点: Math.floor((i-1)/2)
|
二叉搜索树(Binary Search Tree, BST)
对于树中的每个节点,其左子树中所有节点的值都小于该节点的值,右子树中所有节点的值都大于该节点的值。
1 2 3 4 5
| 5 / \ 3 7 / \ / \ 2 4 6 8
|
- 中序遍历 BST 可以得到一个有序数组
- 查找、插入、删除的平均时间复杂度为 O(logn)
- 最差情况退化为链表,时间复杂度 O(n)
平衡二叉树(AVL 树)
一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
三、树的遍历
树的遍历是指按照某种规则访问树中的所有节点,且每个节点只访问一次。
3.1 深度优先遍历(DFS)
沿着树的深度遍历树的节点,尽可能深地搜索树的分支。
前序遍历(Pre-order)
访问顺序: 根 → 左 → 右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function preorder(root) { if (!root) return; console.log(root.value); preorder(root.left); preorder(root.right); }
function preorderIterative(root) { if (!root) return []; const result = []; const stack = [root];
while (stack.length) { const node = stack.pop(); result.push(node.value); if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } return result; }
|
中序遍历(In-order)
访问顺序: 左 → 根 → 右
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function inorder(root) { if (!root) return; inorder(root.left); console.log(root.value); inorder(root.right); }
function inorderIterative(root) { if (!root) return []; 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.value); current = current.right; } return result; }
|
注意: 二叉搜索树的中序遍历是有序的!
后序遍历(Post-order)
访问顺序: 左 → 右 → 根
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| function postorder(root) { if (!root) return; postorder(root.left); postorder(root.right); console.log(root.value); }
function postorderIterative(root) { if (!root) return []; const result = []; const stack = [root];
while (stack.length) { const node = stack.pop(); result.push(node.value); if (node.left) stack.push(node.left); if (node.right) stack.push(node.right); } return result.reverse(); }
function postorderIterative2(root) { if (!root) return []; const result = []; const stack = [[root, false]];
while (stack.length) { const [node, visited] = stack.pop(); if (visited) { result.push(node.value); } else { stack.push([node, true]); if (node.right) stack.push([node.right, false]); if (node.left) stack.push([node.left, false]); } } return result; }
|
3.2 广度优先遍历(BFS)
也叫层序遍历,按照树的层次从上到下、从左到右访问节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function levelOrder(root) { if (!root) return []; const result = []; const queue = [root];
while (queue.length) { const node = queue.shift(); result.push(node.value); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } return result; }
function levelOrderWithLevel(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.value); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } result.push(currentLevel); } return result; }
|
3.3 遍历应用场景
| 遍历方式 | 应用场景 |
|---|
| 前序遍历 | 复制树、前缀表达式 |
| 中序遍历 | 二叉搜索树排序、中缀表达式 |
| 后序遍历 | 删除树、后缀表达式、计算目录大小 |
| 层序遍历 | 求树的宽度、打印树、找最短路径 |
四、常见题型
树的题目主要分为三类:搜索类、构建类、修改类,其中搜索类最多。
4.1 搜索类
题型一:路径和问题
LeetCode 112:路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
1 2 3 4 5 6 7 8 9 10 11 12
| var hasPathSum = function(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); };
|
LeetCode 113:路径总和 II
给定一个二叉树和一个目标和,找出所有从根节点到叶子节点路径总和等于给定目标和的路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var pathSum = function(root, targetSum) { const result = [];
function backtrack(node, path, sum) { if (!node) return;
path.push(node.val); sum -= node.val;
if (!node.left && !node.right && sum === 0) { result.push([...path]); } else { backtrack(node.left, path, sum); backtrack(node.right, path, sum); }
path.pop(); }
backtrack(root, [], targetSum); return result; };
|
题型二:树的最大深度
LeetCode 104:二叉树的最大深度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| var maxDepth = function(root) { if (!root) return 0; const left = maxDepth(root.left); const right = maxDepth(root.right); return Math.max(left, right) + 1; };
var maxDepthBFS = function(root) { if (!root) return 0; let depth = 0; const queue = [root];
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; };
|
题型三:对称二叉树
LeetCode 101:对称二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var isSymmetric = function(root) { if (!root) return true;
function isMirror(left, right) { if (!left && !right) return true; if (!left || !right) return false; if (left.val !== right.val) return false; return isMirror(left.left, right.right) && isMirror(left.right, right.left); }
return isMirror(root.left, root.right); };
|
题型四:二叉搜索树的验证
LeetCode 98:验证二叉搜索树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| var isValidBST = function(root) { function helper(node, min, max) { if (!node) return true; if (node.val <= min || node.val >= max) return false; return helper(node.left, min, node.val) && helper(node.right, node.val, max); } return helper(root, -Infinity, Infinity); };
var isValidBSTInorder = function(root) { let prev = -Infinity; let isValid = true;
function inorder(node) { if (!node || !isValid) return; inorder(node.left); if (node.val <= prev) { isValid = false; return; } prev = node.val; inorder(node.right); }
inorder(root); return isValid; };
|
4.2 构建类
题型一:从前序与中序遍历序列构造二叉树
LeetCode 105:从前序与中序遍历序列构造二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var buildTree = function(preorder, inorder) { if (!preorder.length || !inorder.length) return null;
const rootVal = preorder[0]; const root = new TreeNode(rootVal);
const rootIndex = inorder.indexOf(rootVal);
root.left = buildTree( preorder.slice(1, rootIndex + 1), inorder.slice(0, rootIndex) );
root.right = buildTree( preorder.slice(rootIndex + 1), inorder.slice(rootIndex + 1) );
return root; };
|
题型二:从中序与后序遍历序列构造二叉树
LeetCode 106:从中序与后序遍历序列构造二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var buildTree = function(inorder, postorder) { if (!inorder.length || !postorder.length) return null;
const rootVal = postorder[postorder.length - 1]; const root = new TreeNode(rootVal);
const rootIndex = inorder.indexOf(rootVal);
root.left = buildTree( inorder.slice(0, rootIndex), postorder.slice(0, rootIndex) );
root.right = buildTree( inorder.slice(rootIndex + 1), postorder.slice(rootIndex, postorder.length - 1) );
return root; };
|
4.3 修改类
题型一:翻转二叉树
LeetCode 226:翻转二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var invertTree = function(root) { if (!root) return null;
const temp = root.left; root.left = root.right; root.right = temp;
invertTree(root.left); invertTree(root.right);
return root; };
|
题型二:二叉树展开为链表
LeetCode 114:二叉树展开为链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| var flatten = function(root) { if (!root) return;
flatten(root.left); flatten(root.right);
const right = root.right;
root.right = root.left; root.left = null;
let p = root; while (p.right) { p = p.right; } p.right = right; };
|
4.4 进阶题型
题型一:最近公共祖先(LCA)
LeetCode 236:二叉树的最近公共祖先
1 2 3 4 5 6 7 8 9 10 11 12
| var lowestCommonAncestor = function(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;
return left || right; };
|
LeetCode 235:二叉搜索树的最近公共祖先
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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; };
|
题型二:二叉树的直径
LeetCode 543:二叉树的直径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var diameterOfBinaryTree = function(root) { let maxDiameter = 0;
function depth(node) { if (!node) return 0; const left = depth(node.left); const right = depth(node.right); maxDiameter = Math.max(maxDiameter, left + right); return Math.max(left, right) + 1; }
depth(root); return maxDiameter; };
|
五、树的常见套路
5.1 递归三部曲
树的题目大多可以用递归解决,记住递归三部曲:
确定递归函数的参数和返回值
确定终止条件
确定单层递归逻辑
5.2 遍历选择
| 情况 | 推荐遍历 |
|---|
| 需要先处理父节点 | 前序遍历 |
| 需要先处理子节点 | 后序遍历 |
| 二叉搜索树相关 | 中序遍历 |
| 按层处理、最短路径 | 层序遍历 |
5.3 时间复杂度分析
| 操作 | 时间复杂度 |
|---|
| 遍历树 | O(n) |
| BST 查找/插入/删除(平均) | O(logn) |
| BST 查找/插入/删除(最坏) | O(n) |
| 平衡树所有操作 | O(logn) |
5.4 空间复杂度分析
| 情况 | 空间复杂度 |
|---|
| 递归遍历 | O(h),h 是树的高度,最坏 O(n) |
| 层序遍历 | O(n),最坏情况是完全二叉树的最后一层 |
六、总结
6.1 核心知识点
- 基础概念:根、叶子、父节点、子节点、深度、高度、度
- 特殊二叉树:满二叉树、完全二叉树、二叉搜索树、平衡树
- 遍历算法:
- DFS:前序、中序、后序(递归 + 迭代)
- BFS:层序遍历
- 常见题型:搜索类、构建类、修改类
6.2 做题建议
- 先掌握递归:树的题目 90% 可以用递归解决
- 多画图解:画图能帮助你理清思路
- 记住模板:遍历模板、回溯模板要熟练
- 分类刷题:按题型分类练习,总结规律
树的题目变化很多,但核心就是遍历!只要掌握了遍历,大多数题目都能迎刃而解!