654. 最大二叉树
给定一个不重复的整数数组
nums。 最大二叉树 可以用下面的算法从nums递归地构建:
- 创建一个根节点,其值为
nums中的最大值。- 递归地在最大值 左边 的 子数组前缀上 构建左子树。
- 递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回
nums构建的 最大二叉树 。
cpp
class Solution {
public:
// dfs 递归构建函数
// 参数说明:数组、当前区间的左边界、当前区间的右边界
TreeNode* buildTree(vector<int>& nums, int left, int right) {
// 1. 终止条件:如果左边界超过右边界,说明区间无效,返回空
if (right < left) return NULL;
// 2. 寻找当前区间的最大值及其下标
// 初始化最大值为左边界元素,最大值下标为 left
int val = nums[left];
int index = left;
// 遍历区间寻找最大值
for (int i = left + 1; i <= right; i++) {
if (nums[i] > val) {
val = nums[i]; // 更新最大值
index = i; // 更新最大值下标
}
}
// 3. 创建根节点(最大值节点)
TreeNode* root = new TreeNode(val);
// 4. 剪枝:如果区间只有一个元素,它就是叶子节点,直接返回
if (left == right) return root;
// 5. 递归构建左子树
// 区间范围:[left, index - 1] (最大值左边的部分)
root->left = buildTree(nums, left, index - 1);
// 6. 递归构建右子树
// 区间范围:[index + 1, right] (最大值右边的部分)
root->right = buildTree(nums, index + 1, right);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return buildTree(nums, 0, nums.size() - 1);
}
};
总结
1. 解题思路:分治法
- 找根:在当前数组区间中找到最大值,它就是根节点。
- 分割:最大值左边的部分是左子树,右边的部分是右子树。
- 递归:对左右两部分重复上述过程。
2. 细节分析
- 寻找最大值:代码中使用线性扫描
for循环查找,逻辑清晰。 - 区间划分:
- 左子树区间:
[left, index - 1] - 右子树区间:
[index + 1, right] - 这与二叉树的前序/中序构造逻辑一致,关键在于确定
index(分割点)。
- 左子树区间:
3. 复杂度分析
- 时间复杂度:O(N²)
- 最坏情况:数组本身有序(如
[1, 2, 3, 4, 5]),每次只能切分出一个右节点,递归深度为 N,每层都要扫描剩余数组,总和为 N+(N−1)+...+1≈O(N2)
- 最坏情况:数组本身有序(如
- 空间复杂度:O(N)
- 主要是递归调用栈的开销,最坏情况(有序数组)深度为 N。
617. 合并二叉树
给你两棵二叉树:
root1和root2。想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
cpp
class Solution {
public:
// merge 函数:递归合并两棵树
// 返回值:合并后的新树的根节点
TreeNode* merge(TreeNode* root1, TreeNode* root2) {
// 1. 终止条件(处理空节点的情况)
// 如果树1为空,直接返回树2(不管树2是空还是有节点,都直接接过去)
if (!root1) return root2;
// 如果树2为空,直接返回树1
else if (!root2) return root1;
// 2. 处理当前节点
// 两棵树都不为空,创建新节点,值为两者之和
TreeNode* root = new TreeNode(root1->val + root2->val);
// 3. 递归合并左右子树
// 这里体现了"同步遍历"的思想:两棵树同时向左走,同时向右走
root->left = merge(root1->left, root2->left);
root->right = merge(root1->right, root2->right);
// 4. 返回合并后的节点
return root;
}
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
return merge(root1, root2);
}
};
总结
1. 解题思路:同步递归
这道题的逻辑非常清晰,就是两棵树"同位置节点相加"。
- 同步移动:
merge(root1->left, root2->left)保证了两棵树在同一层级、同一位置进行操作。 - 新树构建:这是一个构造新树的过程,通过
new TreeNode(...)创建新节点,而不是修改原有的树(虽然修改原有树也可以解题,但新建树更安全、逻辑更清晰)。
3. 复杂度分析
- 时间复杂度:O(N)
- N 是两棵树中节点数量的最小值。因为只要任意一棵树遍历完了(遇到空节点),递归就会直接返回。每个节点只访问一次。
- 空间复杂度:O(N)
- 取决于递归调用栈的深度。最坏情况(树退化为链表)为 O(N)。
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点
root和一个整数值val。你需要在 BST 中找到节点值等于
val的节点。 返回以该节点为根的子树。 如果节点不存在,则返回null。
cpp
// 方法一
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
// 1. 终止条件:节点为空,说明没找到,返回 NULL
if (root == NULL) return NULL;
// 2. 单层搜索逻辑
// 目标值比当前节点大,根据 BST 性质,去右子树搜索
if (root->val < val) {
return searchBST(root->right, val);
}
// 目标值比当前节点小,去左子树搜索
else if (root->val > val) {
return searchBST(root->left, val);
}
// 3. 找到目标值,直接返回当前节点
return root;
}
};
// 方法二
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
// 只要节点不为空,就继续向下搜索
while (root) {
// 目标值小于当前节点,向左走
if (root->val > val) {
root = root->left;
}
// 目标值大于当前节点,向右走
else if (root->val < val) {
root = root->right;
}
// 找到了,直接返回当前节点
else {
return root;
}
}
// 循环结束(root 为空),说明遍历到叶子也没找到,返回 NULL
return NULL;
}
};
总结
1. 解题思路对比
这道题利用了二叉搜索树(BST)的核心性质:左 < 根 < 右。
- 递归法:逻辑非常直观,利用系统栈来保存状态。代码简洁,符合直觉。
- 迭代法:因为 BST 的搜索路径是单向的(不需要回溯),所以非常适合使用迭代(循环)来实现。
2. 复杂度分析
- 时间复杂度:O(H)
- H 是树的高度。平均情况 O(log N),最坏情况 O(N)。因为每次比较都能排除一半的子树。
- 空间复杂度:
- 递归:O(H)(栈空间)。
- 迭代:O(1)。
98. 验证二叉搜索树
给你一个二叉树的根节点
root,判断其是否是一个有效的二叉搜索树。有效 二叉搜索树定义如下:
- 节点的左子树只包含 严格小于 当前节点的数。
- 节点的右子树只包含 严格大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
cpp
// 递归法(存数组)
class Solution {
public:
vector<int> ans; // 存储中序遍历的结果
// 中序遍历递归函数
void traversal(TreeNode* root) {
if (root == NULL) return;
traversal(root->left); // 左
ans.push_back(root->val); // 中:将节点值存入数组
traversal(root->right); // 右
}
bool isValidBST(TreeNode* root) {
// 1. 执行中序遍历
traversal(root);
// 2. 检查遍历结果是否严格递增
// BST 的中序遍历结果应该是一个严格递增的序列
for (int i = 1; i < ans.size(); i++) {
// 如果后一个元素 <= 前一个元素,说明不是 BST
if (ans[i] <= ans[i - 1]) return false;
}
return true;
}
};
// 递归法(双指针)
class Solution {
public:
TreeNode* pre = NULL; // 指针,用于记录遍历过程中的前一个节点
bool isValidBST(TreeNode* root) {
if (root == NULL) return true;
// 1. 递归左子树
bool left = isValidBST(root->left);
// 2. 处理当前节点(中序遍历的位置)
// 如果前驱节点存在,且前驱值 >= 当前值,违反 BST 规则
if (pre && pre->val >= root->val) return false;
// 更新前驱节点为当前节点
pre = root;
// 3. 递归右子树
bool right = isValidBST(root->right);
// 左右子树都合法才返回 true
return left && right;
}
};
// 迭代法(栈模拟中序)
class Solution {
public:
bool isValidBST(TreeNode* root) {
if (root == NULL) return true;
stack<TreeNode*> st; // 栈,用于模拟递归调用栈
TreeNode* cur = root; // 工作指针
TreeNode* pre = NULL; // 记录前一个访问的节点
// 循环条件:节点没遍历完 或 栈不为空
while (cur || !st.empty()) {
// 1. 模拟递归深入左子树
if (cur) {
st.push(cur); // 将访问过的节点入栈
cur = cur->left; // 继续向左
}
// 2. 左边走到头了,开始处理节点
else {
cur = st.top(); // 弹出栈顶元素(即当前最左节点)
st.pop();
// 【核心判断】比较当前节点和前驱节点
if (pre && pre->val >= cur->val) return false;
pre = cur; // 更新前驱指针
cur = cur->right; // 转向右子树
}
}
return true;
}
};
总结
1. 核心原理:中序遍历特性
二叉搜索树(BST)的一个重要性质是:中序遍历序列是一个严格递增的序列。这三种方法都是基于这个原理实现的。
2. 方法对比
- 方法一(存数组):
- 优点:逻辑最简单,容易理解。
- 缺点:空间复杂度高。需要额外的 O(N) 空间存储数组,且需要二次遍历数组。
- 方法二(递归双指针):
- 优点:空间复杂度较优(仅需递归栈空间),在遍历的同时直接比较,不需要存全部节点。
- 缺点:需要定义全局变量
pre,面试时要注意初始化问题。
- 方法三(迭代法):
- 优点:最优解。既避免了递归栈溢出的风险,又实现了 O(1) 的额外空间(不算栈空间),逻辑紧凑,一次遍历即可完成判断。
3. 关键细节:pre 指针
在方法二和方法三中,pre 指针代表中序遍历过程中的前一个节点。
- 判断条件必须是
pre->val >= cur->val,因为 BST 要求严格递增,等于的情况也是非法的。 - 在处理当前节点后,必须更新
pre = cur,为下一次比较做准备。
4. 复杂度分析
- 时间复杂度:O(N)。无论哪种方法,都需要遍历所有节点一次。
- 空间复杂度:
- 方法一:O(N)(数组 + 递归栈)。
- 方法二:O(H)(递归栈,H 为树高)。
- 方法三:O(H)(显式栈,H 为树高)。