文章目录
小总结
二叉树一般先考虑下前中后序遍历的方法,而后就是DFS和BFS。
1、平衡二叉树
分析
这题有两种解法:
1)从顶至底:构造一个获取当前节点最大深度的方法 depth(root) ,通过比较此子树的左右子树的最大高度差abs(depth(root.left) - depth(root.right)),来判断此子树是否是二叉平衡树。若树的所有子树都平衡时,此树才平衡。
2)从底至顶:对二叉树做先序遍历,从底至顶返回子树最大高度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
bool isBalanced(TreeNode* root) {
if(root==NULL) return true;
return (dfs(root)==-1)?false:true;
}
int dfs(TreeNode* root){
if(root==NULL) return 0;
int left_dep=dfs(root->left);
if(left_dep==-1) return -1;
int right_dep=dfs(root->right);
if(right_dep==-1) return -1;
return abs(left_dep-right_dep)<2?max(left_dep,right_dep)+1:-1;
}
复杂度
时间复杂度 O(N)
空间复杂度 O(N): 最差情况下(树退化为链表时),系统递归需要使用 O(N) 的栈空间。
2、通过前中、中后遍历构造二叉树
题目
分析
这个题目的做法简单明了,关键在于细节。
先写下前中后序遍历的过程:
前:根->左->右
中:左->根->右
后:左->右->根
根据前/后找出根节点,然后找出中序遍历中根节点的位置,这样就能将左右子树分别找出来,进行下一次递归。
这里需要注意,在中序遍历中找到的根节点的位置在下次递归中只能用于确认中序遍历的范围,而前/后序遍历的范围则需要再进行确认,如下:
//由前中构建
int a=preorder[pleft];
TreeNode* root=new TreeNode(a);
int i=ileft;
while(i<iright&&inorder[i]!=a) ++i;
int step=i-ileft;
root->left=helper(preorder,inorder,pleft+1,pleft+step+1,ileft,i);
root->right=helper(preorder,inorder,pleft+step+1,pright,i+1,iright);
这里前序范围的确认需要先找出中序遍历中根节点距离左节点的距离,然后将其同前序遍历的左节点计算得出最终范围。
由中后构建也是如此,
int a = postorder[pright-1];
TreeNode* root = new TreeNode(a);
int i = ileft;
while (i < iright && inorder[i] != a) ++i;
int step=i-ileft;
root->left = helper(inorder, postorder, ileft, i , pleft, pleft+step);
root->right = helper(inorder, postorder, i+1 , iright, pleft+step, pright-1);
复杂度
时间复杂度:O(N log N),这里 N 是二叉树的结点个数,算法中每个结点都会被看到一次,是线性级别的,递归的深度是对数级别的。
空间复杂度 O(N)
3、二叉树的层序遍历和二叉树的之字形层次遍历
分析
这两题简称遍历双雄,第一个使用队列解决,相对简单。
而第二个则是需要使用栈来处理,这里要注意的是需要用到两个栈,然后相互存储,如下:
if(!s1.empty()){
while(!s1.empty()){
TreeNode* cur=s1.top();
s1.pop();
a.push_back(cur->val);
if(cur->left) s2.push(cur->left);
if(cur->right) s2.push(cur->right);
}
out.push_back(a);
}
else{
while(!s2.empty()){
TreeNode* cur=s2.top();
s2.pop();
a.push_back(cur->val);
if(cur->right) s1.push(cur->right);
if(cur->left) s1.push(cur->left);
}
out.push_back(a);
}
复杂度
时间复杂度: O(N),这里 N 是二叉树的结点个数
空间复杂度 :O(N/2),用到的空间最大时为叶子节点的个数
4、先序/后序遍历构造二叉树
题目
分析
这里使用二分法进行构造,如下:
TreeNode* preordertobst(vector<int>& preorder,int left,int right){
if(left>right) return NULL;
int flag=preorder[left];
TreeNode* root=new TreeNode(flag);
int l=left,r=right;
while(l<r){
int mid=l+((r-l+1)>>1);
if(preorder[mid]>flag) r=mid-1;
else l=mid;
}
root->left=preordertobst(preorder,left+1,l);
root->right=preordertobst(preorder,l+1,right);
return root;
}
这里要注意 l 和 r 的赋值,如果将 l 赋值为 left+1 会引起很多麻烦,所以最后像下面这样赋值:
int l=left,r=right;
后序遍历构造二叉树和前序差不多,把判断位置从left改到right,l=left,r=right-1 以及
root->left=preordertobst(preorder,left,l);
root->right=preordertobst(preorder,l+1,right-1);
注意:这里if (preorder[l] > preorder[right]) --l; 不能丢,假设输入为 [12,10] 分析可知。
复杂度
时间复杂度: O(lgN)
空间复杂度 :O(1)
5、前中后序遍历(迭代)
题目
这里只给个中序的
分析
参考:link
这里需要使用栈辅助,如下:
前序(根->左->右)
概述
栈S;
p= root;
while(p || S不空){
while(p){
访问p节点;
p的右子树入S;
p = p的左子树;
}
p = S栈顶弹出;
}
代码:
vector<int> preorder(TreeNode* root){
if(root==NULL) return {};
TreeNode* ptr=root;
stack<TreeNode*> s;
vector<int> out;
while(ptr){
while(ptr){
out.push_back(ptr->val);
if(ptr->right) s.push(ptr->right);
ptr=ptr->left;
}
if(!s.empty()){
ptr=s.top();
s.pop();
}
}
return out;
}
中序(左->根->右)
概述
栈S;
p= root;
while(p || S不空){
while(p){
p入S;
p = p的左子树;
}
p = S.top 出栈;
访问p;
p = p的右子树;
}
代码
vector<int> inorder(TreeNode* root){
if(root==NULL) return {};
TreeNode* ptr=root;
stack<TreeNode*> s;
vector<int> out;
while(ptr||!s.empty()){
while(ptr){
s.push(ptr);
ptr=ptr->left;
}
ptr=s.top();
s.pop();
out.push_back(ptr->val);
ptr=ptr->right;
}
return out;
}
后序(左->右->根)
这里采用了一种取巧的办法,即 先按 根->右->左 打印,最后再反转。
概述
栈S;
p= root;
while(p || S不空){
while(p){
访问p节点;
p的左子树入S;
p = p的右子树;
}
p = S栈顶弹出;
}
结果序列逆序;
代码:
vector<int> postorder(TreeNode* root) {
if (root == NULL) return {};
TreeNode* ptr = root;
stack<TreeNode*> s;
vector<int> out;
while (ptr) {
while (ptr) {
out.push_back(ptr->val);
if(ptr->left)s.push(ptr->left);
ptr = ptr->right;
}
if (!s.empty()) {
ptr = s.top();
s.pop();
}
}
reverse(out.begin(), out.end());
return out;
}
6、树的子结构
题目
分析
本题采用BFS进行遍历,然后不断进行比较即可,这里需要注意的是比较函数的写法。
这里要注意,因为 b 是 a 的子树,因此下面这种写法是有问题的:
bool isequal(TreeNode* a,TreeNode* b){
if(a==NULL&&b==NULL) return true;
if((a==NULL&&b!=NULL)||(a!=NULL&&b==NULL)||a->val!=b->val) return false;
return (isequal(a->left,b->left)&&isequal(a->right,b->right));
}
如 a 和 b 分别为[10,12,6,8,3,11]和[10,12,6,8],那么上述代码就会出错,因为会出现 a!=NULL&&b==NULL 的情况。
正确的写法如下:
bool isequal(TreeNode* a,TreeNode* b){
//由于 b 为子树,所以当 b 为NULL的时候无论 a 如何都返回true
if(b==NULL) return true;
//此时可以确认 b!=NULL ,而若a==NULL 或者 a和b的值不等,都返回false
if(a==NULL||a->val!=b->val) return false;
return (isequal(a->left,b->left)&&isequal(a->right,b->right));
}
7、翻转二叉树
题目
分析
本题需要注意的是可以使用常数空间实现,把所有节点的左右子节点交换即可,具体见(https://leetcode-cn.com/problems/invert-binary-tree/solution/dong-hua-yan-shi-liang-chong-shi-xian-226-fan-zhua/)的演示。
代码如下:
void exchange(TreeNode* root){
if(root->left==NULL&&root->right==NULL) return;
TreeNode* a=root->left;
root->left=root->right;
root->right=a;
if(root->left) exchange(root->left);
if(root->right) exchange(root->right);
return;
}
8、二叉树的最近公共祖先
题目
分析
参考:link
这里运用后序遍历的性质,由于根节点是最后一个访问的,我们可以记录根节点的左右节点中是否含有目标节点,有以下四种情况:
1)当 left 和 right 同时为空 :说明 root 的左 / 右子树中都不包含 p,q,返回 null;
2)当 left 和 right 同时不为空 :说明 p, q分列在 root 的 异侧 (分别在 左 / 右子树),因此 root 为最近公共祖先,返回 root ;
3)当 left 为空 ,right 不为空 :p,q都不在 root 的左子树中,直接返回 right 。具体可分为两种情况:
p,q 其中一个在 root 的 右子树 中,此时 right 指向 p(假设为 p );
p,q 两节点都在 root 的 右子树 中,此时的 right 指向最近公共祖先节点 ;
4)当 left 不为空 , right 为空 :与情况 3. 同理;
注:这里要维护一个bool,来记录两个节点是否都已找到,以处理情况3、4。
递归的终点:
1)当越过叶节点,则直接返回 null;
2)当 root 等于 p, q ,则直接返回 root ;
代码如下:
TreeNode* postsearch(TreeNode* root, TreeNode* p, TreeNode* q,bool &is_double){
if(root==NULL) return NULL;
TreeNode* l=postsearch(root->left,p,q,is_double);
TreeNode* r=postsearch(root->right,p,q,is_double);
if(root==p||root==q){
if(l||r) is_double=true;
return root;
}
if(l==NULL&&r==NULL) return NULL;
else if(l&&r){
is_double=true;
return root;
}
else if(l==NULL){
if(is_double) return r;
else return root;
}
else if(r==NULL){
if(is_double) return l;
else return root;
}
return NULL;
}
9、二叉树展开为链表
题目
分析
参考:link
这题我分别尝试了先序和后序遍历的做法,这两个做法属于最直接的解答方法,这里贴一下后序的代码:
TreeNode* postsearch(TreeNode* root){
TreeNode* l=root->left,*r=root->right;
if(!l&&!r) return root;
TreeNode* lend=NULL,*rend=NULL;
if(l){
lend=postsearch(l);
}
if(r){
rend=postsearch(r);
}
if(lend){
lend->right=r;
root->right=l;
root->left=NULL;
if (!rend) rend = lend;
}
return rend;
}
这里要注意的是 “if (!rend) rend = lend;” 这一步,如果给的是 [1,2,null,3] ,没有这一步的话,这里就会出错,因为 2 并不存在右子树,所以这里rend会返回NULL,这显然是错误的。
还有一点就是要将 root->left=NULL; 给带上。
还有一个做法就上述链接中的解法二,这个解法是我认为的最佳解法之一,这里直接贴他的源码:
void flatten(TreeNode* root) {
while (root != nullptr) {
if (root->left != nullptr) {
auto most_right = root->left; // 如果左子树不为空, 那么就先找到左子树的最右节点
while (most_right->right != nullptr) most_right = most_right->right; // 找最右节点
most_right->right = root->right; // 然后将跟的右孩子放到最右节点的右子树上
root->right = root->left; // 这时候跟的右孩子可以释放, 因此我令左孩子放到右孩子上
root->left = nullptr; // 将左孩子置为空
}
root = root->right; // 继续下一个节点
}
return;
}
10、恢复二叉树
题目
分析
参考:link
这题如果不对空间做任何要求的话,最简单的做法就是申请一个vector,然后遍历树,将值存进vector中,然后排序,再进行中序遍历,并将vector中的值依次替换进树中。
但如果要求不得申请额外空间的话,可以发现这两个节点有个规律:
第一个节点,是第一个按照中序遍历时候前一个节点大于后一个节点,我们选取前一个节点,这里指节点 4;
第二个节点,是在第一个节点找到之后,后面出现前一个节点大于后一个节点,我们选择后一个节点,这里指节点 1;
找到这两个点后交换其值即可,代码如下:
void recoverTree(TreeNode* root) {
if (root == NULL) return;
TreeNode* ptr = root;
stack<TreeNode*> tree;
TreeNode* firstnode=NULL,*secondnode=NULL;
TreeNode* pre=new TreeNode(INT_MIN);
while (ptr || !tree.empty()) {
while (ptr) {
tree.push(ptr);
ptr = ptr->left;
}
ptr = tree.top();
tree.pop();
if(firstnode==NULL&&pre->val>ptr->val) firstnode=pre;
if(firstnode!=NULL&&pre->val>ptr->val) secondnode=ptr;
pre=ptr;
ptr = ptr->right;
}
swap(firstnode->val,secondnode->val);
return;
}
这里注意,其实中序遍历也是需要空间的,就算是使用递归也会用到栈空间,最差的情况空间消耗依旧是N。
11、二叉树中的最大路径和
题目
分析
参考:link
对于二叉树 abc,a 是根结点(递归中的 root),bc 是左右子结点(代表其递归后的最优解),如下,
最大的路径,可能的路径情况有三种:
1)b+a+c
2)b+a+a的父节点
3)c+a+a的父节点
对于情况 1,表示如果不联络父结点的情况,或本身是根结点的情况。这种情况是没法递归的,但是结果有可能是全局最大路径和。
而情况 2 和 3,递归时计算 a+b 和 a+c,选择一个更优的方案返回。
注意,结点有可能是负值,最大和肯定就要想办法舍弃负值(max(0, x))(max(0,x))。
但是上面 3 种情况,无论哪种,a 作为联络点,都不能够舍弃。
代码如下:
class Solution {
public:
int maxPathSum(TreeNode* root) {
if(root==NULL) return 0;
maxlen=INT_MIN;
helper(root);
return maxlen;
}
int helper(TreeNode* root){
if(root==NULL) return 0;
int l=max(0,helper(root->left));
int r=max(0,helper(root->right));
int mid=l+r+root->val;
int ret=max(l+root->val,r+root->val);
maxlen=max(maxlen,max(mid,ret));
return ret;
}
private:
int maxlen;
};
12、二叉树的序列化与反序列化
题目
分析
参考:link
序列化
这里的序列化不必说,进行层序遍历即可,但为了反序列化能顺利进行,这里在进行BFS时需要将“null”也加入其中,如下图所示:
代码如下:
string serialize(TreeNode* root) {
if (root == NULL) return "[]";
queue<TreeNode*> q;
q.push(root);
vector<string> res;
while (!q.empty()) {
TreeNode* cur = q.front();
q.pop();
if (cur) {
q.push(cur->left);
q.push(cur->right);
}
if (cur) {
res.push_back(to_string(cur->val));
}
else res.push_back("null");
}
string ret;
ret += '[';
for (int i = 0; i < res.size(); ++i) {
ret += res[i];
ret += ',';
}
ret.pop_back();
ret += ']';
return ret;
}
反序列化
利用队列按层构建二叉树,借助一个指针 i 指向节点 node 的左、右子节点,每构建一个 node 的左、右子节点,指针 i 就向右移动 1 位。
代码如下:
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
if (data == "[]") return NULL;
int n = data.size();
int i = 1;
string s;
while (data[i] != ','&&data[i] != ']') {
s += data[i];
++i;
}
++i;
TreeNode* root = new TreeNode(atoi(s.c_str()));
queue<TreeNode*> q;
q.push(root);
while (i < n) {
string a,b;
while (data[i] != ','&&data[i] != ']') {
a += data[i];
++i;
}
++i;
while (data[i] != ','&&data[i] != ']') {
b += data[i];
++i;
}
++i;
if(a!=""||b!=""){
TreeNode* cur=q.front();
q.pop();
if(a!="null"){
TreeNode* aa = new TreeNode(atoi(a.c_str()));
q.push(aa);
cur->left=aa;
}
if(b!="null"){
TreeNode* bb = new TreeNode(atoi(b.c_str()));
q.push(bb);
cur->right=bb;
}
}
}
return root;
}