654.最大二叉树:
文档讲解:代码随想录|654.最大二叉树
视频讲解:又是构造二叉树,又有很多坑!| LeetCode:654.最大二叉树_哔哩哔哩_bilibili
状态:已做出
思路:
这道题目要求使用给定的数组创建一个最大二叉树,递归是使用分治思想来解决,找到数组最大值,把这个最大值前后区域分开,分别再对子数组进行判断,这样不断的划分子数组,递归函数返回值是树节点,递归函数里每次都使用循环找到最大节点后这个节点的左右子树通过递归来创建,递归后就成功创建了要求的二叉树。递推算是这道题目的最优解吧,递推是用来了单调栈,使用单调栈遍历数组,单调栈里面维护单调递增,每次遍历到一个数组的元素后把小于此时遍历到的数组元素的栈顶元素都删除,这样栈里的元素就是能保证递增,随后在每层判断时如果数组元素大于栈顶元素,那么就让栈顶元素设置成数组元素节点的左子树,这个操作是个循环,每次大于栈顶元素就改变一次数组元素结点的左子树,这样就能让此时遍历的数组元素左子树指向子区域最大值,如果站顶元素大于数组元素,就让栈顶义元素的右子树指向数组元素节点,这样不断递归就能建立最大二叉树了。
递归(时间复杂度是n^2):
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
//使用索引进行分治
TreeNode* dfs(vector<int>& num,int l,int r) {
if(l>r) return nullptr; //当索引出现异常结束递归
int Max=num[l]; //先对这个最大值进行初始化,用来保存最大值
int xid=l; //这个用来保存最大值的下标
//通过循环找到这个区域的最大值
for(int i=l+1;i<=r;++i) {
if(num[i]>Max) {
Max=num[i];
xid=i;
}
}
TreeNode* root=new TreeNode(Max); //创建新节点
//创建根的左右子树
root->left=dfs(num,l,xid-1);
root->right=dfs(num,xid+1,r);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
TreeNode* root=dfs(nums,0,nums.size()-1);
return root;
}
};
递推(使用两个栈操作,过程过于繁琐,时间复杂度4n):
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
stack<TreeNode*>st;//创建栈,这个栈用来维护递增单调栈
if(nums.size()==0) return nullptr;
stack<TreeNode*>tempst; //这个栈用来保存从st栈出的元素
st.push(new TreeNode(nums[0])); //初始化栈
int dix=1;
TreeNode* root; //最终创建树的根
while(!st.empty()) {
//这里的循环一共分为了两个部分,一个部分是数组未遍历完,一个是数组已遍历完毕
if(nums.size()>dix) {
TreeNode* temp=new TreeNode(nums[dix++]);//创建临时节点
//这里循环让小于此时遍历的数组元素的栈元素都出栈,并让出栈的元素插入到tempst里,这里就是维护单调栈的操作
while(!st.empty() && temp->val>st.top()->val) {
tempst.push(st.top());
st.pop();
}
//下面就是处理tempst栈的操作
st.push(temp);
if(!tempst.empty()) {
//tempst栈里的元素都小于此下标的数组,并且栈是从栈顶到栈底递减,所以都处理成上一个左子树
st.top()->left=tempst.top();
TreeNode* t=tempst.top();
tempst.pop();
//下面就是对tempst所有的节点元素进行左子树的连接
while(!tempst.empty()) {
t->right=tempst.top();
t=tempst.top();
tempst.pop();
}
}
}
else {
//st剩下的节点元素必定是递增的,所以都以右子树操作连接
TreeNode* cur=st.top();
st.pop();
if(!st.empty())
st.top()->right=cur;
else root=cur;
}
}
return root;
}
};
递推(通过ai优化,只用了一个栈,时间复杂度优化到了2n):
cpp
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
stack<TreeNode*> stk;
for (int i = 0; i < nums.size(); ++i) {
TreeNode* curr = new TreeNode(nums[i]);
// 小于当前值的节点是当前节点的左孩子
while (!stk.empty() && stk.top()->val < nums[i]) {
curr->left = stk.top();
stk.pop();
}
// 当前节点是栈顶元素的右孩子
if (!stk.empty()) {
stk.top()->right = curr;
}
stk.push(curr);
}
// 栈底就是根节点
while (stk.size() > 1) stk.pop();
return stk.top();
}
};
遇到的困难:
这道题目最开始应该做过类似的,所以递归的分治做法不是很难,当时主要在思考怎么去用单调栈优化时间复杂度,因为递归每层都会使用到循环来找最大值,时间复杂度绝对不是最优解,所以去查看了最优解的方法是使用单调栈。最初单调栈最难想到的就是怎么去正确处理出栈和入栈的左右子树的指向,感觉这个很难处理,当时出现了很多大大小小的问题,还有循环和if语句的条件处理都没处理好,导致错了很多次,我最初的思路是把整体遍历分为两个部分,一个是数组未遍历完的操作,一个是数组遍历完的操作,为了处理好出栈入栈的操作,我设置了两个栈来辅助操作,一个栈用来保存出栈的较小节点,一个几也是单调栈,这个较小栈保存出栈的所有节点,最后这个较小栈的栈顶元素必定是这个栈里最大的结点,然后就是让这个较小栈里的节点元素的右子树都指向下一个栈顶的结点,这样就能完成处理右子树建立的问题,最后数组遍历完后单调栈剩下的就是递增节点了,全部都右子树处理既可。但这种方式太过复杂,处理起来太麻烦,而且这种方式其实时间复杂度是4n,因为每个结点要出入栈两次,最后ai优化后才把时间复杂度优化为2n,只使用了一个栈。
收获:
这道题目的优化解法让我接触到了单调栈这个用法,之前接触过单调队列的题目,这次终于遇到了单调栈的题型了,也是体验到了单调栈的妙处,通过单调栈的出栈入栈让强行让数组的所有节点都出栈入栈一次就可以成功创建最大树。不过分治思想递归也是一种很好的方法,使用分治思想更能体现出二叉树的特点。
617.合并二叉树:
文档讲解:代码随想录|617.合并二叉树
视频讲解:一起操作两个二叉树?有点懵!| LeetCode:617.合并二叉树_哔哩哔哩_bilibili
状态:已做出
思路:
这道题目要求我们合并二叉树,我也是使用了递归和递推两种方法,首先递归就是同时对两个二叉树的节点进行递归,这个递归函数的返回值就是节点指针,递归函数内部首先判断此时递归的两个二叉树节点是否都为空,是就直接结束递归,随后使用三段else-if语句来对此层合并节点的处理,如果递归的这两个二叉树节点都为非空,合并的val值就是两个val值的和,然后使用两个递归分别递归这两个二叉树的左右子树来建立合并后树的左右子树,如果一处为空,那和并的val值就是非空的那一个节点的val值,随后对这两个儿茶素递归时一个二叉树为空的那个位置传入的参数就是空,最后if语句判断完后返回合并后树的节点既可完成合并操作。递推当时我对合并二叉树有一个致命的误解,导致编写代码出现了很大的问题,我没有意识到合并合并二叉树出现一个二叉树节点为空一个不为空时可以直接加入这个非空的这个节点以及后序所有子结点都没有必要再进行判断了,这点当时并没有发现,我第一次实现递推代码非常繁琐,编写代码直接是把空的情况也考虑进去了,然而为空的情况本来就可以直接加上这一整个子树就可以了,后续为空的情况完全不需要去额外考虑,文档给出了最简洁的代码,我最后靠ai优化的代码也还是考虑到了空节点,导致不必要的时间消耗,就是使用tuple<TreeNode*,TreeNode*,TreeNode*>为元素的栈,这三个元素分别是两个不合并的二叉树节点和一个合并的二叉树节点,使用栈实现二叉树的前序思想来遍历二叉树,一整个循环的结束条件是这个栈为空,在循环里面判断栈顶元素并让栈顶元素出栈,判断栈顶元素前两个元素是否为空来创建此时节点位置合并后的val值,随后对左右子树进行处理,这里就出现了很大的问题,就是我的操作是只要需要合并的两个二叉树其中一个二叉树此位置的节点不为空就会去单独处理这个节点并插入栈,这样会浪费很对的时间,大致思路就是这样。
递归(把一个子树为空一个子树不为空的情况也考虑进去了,过程很多没有必要):
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
TreeNode* dfs(TreeNode* root1, TreeNode* root2) {
if(!root1 && !root2) return nullptr;
TreeNode* root;
//处理此节点的val值
if(root1 && root2) {
root=new TreeNode(root1->val+root2->val);
}
else if(root1) {
root=new TreeNode(root1->val);
}
else {
root=new TreeNode(root2->val);
}
//创建此节点的左右子树
if(root1 && root2) {
root->left=dfs(root1->left,root2->left);
root->right=dfs(root1->right,root2->right);
}
else if(!root1) {
root->left=dfs(nullptr,root2->left);
root->right=dfs(nullptr,root2->right);
}
else {
root->left=dfs(root1->left,nullptr);
root->right=dfs(root1->right,nullptr);
}
return root;
}
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
return dfs(root1,root2);
}
};
递推(和上面的代码一样多考虑了一个为空一个不为空的情况):
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
stack<pair<TreeNode*, TreeNode*>>st;
stack<TreeNode*>sroot;
if(!root1 && !root2) return nullptr;
TreeNode* root=new TreeNode();
sroot.push(root);
st.push({root1, root2});
while(!st.empty()) {
TreeNode* temp;
temp=sroot.top();
sroot.pop();
if(st.top().first && st.top().second) {
temp->val=st.top().first->val+st.top().second->val;
}
else {
temp->val=st.top().first?st.top().first->val:st.top().second->val;
}
TreeNode* t1=st.top().first;
TreeNode* t2=st.top().second;
st.pop();
if (t1 && t2) {
if(t1->right || t2->right) {
st.push({t1->right, t2->right});
temp->right=new TreeNode();
sroot.push(temp->right);
}
if(t1->left || t2->left) {
st.push({t1->left, t2->left});
temp->left=new TreeNode();
sroot.push(temp->left);
}
} else if (t1) {
if(t1->right) {
temp->right=new TreeNode();
sroot.push(temp->right);
st.push({t1->right, nullptr});
}
if(t1->left) {
temp->left=new TreeNode();
sroot.push(temp->left);
st.push({t1->left, nullptr});
}
} else if (t2) {
if(t2->right) {
temp->right=new TreeNode();
sroot.push(temp->right);
st.push({nullptr, t2->right});
}
if(t2->left) {
temp->left=new TreeNode();
sroot.push(temp->left);
st.push({nullptr, t2->left});
}
}
}
return root;
}
};
递推优化:
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if (!root1 && !root2) return nullptr;
TreeNode* root = new TreeNode();
stack<tuple<TreeNode*, TreeNode*, TreeNode*>> st;
st.push({root1, root2, root});
while (!st.empty()) {
auto [node1, node2, merged] = st.top();
st.pop();
// 节点值合并
if (node1 && node2)
merged->val = node1->val + node2->val;
else if (node1)
merged->val = node1->val;
else
merged->val = node2->val;
// 左子节点处理
TreeNode* left1 = node1 ? node1->left : nullptr;
TreeNode* left2 = node2 ? node2->left : nullptr;
if (left1 || left2) {
merged->left = new TreeNode();
st.push({left1, left2, merged->left});
}
// 右子节点处理
TreeNode* right1 = node1 ? node1->right : nullptr;
TreeNode* right2 = node2 ? node2->right : nullptr;
if (right1 || right2) {
merged->right = new TreeNode();
st.push({right1, right2, merged->right});
}
}
return root;
}
};
遇到的困难:
这道题目我最终的递归和递推都单独处理了空节点,会浪费大量时间,文档给出的用法就是最简洁的代码,文档思路特点就是把root2给添加到root1里,如果节点root2的子树root1没有就直接把这一处的整个子树都添加到root1里,如果root1里有的子树root2里没有就直接不用管这个子树,最后返回root1就完成合并了,这样的做法确实代码更加简洁,时间复杂度也更加小我当时做到最后都会有发现这个方法。
收获:
这道题目本身不难,主要是没想到可以直接忽略一个子树为空一个不为空的情况,通过这道题目的练习,更加体会到了合理正确的思路带来的便捷,所以思考思路也是一件非常重要的步骤,当时只是一股脑想着遍历二叉树,根本没想到会有这种做法。
700.二叉搜索树的搜索:
文档讲解:代码随想录|700.二叉搜索树的搜索
视频讲解:不愧是搜索树,这次搜索有方向了!| LeetCode:700.二叉搜索树中的搜索_哔哩哔哩_bilibili
状态:已做出
思路:
这道题目只要知道搜索二叉树的特性就能做出来,搜索二叉树就是左子树必定比跟节点小,右子树比根结点大,我这里只写了迭代,就是利用循环来遍历搜索二叉树,当发现目标值大于节点,就遍历这个节点的右子树,小于就遍历左子树,相等就表示找到了,找到空节点就表示没有这个结点。
代码:
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if(!root) return nullptr;
TreeNode* result=root;
while(result) {
if(val==result->val) return result;
else if(val>result->val) {
result=result->right;
}
else {
result=result->left;
}
}
return nullptr;
}
};
收获:
这道题目主要是熟悉使用二叉搜索树的特性去树中查找需要的节点,只要知道搜索二叉树就能马上做出来。
98.验证二叉搜索树:
文档讲解:代码随想录|98.验证二叉搜索树
视频讲解:你对二叉搜索树了解的还不够! | LeetCode:98.验证二叉搜索树_哔哩哔哩_bilibili
状态:已做出
思路:
这道题目让我们判断一颗树是不是二叉搜索树,我一开始想到的是递归通过记录子树的最大最小值来来判断是否是搜索树,因为二叉搜索树的特点就是左右子树的左子树都必须小于根,右子树必须大于根,那么就等于是每个节点值大于左子树的最大值小于右子树的最小值就是二叉搜索树了,所以我的递归返回值设置为pair,pair一个元素记录最小值,一个记录最大值,在遇到空节点后结束递归并且返回pair类型,pair元素一个装INT_MAX,一个装INT_MIN,这样才能在后面进行正确判断。随后再对左右子树进行判断,如果左右子树是空就不做处理,非空就判断左子树递归返回的pair最大值是否小于此节点,就说明这棵树不是搜索树,右子树此时判断最小是是否大于此节点,最后返回这个节点的最大值和最小值,依次操作最后就能判断出这棵树是否为搜索二叉树了。但是这道题目的优解书文档给出的中序遍历判断,文档使用中序给出了递归和迭代两种方式,二叉搜索树的特点刚好符合中序遍历的思想,因为二叉搜索树是左<跟<右,所以利用中序遍历刚好是从小到大遍历,文档使用了变量和指针两种方式来保存上一个遍历的值,当下一个值比上一个保存的值小时说明这个二叉树不是搜索树,迭代使用栈来模拟中序遍历,用一个指针变量来记录上一个遍历的节点,最后判断出是否为搜索二叉树。
递归(非中序做法):
cpp
/**
* Definition for a binary tree node.
* 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) {}
* };
*/
class Solution {
public:
bool result = true;
//递归返回值是队组,用来保存子树的最大值和最小值
pair<int, int> dfs(TreeNode* root) {
//结束递归并返回队组
if (!root) return {INT_MAX, INT_MIN};
//先对最大值和最小值的局部变量进行初始化,都复制为此节点的val值
int minVal = root->val;
int maxVal = root->val;
//下面两个if语句判断此节点的左子树和右子树是否符合搜索树
if (root->left) {
auto left = dfs(root->left);
if (left.second >= root->val) result = false;
minVal = min(minVal, left.first);
}
if (root->right) {
auto right = dfs(root->right);
if (right.first <= root->val) result = false;
maxVal = max(maxVal, right.second);
}
return {minVal, maxVal};
}
bool isValidBST(TreeNode* root) {
result = true;
dfs(root);
return result;
}
};
遇到的困难:
我一开始使用的方法并不是中序遍历,没想到可以使用中序来解决,但是通过记录最大最小值最后也解决了,这道题目主要还是要了解搜索二叉树的特性,搜索二叉树并不是单纯的跟大于左小于右,而是每个子树都符合这个要求,并且左子树的所有节点都要小于跟,右子树的左右节点都要大于根,这就是判断搜索二叉树的坑,如果只是以为左<根<右就会出现错误,中序遍历应该是最符合搜索二叉树的特性的,使用这个方式是最好解决的。
收获:
虽然这倒题目并没有使用中序遍历来解决,但也是通过自己的思路打到的题目要求,最后看了文档的方法也了解到了这类题目的优解,了解到了搜索二叉树和中序遍历特性相似,后续可以使用这种方式来快速解判断是否为二叉搜索树。