最大二叉树
题干
题目:给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
-
创建一个根节点,其值为 nums 中的最大值。
-
递归地在最大值 左边 的 子数组前缀上 构建左子树。
-
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
注意:nums 中的所有整数 互不相同。
链接:. - 力扣(LeetCode)
思路和代码
给定的数组其实就是中序序列,其中最大值为根节点,每次都以最大值为分界划分左右子树。这与之前做的一道题 “利用中序序列和前序序列构造二叉树” 有异曲同工之妙,之前我们是根据前序序列的 “中左右” 顺序来确定根节点就是前序序列的第一个元素。而在这道题中,是直接告诉了我们根节点为最大值元素。
递归法
按照前序遍历的顺序,先找出数组中的最大值作为根节点,再划分为左右子树序列,递归构建左右子树即可。
class Solution {
public:
// 找最大值下标
int findMax(vector<int> &nums){
int max = nums[0];
int index = 0;
for (int i = 0; i < nums.size(); ++i) {
if (max < nums[i]){
max = nums[i];
index = i; // 更新最大值下标
}
}
return index;
}
// 构建二叉树
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
if (nums.size() == 0) return nullptr;
int maxIndex = findMax(nums); // 找序列中的最大值下标
TreeNode* root = new TreeNode(nums[maxIndex]); // 创建根节点
// 左子树序列
vector<int> leftTree(nums.begin(),nums.begin()+maxIndex);
// 右子树序列
vector<int> rightTree(nums.begin()+maxIndex+1,nums.end());
// 创建左子树
TreeNode* leftNode = constructMaximumBinaryTree(leftTree);
// 创建右子树
TreeNode* rightNode = constructMaximumBinaryTree(rightTree);
root->left = leftNode;
root->right = rightNode;
return root;
}
};
递归法:优化
和昨天的题目类似,我们在递归建立左右子树时需要知道左右子树的序列,但是我们并不一定需要新建数组存储子树序列,只要能知道子树序列对应的索引下标范围即可。
-
递归参数和返回值:参数是原始序列数组、传入的子树的序列起始下标和终止下标;返回值是当前树的根节点。
-
递归的结束条件:当起始下标比终止下标要大或相等,说明此时传进来的是空结点,直接返回空指针。
-
递归的顺序:根据前序遍历 “中左右” 的顺序,先建立根结点,再递归建立左右子树。
注意!!!!序列数组 nums 在传参时一定要取地址!!
class Solution {
public:
// 左闭右开
TreeNode* BuildTree(vector<int> &nums, int start, int end) {
// 这里的参数 nums 虽然取不取地址都可以,因为 nums 并没有在递归中被改变。
// 但是如果取地址,传进来的就是指针型变量,不需要新建数组
// 如果不取nums地址,则传参时会在栈空间中新建vector数组赋值,会很耗时!!
// 空结点
if (start >= end){
return nullptr;
}
// 找最大值下标
int maxValue = nums[start]; // 记录最大值
int maxIndex = start; // 最大值下标
for (int i = start+1; i < end; ++i) {
if (nums[i] > maxValue){
maxValue = nums[i];
maxIndex = i;
}
}
// 建立根节点
TreeNode* root = new TreeNode(nums[maxIndex]);
// 左子树序列
int leftStart = start;
int leftEnd = maxIndex;
// 右子树序列
int rightStart = maxIndex+1;
int rightEnd = end;
// 递归建立左右子树
root->left = BuildTree(nums,leftStart,leftEnd);
root->right = BuildTree(nums,rightStart,rightEnd);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums){
return BuildTree(nums,0,nums.size());
}
};
合并二叉树
题干
题目:给你两棵二叉树 root1 和 root2 。想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。
注意:合并过程必须从两个树的根节点开始。
链接:. - 力扣(LeetCode)
思路和代码
递归法
题目需要返回合并后的二叉树,那么可以重复利用 root1 来存储合并后的二叉树,只需让 root1 和 root2 对比,不需要新建树。
-
递归参数和返回值:参数分别是两个树的根,返回值是合并后的树的根结点。
-
递归结束的条件:当两个树的根都为空,则返回空指针;若根1非空,根2空,返回根1;若根1空,根2非空,返回根2;
-
递归顺序:根据前序遍历 “中左右” 的顺序,先看两个根节点是否重叠,若重叠则相加,根节点处理完再先后合并左右子树。
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
// 两棵树均为空
if (!root1 && !root2) return nullptr;
// root1 非空,root2 空,合并后应该是返回 root1
if (root1 && !root2) return root1;
// root1 空,root2 非空,合并后应该是返回 root2
if (!root1 && root2) return root2;
// 前序遍历
root1->val += root2->val;
root1->left = mergeTrees(root1->left,root2->left);
root1->right = mergeTrees(root1->right,root2->right);
return root1;
}
};
迭代法:层序遍历
同时层序遍历两棵树 root1 和 root2,用一个队列 queue 同时存储两棵树对应的结点,再相互比较这两个结点。同样还是利用 root1 存储合并后的二叉树。
在层序遍历的过程中,我们需要先判断当前两个树的结点是否都有左右孩子(即是否重叠),如果有,则让左右孩子相应入队;执行完上一步判断,才执行 root1 孩子为空而 root2 孩子非空的情况,因为我们是用 root1 存储合并后的树,root1 缺少的需要 root2 进行补齐,而 root2 缺少的不需要理会。这两个步骤的顺序不可以颠倒!!
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
// 两颗树中只要有一颗为空,返回非空的那棵
if (!root1) return root2;
if (!root2) return root1;
// 层序遍历
queue<TreeNode*> layer; // 存储一层的结点
layer.push(root1);
layer.push(root2);
int count = 1;
while (!layer.empty()){
TreeNode* cur1 = layer.front();
layer.pop();
TreeNode* cur2 = layer.front();
layer.pop();
// 我们要保证传入的两个结点都非空
cur1->val += cur2->val;
// 先判断左右孩子都非空的情况,即判断是否为重叠的结点,如果是,则入队
if (cur1->left && cur2->left){
// root1和 root2的左孩子都非空,要进行比较,加入队列
layer.push(cur1->left);
layer.push(cur2->left);
}
if (cur1->right && cur2->right){
// root1和 root2的右孩子都非空,要进行比较,加入队列
layer.push(cur1->right);
layer.push(cur2->right);
}
// 顺序不可颠倒!!!
// 再判断如果 root1 的左右孩子空,而 root2 的左右孩子非空的情况
if (!cur1->left && cur2->left){
// root1左孩子空,root2左孩子非空
cur1->left = cur2->left;
}
if (!cur1->right && cur2->right){
// root1右孩子空,root2右孩子非空
cur1->right = cur2->right;
}
}
return root1;
}
};
为什么顺序不可以颠倒?让我们来假设顺序颠倒的情况(注意,以下代码是错误示范!):
// 如果先判断 root1 的左孩子空,而 root2 的左孩子非空的情况
if (!cur1->left && cur2->left){
cur1->left = cur2->left;
// 此时本来 cur1 的左孩子为空,在这里补齐了
// 则cur1的左孩子变为非空了
}
// 再判断左孩子都非空的情况
// 由于前面先补齐了cur1的左孩子,这里就满足了cur1的左孩子和cur2的左孩子都非空的情况
// 这时候两个左孩子都入队了,在后续的层序遍历中,程序会认为入队的两个结点就是重叠的结点,将两个节点值相加
// 但这两个结点并不属于重叠的结点!不需要相加!
if (cur1->left && cur2->left){
layer.push(cur1->left);
layer.push(cur2->left);
}
二叉搜索树中的搜索
题干
题目:给定二叉搜索树(BST)的根节点 root 和一个整数值 val。你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
链接:. - 力扣(LeetCode)
思路和代码
根据二叉搜索树的性质,将要查询的值 val 和根节点比较,比根节点小的查询左子树,比根节点的查询右子树。
递归法
-
递归参数:参数是传入的要查询的树的根节点,要查询的值;返回值是查询到的结点。
-
递归结束的条件:当传入的结点为空,说明已经没找到,直接返回空指针;如果找到了,直接返回当前结点。
-
递归顺序:前序遍历,根据 “中左右” 的顺序,先查询中间结点,再递归查询左子树、右子树。
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (root == nullptr) return nullptr;
if (val == root->val) return root;
else if (val < root->val) return searchBST(root->left,val); // 递归查询左子树
else return searchBST(root->right,val); // 递归查询右子树
}
};
迭代法
使用一个指针 cur 遍历二叉树,将要查询的值 val 和当前结点 cur 比较,比根节点小的查询左子树,比根节点的查询右子树。
二叉搜索树在遍历查询时不需要回溯!这和在一般二叉树中查询结点不同,因为二叉搜索树的结点是有序的,直接就能确定搜索的方向。
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
TreeNode* cur = root;
while (cur){
if (val == cur->val){
break;
} else if (val < cur->val){
cur = cur->left; // 遍历左子树查询
} else{
cur = cur->right; // 遍历右子树查询
}
}
return cur;
}
};
验证二叉搜索树
题干
题目:给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效二叉搜索树定义如下:
-
节点的左子树只包含 小于 当前节点的数。
-
节点的右子树只包含 大于 当前节点的数。
-
所有左子树和右子树自身必须也是二叉搜索树。
链接:. - 力扣(LeetCode)
思路和代码
方法一:递归法,中序遍历
这个问题没有那么简单,单纯将问题分解为每个结点的左右子树是否都是二叉搜索树是行不通的。
陷阱一:不能单纯的比较左节点小于中间节点,右节点大于中间节点。
正确思路:要比较 左子树所有节点小于中间节点,右子树所有节点大于中间节点。
解决方法:需要定义全局变量 maxValue 来记录遍历过的最大值,按照 “左中右” 的顺序,后来遍历的元素都要比这个最大值要大。
陷阱二:一般我们都将 maxValue 初始化为 int 的最小值,再判断后来的结点是否都比 maxValue 要大。但样例中整棵树的最小节点也有可能是 int 的最小值,此时无法比较大小。
解决方法:将 maxValue 初始化为 longlong 的最小值,这样就比 int 的最小值还要小。
-
递归参数和返回值:参数是传入的树的根结点,返回值是判断是否为二叉搜索树的 bool 型值。
-
递归结束条件:当传入结点为空时,返回真;
-
递归顺序:采用中序遍历,先判断左子树是否为二叉搜索树,在递归左子树的过程中不断更新左子树的最大值 maxValue,左子树遍历完回到中间结点时,要保证中间结点比左子树的最大值还要大,并更新最大值 maxValue 为中间节点值;若中间节点没有比 maxValue 大,说明不是二叉搜索树,直接返回false。中间结点处理完要继续遍历右子树,判断右子树是否为二叉搜索树。如果左右子树都为二叉搜索树,则返回 true。
class Solution {
public:
long long maxValue = LONG_MIN; // 记录遍历过程中的最大值
bool isValidBST(TreeNode* root) {
if (root == nullptr) return true;
bool isLeftBST = isValidBST(root->left);
// 由于先遍历了左子树,我们需要不断更新左子树的最大值 maxValue
// 二叉搜索树要保证后来的结点都比整个左子树都大
// 只需要保证当前结点比左子树的最大值 maxValue 大
if (root->val > maxValue){
maxValue = root->val;
// 更新最大值为中间结点
} else {
return false;
}
// 在遍历右子树之前,我们已经用 maxValue记录了左子树和中间节点的最大值
// 要保证所有右子树的结点都要比 maxValue 大才满足二叉搜索树
bool isRightBST = isValidBST(root->right);
return isLeftBST&&isRightBST;
}
};
方法二:求中序序列
如果当前二叉树是有效的二叉搜索树,则它的中序遍历序列一定是升序排列,且没有重复的元素。只需要求出中序序列即可。
class Solution {
public:
void inorder(TreeNode* root, vector<int> &nums){
if (root == nullptr) return;
inorder(root->left,nums);
nums.push_back(root->val);
inorder(root->right,nums);
}
bool isValidBST(TreeNode* root) {
vector<int> nums; // 存储中序序列
inorder(root,nums);
// 判断中序序列是否升序
for (int i = 0; i < nums.size()-1; ++i) {
if (nums[i] >= nums[i+1])
return false; // 只要不是升序,直接返回 false
}
return true;
}
};