文章目录
- 二叉树理论基础
-
- [1. 二叉树的基本概念](#1. 二叉树的基本概念)
-
- [1.1 基本术语](#1.1 基本术语)
- [1.2 二叉树的特点](#1.2 二叉树的特点)
- [1.3 二叉树节点的定义](#1.3 二叉树节点的定义)
- [2. 二叉树的分类](#2. 二叉树的分类)
-
- [2.1 满二叉树(Full Binary Tree)](#2.1 满二叉树(Full Binary Tree))
- [2.2 完全二叉树(Complete Binary Tree)](#2.2 完全二叉树(Complete Binary Tree))
- [2.3 二叉搜索树(Binary Search Tree, BST)](#2.3 二叉搜索树(Binary Search Tree, BST))
- [2.4 平衡二叉树(Balanced Binary Tree)](#2.4 平衡二叉树(Balanced Binary Tree))
- [3. 二叉树的遍历](#3. 二叉树的遍历)
-
- [3.1 遍历方式分类](#3.1 遍历方式分类)
- [3.2 递归遍历模板](#3.2 递归遍历模板)
- [3.3 迭代遍历模板](#3.3 迭代遍历模板)
- [3.4 统一迭代法模板](#3.4 统一迭代法模板)
- [3.5 层序遍历模板](#3.5 层序遍历模板)
- [4. 二叉树的基本操作模板](#4. 二叉树的基本操作模板)
-
- [4.1 翻转二叉树](#4.1 翻转二叉树)
- [4.2 对称二叉树](#4.2 对称二叉树)
- [4.3 二叉树的最大深度](#4.3 二叉树的最大深度)
- [4.4 二叉树的最小深度](#4.4 二叉树的最小深度)
- [4.5 完全二叉树的节点个数](#4.5 完全二叉树的节点个数)
- [4.6 平衡二叉树](#4.6 平衡二叉树)
- [4.7 二叉树的所有路径](#4.7 二叉树的所有路径)
- [4.8 路径总和](#4.8 路径总和)
- [4.9 路径总和II](#4.9 路径总和II)
- [4.10 二叉树的最近公共祖先](#4.10 二叉树的最近公共祖先)
- [4.11 合并二叉树](#4.11 合并二叉树)
- [4.12 其他常见操作](#4.12 其他常见操作)
- [5. 二叉搜索树(BST)操作模板](#5. 二叉搜索树(BST)操作模板)
-
- [5.1 验证二叉搜索树](#5.1 验证二叉搜索树)
- [5.2 二叉搜索树中的搜索](#5.2 二叉搜索树中的搜索)
- [5.3 二叉搜索树中的插入操作](#5.3 二叉搜索树中的插入操作)
- [5.4 删除二叉搜索树中的节点](#5.4 删除二叉搜索树中的节点)
- [5.5 修剪二叉搜索树](#5.5 修剪二叉搜索树)
- [5.6 将有序数组转换为二叉搜索树](#5.6 将有序数组转换为二叉搜索树)
- [5.7 把二叉搜索树转换为累加树](#5.7 把二叉搜索树转换为累加树)
- [5.8 二叉搜索树中的众数](#5.8 二叉搜索树中的众数)
- [5.9 二叉搜索树的最小绝对差](#5.9 二叉搜索树的最小绝对差)
- [5.10 二叉搜索树的最近公共祖先](#5.10 二叉搜索树的最近公共祖先)
- [5.11 二叉搜索树中第k小的元素](#5.11 二叉搜索树中第k小的元素)
- [6. 二叉树的构造](#6. 二叉树的构造)
-
- [6.1 从前序与中序遍历序列构造二叉树](#6.1 从前序与中序遍历序列构造二叉树)
- [6.2 从中序与后序遍历序列构造二叉树](#6.2 从中序与后序遍历序列构造二叉树)
- [6.3 最大二叉树](#6.3 最大二叉树)
- [7. 二叉树操作的时间复杂度](#7. 二叉树操作的时间复杂度)
- [8. 何时使用二叉树技巧](#8. 何时使用二叉树技巧)
-
- [8.1 使用场景](#8.1 使用场景)
- [8.2 判断标准](#8.2 判断标准)
- [9. 二叉树的优缺点](#9. 二叉树的优缺点)
-
- [9.1 优点](#9.1 优点)
- [9.2 缺点](#9.2 缺点)
- [10. 常见题型总结](#10. 常见题型总结)
-
- [10.1 遍历类](#10.1 遍历类)
- [10.2 属性判断类](#10.2 属性判断类)
- [10.3 路径问题类](#10.3 路径问题类)
- [10.4 BST操作类](#10.4 BST操作类)
- [10.5 构造类](#10.5 构造类)
- [11. 总结](#11. 总结)
二叉树理论基础
1. 二叉树的基本概念
**二叉树(Binary Tree)**是一种树形数据结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。
1.1 基本术语
- 节点(Node):树中的基本单位,包含数据和指针
- 根节点(Root):树的顶层节点,没有父节点
- 叶子节点(Leaf):没有子节点的节点
- 父节点(Parent):有子节点的节点
- 子节点(Child):节点的直接下级节点
- 深度(Depth):从根节点到该节点的最长简单路径边的条数
- 高度(Height):从该节点到叶子节点的最长简单路径边的条数
- 层(Level):根节点为第1层,其子节点为第2层,以此类推
1.2 二叉树的特点
- 每个节点最多有两个子节点:左子节点和右子节点
- 子树有序:左子树和右子树是有顺序的
- 递归结构:每个子树也是二叉树
示例:
1
/ \
2 3
/ \ / \
4 5 6 7
节点1:根节点
节点2、3:内部节点
节点4、5、6、7:叶子节点
1.3 二叉树节点的定义
C++中的二叉树节点定义:
cpp
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
2. 二叉树的分类
2.1 满二叉树(Full Binary Tree)
特点:
- 每个节点要么是叶子节点,要么有两个子节点
- 所有叶子节点都在同一层
示例:
1
/ \
2 3
/ \ / \
4 5 6 7
2.2 完全二叉树(Complete Binary Tree)
特点:
- 除了最后一层,其他层都是满的
- 最后一层的节点从左到右连续排列
示例:
1
/ \
2 3
/ \
4 5
2.3 二叉搜索树(Binary Search Tree, BST)
特点:
- 左子树所有节点的值都小于根节点的值
- 右子树所有节点的值都大于根节点的值
- 左右子树也都是二叉搜索树
性质:
- 中序遍历是有序的:这是BST最重要的性质
- 可以快速查找、插入、删除
示例:
5
/ \
3 7
/ \ / \
2 4 6 8
中序遍历:2, 3, 4, 5, 6, 7, 8(有序)
2.4 平衡二叉树(Balanced Binary Tree)
特点:
- 任意节点的左右子树高度差的绝对值不超过1
- 常见类型:AVL树、红黑树
3. 二叉树的遍历
3.1 遍历方式分类
深度优先遍历(DFS):
- 前序遍历(Preorder):中 → 左 → 右
- 中序遍历(Inorder):左 → 中 → 右
- 后序遍历(Postorder):左 → 右 → 中
广度优先遍历(BFS):
- 层序遍历(Level Order):逐层从左到右遍历
记忆方法:
- 前序、中序、后序指的是中间节点在遍历结果中的位置
- 前序:中间节点在最前面
- 中序:中间节点在中间
- 后序:中间节点在最后面
3.2 递归遍历模板
模板1:前序遍历(递归)
核心思路:
- 先处理中间节点
- 再递归处理左子树
- 最后递归处理右子树
模板代码:
cpp
// LeetCode 144. 二叉树的前序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur == nullptr) return; // 终止条件
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
关键点:
- 终止条件:节点为空时返回
- 处理顺序:中 → 左 → 右
- 时间复杂度:O(n),空间复杂度:O(h),h为树的高度
模板2:中序遍历(递归)
模板代码:
cpp
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur == nullptr) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
关键点:
- 处理顺序:左 → 中 → 右
- BST中序遍历是有序的:这是验证BST的关键
模板3:后序遍历(递归)
模板代码:
cpp
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur == nullptr) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
关键点:
- 处理顺序:左 → 右 → 中
- 适合自底向上处理:如计算高度、找最近公共祖先
3.3 迭代遍历模板
模板1:前序遍历(迭代)
核心思路:
- 使用栈模拟递归
- 先右后左入栈,出栈时先左后右
模板代码:
cpp
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if(root == nullptr) return result;
st.push(root);
while(!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
result.push_back(node->val);
if(node->right) st.push(node->right); // 右(空节点不入栈)
if(node->left) st.push(node->left); // 左(空节点不入栈)
}
// 先右后左入栈,出栈时先左后右
return result;
}
};
关键点:
- 先右后左入栈:保证出栈时先左后右
- 空节点不入栈:减少不必要的操作
- 时间复杂度:O(n),空间复杂度:O(h)
模板2:后序遍历(迭代)
核心思路:
- 前序遍历顺序改为:中 → 右 → 左
- 将结果反转得到:左 → 右 → 中
模板代码:
cpp
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if(root == nullptr) return result;
st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if(node->left) st.push(node->left); // 左
if(node->right) st.push(node->right); // 右
}
reverse(result.begin(), result.end()); // 反转后得到左右中
return result;
}
};
关键点:
- 前序遍历的变种:中 → 右 → 左
- 结果反转:得到后序遍历结果
3.4 统一迭代法模板
核心思路:
- 使用空指针标记要处理的节点
- 统一前序、中序、后序遍历的写法
模板代码:
cpp
// 中序遍历(统一迭代法)
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root != nullptr) st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
if(node != nullptr) {
st.pop(); // 弹出,避免重复操作
if(node->right) st.push(node->right); // 右
st.push(node); // 中
st.push(nullptr); // 标记
if(node->left) st.push(node->left); // 左
} else {
st.pop(); // 弹出空节点
node = st.top();
st.pop();
result.push_back(node->val); // 处理节点
}
}
return result;
}
};
// 前序遍历(统一迭代法):调整入栈顺序
// 右 → 左 → 中 → nullptr
// 后序遍历(统一迭代法):调整入栈顺序
// 中 → nullptr → 右 → 左
关键点:
- 空指针标记:标记要处理的节点
- 统一写法:三种遍历只需调整入栈顺序
- 入栈顺序:
- 前序:右 → 左 → 中 → nullptr
- 中序:右 → 中 → nullptr → 左
- 后序:中 → nullptr → 右 → 左
3.5 层序遍历模板
核心思路:
- 使用队列实现
- 每次处理一层,记录每层的大小
模板代码:
cpp
// LeetCode 102. 二叉树的层序遍历
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> result;
if(root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size(); // 记录当前层的大小
vector<int> vec;
// 处理当前层的所有节点
for(int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
关键点:
- 使用队列:先进先出
- 固定size:必须使用固定大小,因为que.size()会变化
- 时间复杂度:O(n),空间复杂度:O(n)
层序遍历的递归实现:
cpp
class Solution {
public:
void Order(TreeNode* cur, vector<vector<int>>& result, int depth) {
if(cur == nullptr) return;
if(result.size() == depth) {
result.push_back(vector<int>()); // 添加新的一层
}
result[depth].push_back(cur->val);
Order(cur->left, result, depth + 1);
Order(cur->right, result, depth + 1);
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
int depth = 0;
Order(root, result, depth);
return result;
}
};
4. 二叉树的基本操作模板
4.1 翻转二叉树
适用场景:将二叉树的左右子树互换
核心思路:
- 前序遍历:先交换左右子树,再递归处理
- 后序遍历:先递归处理,再交换左右子树
模板代码:
cpp
// LeetCode 226. 翻转二叉树
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return root;
swap(root->left, root->right); // 中:交换左右子树
invertTree(root->left); // 左
invertTree(root->right); // 右
return root;
}
};
关键点:
- 前序遍历:先交换再递归
- 时间复杂度:O(n),空间复杂度:O(h)
4.2 对称二叉树
适用场景:判断二叉树是否对称
核心思路:
- 比较根节点的左右子树是否对称
- 递归比较:左子树的左节点 vs 右子树的右节点,左子树的右节点 vs 右子树的左节点
模板代码:
cpp
// LeetCode 101. 对称二叉树
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root == nullptr) return true;
return compare(root->left, root->right);
}
private:
bool compare(TreeNode* left, TreeNode* right) {
// 处理空节点
if(left == nullptr && right == nullptr) return true;
if(left == nullptr || right == nullptr) return false;
if(left->val != right->val) return false;
// 递归比较
bool outside = compare(left->left, right->right); // 外侧
bool inside = compare(left->right, right->left); // 内侧
return outside && inside;
}
};
关键点:
- 比较两个子树:不是比较单个节点
- 外侧和内侧:左左 vs 右右,左右 vs 右左
- 时间复杂度:O(n),空间复杂度:O(h)
4.3 二叉树的最大深度
适用场景:计算二叉树的最大深度
核心思路:
- 后序遍历:自底向上计算
- 返回左右子树的最大深度 + 1
模板代码:
cpp
// LeetCode 104. 二叉树的最大深度
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
int leftDepth = maxDepth(root->left); // 左
int rightDepth = maxDepth(root->right); // 右
return 1 + max(leftDepth, rightDepth); // 中
}
};
关键点:
- 后序遍历:先处理左右子树,再处理根节点
- 返回条件:空节点返回0
- 时间复杂度:O(n),空间复杂度:O(h)
层序遍历实现:
cpp
class Solution {
public:
int maxDepth(TreeNode* root) {
int depth = 0;
queue<TreeNode*> que;
if(root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size();
depth++; // 每处理一层,深度+1
for(int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
}
return depth;
}
};
4.4 二叉树的最小深度
适用场景:计算二叉树的最小深度
核心思路:
- 注意:最小深度是到叶子节点的最短路径
- 如果左子树或右子树为空,不能直接返回0
模板代码:
cpp
// LeetCode 111. 二叉树的最小深度
class Solution {
public:
int minDepth(TreeNode* root) {
if(root == nullptr) return 0;
int leftDepth = minDepth(root->left);
int rightDepth = minDepth(root->right);
// 如果左子树或右子树为空,返回非空子树的深度
if(root->left == nullptr && root->right != nullptr) {
return 1 + rightDepth;
}
if(root->right == nullptr && root->left != nullptr) {
return 1 + leftDepth;
}
// 左右子树都不为空,返回较小值
return 1 + min(leftDepth, rightDepth);
}
};
关键点:
- 叶子节点:必须是没有子节点的节点
- 特殊情况:单侧子树为空时,不能返回0
- 时间复杂度:O(n),空间复杂度:O(h)
4.5 完全二叉树的节点个数
适用场景:计算完全二叉树的节点个数
核心思路:
- 方法1:普通遍历(适用于所有二叉树)
- 方法2:利用完全二叉树性质(优化)
方法1:普通遍历
模板代码:
cpp
// LeetCode 222. 完全二叉树的节点个数(普通遍历)
class Solution {
public:
int countNodes(TreeNode* root) {
if(root == nullptr) return 0;
return 1 + countNodes(root->left) + countNodes(root->right);
}
};
方法2:利用完全二叉树性质
核心思路:
- 如果左右子树深度相同,说明左子树是满二叉树
- 如果左右子树深度不同,说明右子树是满二叉树
模板代码:
cpp
// LeetCode 222. 完全二叉树的节点个数(优化)
class Solution {
public:
int countNodes(TreeNode* root) {
if(root == nullptr) return 0;
TreeNode* left = root->left;
TreeNode* right = root->right;
int leftDepth = 0, rightDepth = 0;
// 计算左子树深度
while(left) {
left = left->left;
leftDepth++;
}
// 计算右子树深度
while(right) {
right = right->right;
rightDepth++;
}
// 如果深度相同,说明是满二叉树
if(leftDepth == rightDepth) {
return (2 << leftDepth) - 1; // 2^(leftDepth+1) - 1
}
// 否则递归计算
return countNodes(root->left) + countNodes(root->right) + 1;
}
};
关键点:
- 完全二叉树性质:可以利用深度判断是否为满二叉树
- 时间复杂度:O(log²n),空间复杂度:O(log n)
4.6 平衡二叉树
适用场景:判断二叉树是否为平衡二叉树
核心思路:
- 后序遍历:自底向上计算高度
- 如果左右子树高度差大于1,返回-1表示不平衡
模板代码:
cpp
// LeetCode 110. 平衡二叉树
class Solution {
public:
bool isBalanced(TreeNode* root) {
return getHeight(root) != -1;
}
private:
// 返回以该节点为根节点的二叉树的高度
// 如果不是平衡二叉树,返回-1
int getHeight(TreeNode* node) {
if(node == nullptr) return 0;
int leftHeight = getHeight(node->left); // 左
if(leftHeight == -1) return -1; // 提前剪枝
int rightHeight = getHeight(node->right); // 右
if(rightHeight == -1) return -1; // 提前剪枝
// 中:判断是否平衡
if(abs(leftHeight - rightHeight) > 1) {
return -1;
}
return 1 + max(leftHeight, rightHeight);
}
};
关键点:
- 后序遍历:计算高度必须自底向上
- 提前剪枝:如果子树不平衡,直接返回-1
- 时间复杂度:O(n),空间复杂度:O(h)
4.7 二叉树的所有路径
适用场景:找到从根节点到所有叶子节点的路径
核心思路:
- 前序遍历:从根节点向叶子节点扩展
- 回溯:递归和回溯要一一对应
模板代码:
cpp
// LeetCode 257. 二叉树的所有路径
class Solution {
private:
void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
path.push_back(cur->val); // 中
// 叶子节点:找到一条路径
if(cur->left == nullptr && cur->right == nullptr) {
string sPath;
for(int i = 0; i < path.size() - 1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
sPath += to_string(path[path.size() - 1]);
result.push_back(sPath);
return;
}
// 左
if(cur->left) {
traversal(cur->left, path, result);
path.pop_back(); // 回溯
}
// 右
if(cur->right) {
traversal(cur->right, path, result);
path.pop_back(); // 回溯
}
}
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
vector<int> path;
if(root == nullptr) return result;
traversal(root, path, result);
return result;
}
};
关键点:
- 前序遍历:从根到叶子
- 回溯:递归和回溯要一一对应
- 时间复杂度:O(n),空间复杂度:O(h)
4.8 路径总和
适用场景:判断是否存在从根节点到叶子节点的路径,使得路径和等于目标值
核心思路:
- 前序遍历:从根节点向叶子节点累加
- 到达叶子节点时判断路径和
模板代码:
cpp
// LeetCode 112. 路径总和
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if(root == nullptr) return false;
// 叶子节点:判断路径和
if(root->left == nullptr && root->right == nullptr) {
return root->val == targetSum;
}
// 递归判断左右子树
return hasPathSum(root->left, targetSum - root->val) ||
hasPathSum(root->right, targetSum - root->val);
}
};
关键点:
- 目标值递减:targetSum - root->val
- 叶子节点判断:必须是叶子节点
- 时间复杂度:O(n),空间复杂度:O(h)
4.9 路径总和II
适用场景:找到所有从根节点到叶子节点的路径,使得路径和等于目标值
核心思路:
- 前序遍历 + 回溯
- 记录路径,到达叶子节点时判断
模板代码:
cpp
// LeetCode 113. 路径总和II
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void traversal(TreeNode* cur, int count) {
// 叶子节点且路径和等于目标值
if(!cur->left && !cur->right && count == 0) {
result.push_back(path);
return;
}
// 叶子节点但路径和不等于目标值
if(!cur->left && !cur->right) return;
// 左
if(cur->left) {
path.push_back(cur->left->val);
count -= cur->left->val;
traversal(cur->left, count);
count += cur->left->val; // 回溯
path.pop_back(); // 回溯
}
// 右
if(cur->right) {
path.push_back(cur->right->val);
count -= cur->right->val;
traversal(cur->right, count);
count += cur->right->val; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
result.clear();
path.clear();
if(root == nullptr) return result;
path.push_back(root->val);
traversal(root, targetSum - root->val);
return result;
}
};
关键点:
- 回溯:递归和回溯要一一对应
- 路径记录:使用path记录当前路径
- 时间复杂度:O(n),空间复杂度:O(h)
4.10 二叉树的最近公共祖先
适用场景:找到两个节点的最近公共祖先(LCA)
核心思路:
- 后序遍历:自底向上回溯
- 如果左右子树都找到了节点,当前节点就是LCA
模板代码:
cpp
// LeetCode 236. 二叉树的最近公共祖先
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 如果找到p或q,或者到达空节点,返回
if(root == p || root == q || root == nullptr) {
return root;
}
// 递归左右子树
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 如果左右子树都找到了,说明当前节点是LCA
if(left != nullptr && right != nullptr) {
return root;
}
// 如果只有一边找到了,返回找到的那一边
if(left != nullptr) return left;
if(right != nullptr) return right;
return nullptr;
}
};
关键点:
- 后序遍历:自底向上回溯
- 返回值:找到节点返回节点,没找到返回nullptr
- 时间复杂度:O(n),空间复杂度:O(h)
4.11 合并二叉树
适用场景:合并两个二叉树,对应节点值相加
核心思路:
- 同时遍历两个二叉树
- 如果节点都存在,值相加;如果只有一个存在,直接使用
模板代码:
cpp
// LeetCode 617. 合并二叉树
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
// 如果两棵树都为空,返回nullptr
if(root1 == nullptr && root2 == nullptr) {
return nullptr;
}
// 如果其中一棵树为空,返回另一棵树
if(root1 == nullptr) return root2;
if(root2 == nullptr) return root1;
// 两棵树都非空,合并当前节点值
root1->val += root2->val;
// 递归合并左右子树
root1->left = mergeTrees(root1->left, root2->left);
root1->right = mergeTrees(root1->right, root2->right);
return root1;
}
};
关键点:
- 同时遍历:两个树同时递归
- 空节点处理:如果一棵树为空,直接使用另一棵树
- 时间复杂度:O(min(m, n)),空间复杂度:O(min(m, n))
4.12 其他常见操作
左叶子之和
适用场景:计算所有左叶子节点的和
核心思路:
- 通过父节点判断左叶子节点
- 左叶子节点:是父节点的左子节点,且是叶子节点
模板代码:
cpp
// LeetCode 404. 左叶子之和
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if(root == nullptr) return 0;
if(root->left == nullptr && root->right == nullptr) return 0;
int leftValue = sumOfLeftLeaves(root->left);
// 判断左叶子节点
if(root->left && !root->left->left && !root->left->right) {
leftValue = root->left->val;
}
int rightValue = sumOfLeftLeaves(root->right);
return leftValue + rightValue;
}
};
找树左下角的值
适用场景:找到二叉树最底层最左边的值
核心思路:
- 层序遍历:记录每层第一个节点的值
- 最后一层的第一个节点就是答案
模板代码:
cpp
// LeetCode 513. 找树左下角的值
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
int result = 0;
queue<TreeNode*> que;
if(root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size();
for(int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if(i == 0) { // 每层第一个节点
result = node->val;
}
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
}
return result;
}
};
二叉树的直径
适用场景:找到二叉树中任意两个节点之间最长路径的长度
核心思路:
- 后序遍历:计算每个节点的左右子树高度
- 直径 = 左子树高度 + 右子树高度
- 在遍历过程中更新最大直径
模板代码:
cpp
// LeetCode 543. 二叉树的直径
class Solution {
public:
int maxd = 0; // 最大直径
int depth(TreeNode* node) {
if(node == nullptr) return 0;
int left = depth(node->left); // 左子树高度
int right = depth(node->right); // 右子树高度
// 更新最大直径:经过当前节点的直径 = left + right
maxd = max(left + right, maxd);
// 返回当前节点的高度
return max(left, right) + 1;
}
int diameterOfBinaryTree(TreeNode* root) {
depth(root);
return maxd;
}
};
关键点:
- 后序遍历:自底向上计算高度
- 直径计算:经过节点的直径 = 左子树高度 + 右子树高度
- 返回值:返回节点高度,用于父节点计算
- 时间复杂度:O(n),空间复杂度:O(h)
二叉树的右视图
适用场景:返回从右侧看到的二叉树节点值
核心思路:
- 层序遍历:记录每层最后一个节点的值
- 或使用DFS,优先访问右子树
模板代码:
cpp
// LeetCode 199. 二叉树的右视图
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> result;
queue<TreeNode*> que;
if(root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size();
vector<int> vec;
for(int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
result.push_back(vec.back()); // 每层最后一个节点
}
return result;
}
};
关键点:
- 层序遍历:每层最后一个节点
- 时间复杂度:O(n),空间复杂度:O(n)
二叉树展开为链表
适用场景:将二叉树展开为单链表(前序遍历顺序)
核心思路:
- 前序遍历:使用栈模拟
- 维护prev指针,将当前节点接到前一个节点的right
- 将left置为nullptr
模板代码:
cpp
// LeetCode 114. 二叉树展开为链表
class Solution {
public:
void flatten(TreeNode* root) {
if(root == nullptr) return;
stack<TreeNode*> st;
st.push(root);
TreeNode* prev = nullptr;
while(!st.empty()) {
TreeNode* cur = st.top();
st.pop();
// 将当前节点接到前一个节点
if(prev != nullptr) {
prev->right = cur;
prev->left = nullptr;
}
// 先右后左入栈(前序遍历)
if(cur->right) st.push(cur->right);
if(cur->left) st.push(cur->left);
prev = cur;
}
}
};
关键点:
- 前序遍历:中 → 左 → 右
- 维护prev指针:用于连接节点
- left置空:链表要求
- 时间复杂度:O(n),空间复杂度:O(h)
路径总和III
适用场景:找到路径和等于目标值的路径数量(路径不需要从根节点开始,也不需要在叶子节点结束)
核心思路:
- 双重递归:外层递归遍历每个节点作为起点,内层递归从该节点开始寻找路径
- 使用前缀和优化(可选)
模板代码:
cpp
// LeetCode 437. 路径总和III
class Solution {
private:
int result = 0;
// 从当前节点开始,向下寻找路径
void traversal(TreeNode* root, long long targetSum) {
if(root == nullptr) return;
// 如果当前路径和正好等于targetSum
if(root->val == targetSum) {
result++;
}
// 继续向左、向右延伸路径
traversal(root->left, targetSum - root->val);
traversal(root->right, targetSum - root->val);
}
public:
int pathSum(TreeNode* root, int targetSum) {
if(root == nullptr) return 0;
// 以root作为路径起点
traversal(root, targetSum);
// 以左子树的节点作为路径起点
pathSum(root->left, targetSum);
// 以右子树的节点作为路径起点
pathSum(root->right, targetSum);
return result;
}
};
关键点:
- 双重递归:外层遍历所有节点,内层从该节点开始寻找
- 路径起点:可以是任意节点
- 路径终点:可以是任意节点
- 时间复杂度:O(n²),空间复杂度:O(h)
二叉树中的最大路径和
适用场景:找到二叉树中任意路径的最大路径和(路径可以从任意节点开始和结束)
核心思路:
- 后序遍历:自底向上计算
- 对于每个节点,计算经过该节点的最大路径和
- 返回给父节点:只能选择左或右一边
模板代码:
cpp
// LeetCode 124. 二叉树中的最大路径和
class Solution {
private:
int maxSum = INT_MIN;
public:
// 返回以node为起点的最大路径和(只能选择一边)
int maxGain(TreeNode* node) {
if(node == nullptr) return 0;
// 左子树贡献(如果为负,则不选择)
int leftGain = max(maxGain(node->left), 0);
// 右子树贡献(如果为负,则不选择)
int rightGain = max(maxGain(node->right), 0);
// 尝试以node为最高点更新答案(可以选择两边)
maxSum = max(maxSum, node->val + leftGain + rightGain);
// 返回给父节点(只能选一边)
return node->val + max(leftGain, rightGain);
}
int maxPathSum(TreeNode* root) {
maxGain(root);
return maxSum;
}
};
关键点:
- 后序遍历:自底向上计算
- 负贡献处理:如果子树贡献为负,则不选择(取0)
- 返回值:只能选择一边,用于父节点计算
- 更新答案:可以选择两边,更新全局最大值
- 时间复杂度:O(n),空间复杂度:O(h)
5. 二叉搜索树(BST)操作模板
5.1 验证二叉搜索树
适用场景:判断二叉树是否为有效的二叉搜索树
核心思路:
- 方法1:中序遍历,判断结果是否有序
- 方法2:递归判断,每个节点都在合理范围内
方法1:中序遍历
模板代码:
cpp
// LeetCode 98. 验证二叉搜索树(中序遍历)
class Solution {
public:
bool isValidBST(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if(root != nullptr) st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
if(node != nullptr) {
st.pop();
if(node->right) st.push(node->right);
st.push(node);
st.push(nullptr);
if(node->left) st.push(node->left);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
// 判断是否有序
for(int i = 1; i < result.size(); i++) {
if(result[i] <= result[i - 1]) { // 注意:BST不能有重复元素
return false;
}
}
return true;
}
};
关键点:
- BST中序遍历是有序的
- 不能有重复元素:使用
<=判断
方法2:递归判断
模板代码:
cpp
// LeetCode 98. 验证二叉搜索树(递归)
class Solution {
public:
bool isValidBST(TreeNode* root) {
return isValidBST(root, LONG_MIN, LONG_MAX);
}
private:
bool isValidBST(TreeNode* root, long min, long max) {
if(root == nullptr) return true;
if(root->val <= min || root->val >= max) {
return false;
}
return isValidBST(root->left, min, root->val) &&
isValidBST(root->right, root->val, max);
}
};
关键点:
- 每个节点都在合理范围内
- 左子树:最大值是当前节点值
- 右子树:最小值是当前节点值
5.2 二叉搜索树中的搜索
适用场景:在BST中查找值为val的节点
核心思路:
- 利用BST的性质:根据节点值决定搜索方向
- 如果val < root->val,搜索左子树
- 如果val > root->val,搜索右子树
模板代码:
cpp
// LeetCode 700. 二叉搜索树中的搜索
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if(root == nullptr || root->val == val) return root;
if(root->val > val) {
return searchBST(root->left, val);
}
if(root->val < val) {
return searchBST(root->right, val);
}
return nullptr;
}
};
关键点:
- 利用BST性质:有方向的搜索
- 时间复杂度:O(h),空间复杂度:O(h)
5.3 二叉搜索树中的插入操作
适用场景:在BST中插入一个新节点
核心思路:
- 找到插入位置(空节点位置)
- 创建新节点并插入
模板代码:
cpp
// LeetCode 701. 二叉搜索树中的插入操作
class Solution {
private:
TreeNode* parent;
void traversal(TreeNode* cur, int val) {
if(cur == nullptr) {
// 找到插入位置
TreeNode* node = new TreeNode(val);
if(val > parent->val) {
parent->right = node;
} else {
parent->left = node;
}
return;
}
parent = cur;
if(cur->val > val) traversal(cur->left, val);
if(cur->val < val) traversal(cur->right, val);
}
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if(root == nullptr) {
return new TreeNode(val);
}
parent = new TreeNode(0);
traversal(root, val);
return root;
}
};
关键点:
- 找到空节点位置:就是插入位置
- 记录父节点:用于插入新节点
- 时间复杂度:O(h),空间复杂度:O(h)
5.4 删除二叉搜索树中的节点
适用场景:在BST中删除值为key的节点
核心思路:
- 找到要删除的节点
- 分五种情况处理:
- 没找到:返回原树
- 叶子节点:直接删除
- 只有左子树:左子树补位
- 只有右子树:右子树补位
- 左右子树都有:将左子树放到右子树最左节点的左孩子位置
模板代码:
cpp
// LeetCode 450. 删除二叉搜索树中的节点
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(root == nullptr) return root; // 没找到
if(root->val == key) {
// 情况2:叶子节点
if(root->left == nullptr && root->right == nullptr) {
delete root;
return nullptr;
}
// 情况3:只有右子树
if(root->left == nullptr) {
TreeNode* retNode = root->right;
delete root;
return retNode;
}
// 情况4:只有左子树
if(root->right == nullptr) {
TreeNode* retNode = root->left;
delete root;
return retNode;
}
// 情况5:左右子树都有
TreeNode* cur = root->right;
while(cur->left != nullptr) {
cur = cur->left; // 找到右子树最左节点
}
cur->left = root->left; // 将左子树放到最左节点的左孩子位置
TreeNode* tmp = root;
root = root->right;
delete tmp;
return root;
}
if(root->val > key) root->left = deleteNode(root->left, key);
if(root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};
关键点:
- 五种情况:需要分别处理
- 情况5:将左子树放到右子树最左节点的左孩子位置
- 时间复杂度:O(h),空间复杂度:O(h)
5.5 修剪二叉搜索树
适用场景:修剪BST,使所有节点值在[low, high]范围内
核心思路:
- 如果节点值 < low,修剪右子树
- 如果节点值 > high,修剪左子树
- 如果节点值在范围内,递归修剪左右子树
模板代码:
cpp
// LeetCode 669. 修剪二叉搜索树
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if(root == nullptr) return nullptr;
// 如果节点值小于low,修剪右子树
if(root->val < low) {
return trimBST(root->right, low, high);
}
// 如果节点值大于high,修剪左子树
if(root->val > high) {
return trimBST(root->left, low, high);
}
// 节点值在范围内,递归修剪左右子树
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
关键点:
- 利用BST性质:可以确定修剪方向
- 时间复杂度:O(n),空间复杂度:O(h)
5.6 将有序数组转换为二叉搜索树
适用场景:将有序数组转换为高度平衡的BST
核心思路:
- 从数组中间构建根节点
- 递归构建左右子树
模板代码:
cpp
// LeetCode 108. 将有序数组转换为二叉搜索树
class Solution {
private:
TreeNode* traversal(vector<int>& nums, int left, int right) {
if(left > right) return nullptr;
int mid = left + ((right - left) / 2); // 防止溢出
TreeNode* root = new TreeNode(nums[mid]);
root->left = traversal(nums, left, mid - 1);
root->right = traversal(nums, mid + 1, right);
return root;
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return traversal(nums, 0, nums.size() - 1);
}
};
关键点:
- 从中间构建:保证平衡
- 左闭右闭区间:[left, right]
- 时间复杂度:O(n),空间复杂度:O(log n)
5.7 把二叉搜索树转换为累加树
适用场景:将BST转换为累加树(每个节点的值等于原树中大于等于该节点值的所有节点值之和)
核心思路:
- 反中序遍历:右 → 中 → 左
- 累加前一个节点的值
模板代码:
cpp
// LeetCode 538. 把二叉搜索树转换为累加树
class Solution {
private:
int pre = 0; // 记录前一个节点的值
void traversal(TreeNode* cur) {
if(cur == nullptr) return;
traversal(cur->right); // 右
cur->val += pre; // 中:累加
pre = cur->val; // 更新pre
traversal(cur->left); // 左
}
public:
TreeNode* convertBST(TreeNode* root) {
pre = 0;
traversal(root);
return root;
}
};
关键点:
- 反中序遍历:右 → 中 → 左
- 累加:当前节点值 += 前一个节点值
- 时间复杂度:O(n),空间复杂度:O(h)
5.8 二叉搜索树中的众数
适用场景:找到BST中出现频率最高的元素
核心思路:
- 方法1:遍历+哈希表统计
- 方法2:利用BST中序遍历有序的性质
方法1:哈希表统计
模板代码:
cpp
// LeetCode 501. 二叉搜索树中的众数(方法1:哈希表)
class Solution {
private:
void traversal(TreeNode* cur, unordered_map<int, int>& map) {
if(cur == nullptr) return;
map[cur->val]++;
traversal(cur->left, map);
traversal(cur->right, map);
}
public:
vector<int> findMode(TreeNode* root) {
unordered_map<int, int> map;
traversal(root, map);
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
});
vector<int> result;
result.push_back(vec[0].first);
for(int i = 1; i < vec.size(); i++) {
if(vec[i].second == vec[0].second) {
result.push_back(vec[i].first);
} else {
break;
}
}
return result;
}
};
方法2:利用BST性质
模板代码:
cpp
// LeetCode 501. 二叉搜索树中的众数(方法2:利用BST性质)
class Solution {
private:
int maxCount = 0; // 最大频率
int count = 0; // 统计频率
TreeNode* pre = nullptr;
vector<int> result;
void searchBST(TreeNode* cur) {
if(cur == nullptr) return;
searchBST(cur->left); // 左
// 中:统计频率
if(pre == nullptr) {
count = 1;
} else if(pre->val == cur->val) {
count++;
} else {
count = 1;
}
pre = cur;
// 更新结果
if(count == maxCount) {
result.push_back(cur->val);
}
if(count > maxCount) {
maxCount = count;
result.clear();
result.push_back(cur->val);
}
searchBST(cur->right); // 右
}
public:
vector<int> findMode(TreeNode* root) {
count = 0;
maxCount = 0;
pre = nullptr;
result.clear();
searchBST(root);
return result;
}
};
关键点:
- 利用BST中序遍历有序:相同值连续出现
- 时间复杂度:O(n),空间复杂度:O(1)(不考虑递归栈)
5.9 二叉搜索树的最小绝对差
适用场景:找到BST中任意两个节点的最小绝对差
核心思路:
- BST中序遍历是有序的
- 最小绝对差就是相邻两个节点的差值
模板代码:
cpp
// LeetCode 530. 二叉搜索树的最小绝对差
class Solution {
private:
void traversal(TreeNode* cur, vector<int>& vec) {
if(cur == nullptr) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
public:
int getMinimumDifference(TreeNode* root) {
vector<int> vec;
traversal(root, vec);
int min = INT_MAX;
for(int i = 1; i < vec.size(); i++) {
if(vec[i] - vec[i - 1] < min) {
min = vec[i] - vec[i - 1];
}
}
return min;
}
};
关键点:
- BST中序遍历有序:相邻节点差值最小
- 时间复杂度:O(n),空间复杂度:O(n)
5.10 二叉搜索树的最近公共祖先
适用场景:在BST中找到两个节点的最近公共祖先
核心思路:
- 利用BST的性质:可以确定搜索方向
- 如果p和q都在左子树,搜索左子树
- 如果p和q都在右子树,搜索右子树
- 如果p和q分别在左右子树,当前节点就是LCA
模板代码:
cpp
// LeetCode 235. 二叉搜索树的最近公共祖先
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr) return root;
// 如果p和q都在左子树
if(root->val > p->val && root->val > q->val) {
return lowestCommonAncestor(root->left, p, q);
}
// 如果p和q都在右子树
if(root->val < p->val && root->val < q->val) {
return lowestCommonAncestor(root->right, p, q);
}
// 如果p和q分别在左右子树,当前节点就是LCA
return root;
}
};
关键点:
- 利用BST性质:可以确定搜索方向
- 时间复杂度:O(h),空间复杂度:O(h)
5.11 二叉搜索树中第k小的元素
适用场景:找到BST中第k小的元素
核心思路:
- 利用BST中序遍历有序的性质
- 中序遍历到第k个元素就是答案
模板代码:
cpp
// LeetCode 230. 二叉搜索树中第k小的元素
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
vector<int> result;
stack<TreeNode*> st;
if(root != nullptr) st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
if(node != nullptr) {
st.pop();
if(node->right) st.push(node->right); // 右
st.push(node); // 中
st.push(nullptr); // 标记
if(node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result[k - 1]; // 第k小的元素(索引从0开始)
}
};
关键点:
- 中序遍历:BST中序遍历是有序的
- 第k个元素:索引k-1
- 时间复杂度:O(n),空间复杂度:O(h)
优化版本(提前终止):
cpp
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
stack<TreeNode*> st;
TreeNode* cur = root;
int count = 0;
while(cur != nullptr || !st.empty()) {
if(cur != nullptr) {
st.push(cur);
cur = cur->left; // 左
} else {
cur = st.top();
st.pop();
count++; // 中
if(count == k) return cur->val; // 提前终止
cur = cur->right; // 右
}
}
return -1;
}
};
关键点:
- 提前终止:找到第k个元素后直接返回
- 时间复杂度:O(k),空间复杂度:O(h)
6. 二叉树的构造
6.1 从前序与中序遍历序列构造二叉树
适用场景:根据前序和中序遍历序列构造二叉树
核心思路:
- 前序遍历第一个元素是根节点
- 在中序遍历中找到根节点,分割左右子树
- 递归构造左右子树
模板代码:
cpp
// LeetCode 105. 从前序与中序遍历序列构造二叉树
class Solution {
private:
TreeNode* traversal(vector<int>& preorder, vector<int>& inorder) {
// 递归终止
if(preorder.empty()) return nullptr;
// 前序遍历第一个元素是根节点
int rootValue = preorder[0];
TreeNode* root = new TreeNode(rootValue);
// 只有一个节点,直接返回
if(preorder.size() == 1) return root;
// 在中序遍历中找到根节点位置
int delimiterIndex = 0;
for(; delimiterIndex < inorder.size(); delimiterIndex++) {
if(inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
// 切割前序数组(跳过第一个根节点)
vector<int> newPreorder(preorder.begin() + 1, preorder.end());
vector<int> leftPreorder(newPreorder.begin(), newPreorder.begin() + leftInorder.size());
vector<int> rightPreorder(newPreorder.begin() + leftInorder.size(), newPreorder.end());
// 递归构造左右子树
root->left = traversal(leftPreorder, leftInorder);
root->right = traversal(rightPreorder, rightInorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.empty()) return nullptr;
return traversal(preorder, inorder);
}
};
关键点:
- 前序第一个元素:根节点
- 中序分割:找到根节点位置,分割左右子树
- 前序分割:跳过第一个元素,根据中序分割结果分割
- 时间复杂度:O(n²),空间复杂度:O(n)
6.2 从中序与后序遍历序列构造二叉树
适用场景:根据中序和后序遍历序列构造二叉树
核心思路:
- 后序遍历最后一个元素是根节点
- 在中序遍历中找到根节点,分割左右子树
- 递归构造左右子树
模板代码:
cpp
// LeetCode 106. 从中序与后序遍历序列构造二叉树
class Solution {
private:
TreeNode* traversal(vector<int>& inorder, vector<int>& postorder) {
if(postorder.size() == 0) return nullptr;
// 后序遍历最后一个元素是根节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 叶子节点
if(postorder.size() == 1) return root;
// 在中序遍历中找到根节点位置
int delimiterIndex;
for(delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if(inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
// 后序数组舍弃末尾元素
postorder.resize(postorder.size() - 1);
// 切割后序数组
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
// 递归构造
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if(inorder.size() == 0 || postorder.size() == 0) return nullptr;
return traversal(inorder, postorder);
}
};
关键点:
- 后序最后一个元素:根节点
- 中序分割:找到根节点位置,分割左右子树
- 时间复杂度:O(n²),空间复杂度:O(n)
6.3 最大二叉树
适用场景:根据数组构造最大二叉树(根节点是最大值)
核心思路:
- 找到数组中的最大值作为根节点
- 递归构造左右子树
模板代码:
cpp
// LeetCode 654. 最大二叉树
class Solution {
private:
TreeNode* traversal(vector<int>& nums, int left, int right) {
if(left >= right) return nullptr;
// 找到最大值及其索引
int maxValueIndex = left;
for(int i = left + 1; i < right; i++) {
if(nums[i] > nums[maxValueIndex]) {
maxValueIndex = i;
}
}
TreeNode* root = new TreeNode(nums[maxValueIndex]);
// 递归构造左右子树
root->left = traversal(nums, left, maxValueIndex);
root->right = traversal(nums, maxValueIndex + 1, right);
return root;
}
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return traversal(nums, 0, nums.size());
}
};
关键点:
- 左闭右开区间:[left, right)
- 时间复杂度:O(n²),空间复杂度:O(n)
7. 二叉树操作的时间复杂度
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 遍历(递归) | O(n) | O(h) | h为树的高度 |
| 遍历(迭代) | O(n) | O(h) | 栈的深度 |
| 层序遍历 | O(n) | O(n) | 队列大小 |
| 查找节点 | O(n) | O(h) | 需要遍历 |
| 路径总和III | O(n²) | O(h) | 双重递归 |
| 最大路径和 | O(n) | O(h) | 后序遍历 |
| 二叉树直径 | O(n) | O(h) | 后序遍历计算高度 |
| 展开为链表 | O(n) | O(h) | 前序遍历 |
| BST查找 | O(h) | O(h) | 利用BST性质 |
| BST插入 | O(h) | O(h) | 利用BST性质 |
| BST删除 | O(h) | O(h) | 利用BST性质 |
| BST第k小 | O(k) | O(h) | 中序遍历提前终止 |
| 构造二叉树 | O(n²) | O(n) | 需要找分割点 |
注意:
- 平衡BST的h = log n,操作时间复杂度为O(log n)
- 最坏情况(链状)h = n,操作时间复杂度为O(n)
- 递归的空间复杂度主要取决于递归栈的深度
8. 何时使用二叉树技巧
8.1 使用场景
-
树的遍历
- 前序遍历:适合从根到叶子的操作
- 中序遍历:BST相关操作
- 后序遍历:适合自底向上的操作
- 层序遍历:适合按层处理
-
树的属性判断
- 对称性、平衡性、BST性质等
-
树的路径问题
- 路径和、所有路径等
-
树的构造
- 根据遍历序列构造树
-
BST操作
- 查找、插入、删除、验证等
8.2 判断标准
当遇到以下情况时,考虑使用二叉树技巧:
- 需要遍历树 → 选择合适遍历方式
- 需要判断树的性质 → 使用递归或迭代
- 需要处理路径问题 → 使用前序遍历+回溯
- 需要自底向上处理 → 使用后序遍历
- BST相关问题 → 利用BST中序遍历有序的性质
- 需要按层处理 → 使用层序遍历
示例:
cpp
// 问题:计算二叉树的最大深度
// 后序遍历:自底向上
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
return 1 + max(left, right);
}
// 前序遍历:自顶向下(需要传递深度参数)
void traversal(TreeNode* root, int depth, int& result) {
if(root == nullptr) return;
depth++;
if(root->left == nullptr && root->right == nullptr) {
result = max(result, depth);
}
traversal(root->left, depth, result);
traversal(root->right, depth, result);
}
9. 二叉树的优缺点
9.1 优点
- 结构清晰:递归结构,易于理解和实现
- 查找效率高:BST的平均查找时间为O(log n)
- 插入删除快:BST的插入删除时间为O(log n)
- 适合递归:树的结构天然适合递归处理
9.2 缺点
- 可能退化为链表:最坏情况下BST退化为链表,查找时间为O(n)
- 需要平衡:需要维护平衡性(如AVL树、红黑树)
- 内存开销:每个节点需要额外的指针空间
- 不适合随机访问:不能像数组一样通过索引访问
10. 常见题型总结
10.1 遍历类
-
前序遍历
- 递归、迭代、统一迭代法
- 适合从根到叶子的操作
-
中序遍历
- BST相关操作
- 验证BST、BST转有序数组
-
后序遍历
- 计算高度、找最近公共祖先
- 自底向上处理
-
层序遍历
- 按层处理、找最底层最左边节点
10.2 属性判断类
-
对称二叉树
- 比较左右子树是否对称
-
平衡二叉树
- 后序遍历计算高度差
-
验证二叉搜索树
- 中序遍历判断是否有序
- 或递归判断每个节点范围
-
二叉树的直径
- 后序遍历:计算每个节点的左右子树高度
- 直径 = 左子树高度 + 右子树高度
-
二叉树的右视图
- 层序遍历:记录每层最后一个节点
-
二叉树展开为链表
- 前序遍历:使用栈模拟,维护prev指针
10.3 路径问题类
-
路径总和
- 前序遍历+目标值递减
-
路径总和II
- 前序遍历+回溯
-
路径总和III
- 双重递归:外层遍历所有节点,内层从该节点开始寻找
-
二叉树的所有路径
- 前序遍历+回溯
-
二叉树中的最大路径和
- 后序遍历:计算每个节点的贡献
- 负贡献处理:如果为负则不选择
10.4 BST操作类
-
BST查找、插入、删除
- 利用BST性质,有方向的搜索
-
BST验证、转换
- 利用BST中序遍历有序的性质
-
BST构造
- 从有序数组构造平衡BST
-
BST中第k小的元素
- 中序遍历:利用BST中序遍历有序的性质
- 提前终止优化:找到第k个元素后直接返回
10.5 构造类
-
从遍历序列构造
- 前序+中序:前序第一个元素是根节点
- 中序+后序:后序最后一个元素是根节点
-
最大二叉树
- 找最大值作为根节点
11. 总结
二叉树是一种重要的树形数据结构,掌握二叉树的遍历和操作对于解决算法问题至关重要。
核心要点:
- 四种遍历方式:前序、中序、后序、层序,各有适用场景
- 递归和迭代:递归简洁,迭代需要栈或队列
- BST性质:中序遍历有序,这是BST最重要的性质
- 遍历选择 :
- 前序:从根到叶子
- 中序:BST相关
- 后序:自底向上
- 层序:按层处理
- 时间复杂度:遍历O(n),BST操作O(h)
使用建议:
- 需要从根到叶子时使用前序遍历
- BST相关问题利用中序遍历有序的性质
- 需要自底向上时使用后序遍历
- 需要按层处理时使用层序遍历
- 理解递归三要素:参数、返回值、终止条件
- 注意回溯:递归和回溯要一一对应
常见题型总结:
- 遍历类:前序、中序、后序、层序遍历
- 属性判断类:对称、平衡、BST验证、直径、右视图、展开为链表
- 路径问题类:路径总和、路径总和II、路径总和III、所有路径、最大路径和
- BST操作类:查找、插入、删除、验证、转换、第k小元素
- 构造类:前序+中序构造、中序+后序构造、最大二叉树