算法-树
算法-树
BST
BST节点模板
struct TreeNode{
int val;
TreeNode *left,*right;
TreeNode():val(0),left(nullptr),right(nullptr){}
TreeNode(int x):val(x),left(nullptr),right(nullptr){}
TreeNode(int x,TreeNode* l,TreeNode* r):val(x),left(l),right(r){}
}
1. 2021-4-18 不同的二叉搜索树 II
题目:
给定一个整数 n,生成所有由 1 … n 为节点所组成的 二叉搜索树 。
思路:
最优二叉树
和最优二叉树的思路很像,本质上是一道树形DP。
通过BST中序遍历的性质,将i从l->r分别取根。
随后先遍历左右两节点获得vector也就是子节点的各种可能。
随后二重循环分别从左右子节点里选择节点,新建根,指向左右子节点,插入到vector中。
最后返回vector。
代码:
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
return dfs(1,n);
}
vector<TreeNode*> dfs(int s,int e){
if(s>e)return {nullptr};
vector<TreeNode*> ans;
for(int i=s;i<=e;++i){
auto left=dfs(s,i-1);
auto right=dfs(i+1,e);
for(auto &l:left){
for(auto &r:right){
TreeNode *root=new TreeNode(i);
root->left=l;
root->right=r;
ans.push_back(root);
}
}
}
return ans;
}
};
2. 2021-4-18 恢复二叉搜索树
题目:
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
思路:
考的还是BST中序遍历的性质。
如果有两个节点出错,那么在中序遍历时出错中大的那个一定先遍历到,出错中小的那个一定后遍历到。
如果设置一个prev和now,只需要当prev>now的时候,就检测到了出错节点。
第一次prev>now,出错的是prev。第二次prev>now,出错的是now。
另外注意两点:
prev的设置应该是左 根 右的根结束操作之时,因为中序遍历是在最左节点开始进行的,prev自然是最左退出来之后的根处。
另外,如果出错的节点相连,那么second就是第一次检测到的now。
代码:
class Solution {
public:
TreeNode* first=nullptr;
TreeNode* second=nullptr;
TreeNode* prev=nullptr;
void dfs(TreeNode *now){
if(!now)return;
dfs(now->left);
if(prev){
if(prev->val>now->val){
if(first==nullptr)first=prev;
second=now;
}
}
prev=now;
dfs(now->right);
}
void recoverTree(TreeNode* root) {
if(!root)return;
dfs(root);
if(first&&second)swap(first->val,second->val);
}
};
3. 2021-4-18 先序中序构造BST 后序中序构造BST
题目:
根据一棵树的前序遍历与中序遍历构造二叉树。
根据一棵树的后序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
思路:
用到的是前序和后序对应中序的性质:
前序从左到右index对应的值,其值在中序中对应的index,index左边是左子树,右边是右子树。
后序从右到左index对应的值,其值在中序中对应的index,index左边是左子树,右边是右子树。
先通过hash将inorder[i]映射到i,随后全局维护一个preindex从0->n-1。
再维护一个postindex从n-1->0。
开始构建以后,val=preorder[preindex]/postorder[postorder],通过val可以快速定位到inorder的indexorder。
随后创建根节点进行构建就可以。
需要注意的是preorder对于preindex的++顺序是从左子树到右子树。
而postorder对于postorder的--顺序是从右子树到左子树。
代码:
//先序中序构造BST
class Solution {
public:
unordered_map<int,int> dic;
int preindex=0;
TreeNode* dfs(vector<int>& preorder, vector<int>& inorder,int l,int r){
if(l>r)return nullptr;
int val=preorder[preindex];
int index=dic[val];
preindex++;
TreeNode* root=new TreeNode(val);
auto ll=dfs(preorder,inorder,l,index-1);
auto rr=dfs(preorder,inorder,index+1,r);
root->left=ll;
root->right=rr;
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i=0;i<inorder.size();++i){
dic[inorder[i]]=i;
}
return dfs(preorder,inorder,0,preorder.size()-1);
}
};
//后序中序构造BST
class Solution {
public:
unordered_map<int,int> dic;
int postindex=0;
TreeNode* dfs(vector<int>& inorder, vector<int>& postorder,int l,int r){
if(l>r)return nullptr;
int val=postorder[postindex];
int index=dic[val];
postindex--;
TreeNode *root=new TreeNode(val);
auto rr=dfs(inorder,postorder,index+1,r);
auto ll=dfs(inorder,postorder,l,index-1);
root->left=ll;
root->right=rr;
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
postindex=postorder.size()-1;
for(int i=0;i<postorder.size();++i){
dic[inorder[i]]=i;
}
return dfs(inorder,postorder,0,postorder.size()-1);
}
};
4. 2021-5-9 满二叉树的递归
题目:
第一行输入0,第二行将0变为01,得到01;第三行将0变换为01,将1变换为10,得到0110;之后的每一行都是通过将其前一行中的0变换为01,将1变换为10而得到。
现给定N,K,其中1≤N≤30,1≤K≤2^(30-1)。输出第N行的第K个字符。
输入输出
输入示例:
5 10
输出示例:
0
思路:
是一棵满二叉树,前一半是上一层,后一半是上一层按位取非。
因此设置递归,find(n,k,now) now一开始为0。
当n=1时,返回now。
如果是前一半,也就是k<=(1<<n-2) (注意这里n-2 因为第一层是0,另外与上一层相比),返回find(n-1,k,now==0?0:1)即可。
如果是后一半,需要将k减去前一半的数字,再按位取反 find(n-1,k-(1<<n-2) ,now==0?1:0)即可。
代码:
int find(int n, int k, int now)
{
if (n == 1)return now;
else
{
if (k <= (1 << (n - 2)))return find(n - 1, k, now == 0 ? 0 : 1);
else return find(n - 1, k - (1 << (n - 2)), now == 0 ? 1 : 0);
}
}
int main()
{
int n, k;
cin >> n >> k;
cout << find(n, k, 0);
}
二叉树结构问题
1. 2021-4-18 二叉树展开成链表
题目:
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
思路:
第一直观感受就是后序遍历。
root->right=left;left->right=right;
出现两个问题:首先是left直接上要先->right到最右节点。
其次是左节点需要指向nullptr。
代码:
class Solution {
public:
TreeNode* dfs(TreeNode* root){
if(!root)return nullptr;
auto l=dfs(root->left);
auto r=dfs(root->right);
if(l){
root->right=l;
while(l->right!=nullptr)l=l->right;
l->right=r;
}
root->left=nullptr;
return root;
}
void flatten(TreeNode* root) {
if(!root)return;
dfs(root);
}
};
2. 2021-4-18 二叉树最近公共祖先
题目:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路:
其实p和q所在的位置对于当前root只有三种情况:
全在root左子树上,全在root右子树上,此时为了最近祖先的概念得进入子树。
还有一种情况就是分别在左右子树上,此时直接返回root即可。
判断是否在左右子树的方法,即通过后序遍历,root为p或者q时返回root即可。
root遍历完左右子树之后如果收到的是nullptr,说明这边子树没有p和q。
代码:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root)return nullptr;
if(root==p||root==q)return root;
TreeNode* l=lowestCommonAncestor(root->left, p, q);
TreeNode* r=lowestCommonAncestor(root->right, p, q);
if(l==nullptr)return r;
if(r==nullptr)return l;
if(l&&r)return root;
return nullptr;
}
};
3. 2021-4-18 满二叉树的next指针
题目:
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。
使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
思路:
一道少见的先序遍历递归题。
其实最难的就是连接不同子树的next。
由于满二叉树的性质,我们在root->left->next=root->right之后可能无从下手。
实际上我们需要连到root->next->left。
那么只需要先序遍历,如果root->next存在,root->right-next=root->next->left即可。
但这个思路其实有点小问题,后面一道题会出现。
代码:
class Solution {
public:
Node* connect(Node* root) {
if(!root)return nullptr;
if(!root->left)return root;
root->left->next=root->right;
if(root->next)root->right->next=root->next->left;
connect(root->left);connect(root->right);
return root;
}
};
4. 2021-4-18 一般二叉树的next指针
题目:
给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。
使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
思路:
与上题不同,子节点的数量不确定。
在最后一层可能会出现最左连到最右。
但本质还是一样,我们要通过next找到最右的父节点。
通过循环即可,如果next过程中有子树就停下来。
如果只是这样做是有问题的,在满二叉树中我们忽略了一个细节。
即next的构建应该是从右至左的,因为next是从左至右的,我们默认右边的next已经构建完成。
实际上当不是满二叉树时,必须要根右左的方式来搭建next。
代码:
class Solution {
public:
Node* connect(Node* root) {
if(!root)return nullptr;
if(!root->left&&!root->right)return root;
Node *child;
if(root->left&&root->right){
root->left->next=root->right;
child=root->right;
}
else {
child=root->left?root->left:root->right;
}
Node* tmp=root->next;
while(tmp&&!tmp->left&&!tmp->right)tmp=tmp->next;
if(!tmp)child->next=nullptr;
else if(tmp->left)child->next=tmp->left;
else if(tmp->right)child->next=tmp->right;
connect(root->right);
connect(root->left);
return root;
}
};
5. 2021-4-20 最大路径和
题目:
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
思路:
首先要搞清楚,一条路径一定会经过当前节点,但是不知道会经过多少左节点和右节点,我们需要清楚的是lmax和rmax。
由于val可能<0,我们每次将路径和的更新放在当前节点上,即lmax+root->val+rmax,并要求lmax>=0且rmax>=0。
每次返回值是子节点->父节点的最大路径。
lmax=max(0,dfs(root->lmax))
rmax=max(0,dfs(root->rmax))
ans=lmax+root->val+rmax
此时返回值即是root->val+max(lmax+rmax)了。
代码:
class Solution {
public:
int ans=-10000;
int dfs(TreeNode *root){
if(!root)return 0;
int lmax=max(dfs(root->left),0);
int rmax=max(dfs(root->right),0);
ans=max(ans,root->val+lmax+rmax);
return max(lmax,rmax)+root->val;
}
int maxPathSum(TreeNode* root) {
if(!root)return 0;
dfs(root);
return ans;
}
};