二叉树-leetcode

 总结:

遍历的实质是访问完二叉树所有结点的函数

二叉树题目思路可以是考虑遍历一遍二叉树得到答案。

怎么遍历:前中后层次遍历

前序vs后序

前序的缺点:

transverse(root,transmit_info){

    visit(root) using transmit_info;//在这里处理本结点root,还未遍历左右子树,使用的额外信息只有transmit_info

    transverse(root->left,transmit_info);

    transverse(root->right,transmit_info);
}

自顶向下遍历二叉树:

处理本结点root时需要某些信息,而这些信息只能通过函数接口tranverse()传递,不能获取左右子树的相关信息

后序遍历相较于前序遍历的优点:

leftinfo = transverse(root->left,transmit_info);//可以获取左子树相关信息返回

rightinfo = transverse(root->right,transmit_info);

visit(root) using leftinfo & rightinfo&transmit_info;//在这里处理本结点root,可以使用额外的left_info,right_info,transmit_info

自底向上遍历二叉树:

处理本结点root时可以通过前面对左右子树的遍历返回子树的相关信息,供本结点使用。

故当要求遍历二叉树所有结点,且对于每个结点的处理需要左右子树的相关信息时优先考虑后序遍历。

543. Diameter of Binary Tree

(1) Diameter of Binary Tree - LeetCode

遍历思路:

此题与104区别:求最大深度必经过根,而最大直径可能在各个结点出现,不一定在根结点出现。

所以此题需要遍历每个结点,找出直径最大的结点的直径

每个结点的直径——每个结点的直径为其左右子树的最大深度之和,故需使用后序遍历

int max = 0;
    int diameterOfBinaryTree(TreeNode* root) {
        helper(root);
        return max;
    }
    int helper(TreeNode* root){
        //递归基
        if(root==NULL)   return -1;
        //每个结点的工作:计算自己所在树的直径+更新最大直径记录+返回本子树的最大直径
        int leftmax = helper(root->left);
        int rightmax = helper(root->right);
        //每个结点的直径=左右子树最大直径+2
        int cur_diameter = leftmax + rightmax + 2;
        //更新
        if(cur_diameter > max) max = cur_diameter;
        //基于左右子树需要获取的额外信息理解本结点需返回的额外信息
        return findmax(leftmax,rightmax)+1;
    }
    int findmax(int a,int b){
       return a>b?a:b;
   }

687. Longest Univalue Path

(1) Longest Univalue Path - LeetCode

本题相较于543,也要求深度,故需使用后序遍历——自底向上

此外,是否向上取决于本结点cur的值是否与其父节点的值相等。

本结点需要做的事

计算满足条件时本结点的长度:使用leftmax和rightmax(左右子树返回的额外信息)

更新最大长度

判断是否继续向上计算:

        if cur->val与父结点val相等,继续往上,参考左右子树返回的额外信息考虑本结点返回的额外信息为本结点最大长度

        if cur->val与父结点val不相等,重新往上

所以本结点进行处理时用的额外信息:leftmax、rightmax由左右子树返回,max是全局变量,仍需通过接口传递父结点val

 int longest = 0;
    int longestUnivaluePath(TreeNode* root) {
        helper(root,-1001);
        return longest;
    }
    int helper(TreeNode* cur,int parentval){
        if(cur==NULL)  return -1;
        //使用后序遍历才能算子树深度——是否往上继续取决于当前结点的val与父结点的va是否相等
        int leftmax = helper(cur->left,cur->val);
        int rightmax = helper(cur->right,cur->val);
        int cur_long = leftmax + rightmax + 2;
        if(cur_long > longest) longest = cur_long;
        if(cur->val == parentval)   
            return findmax(leftmax,rightmax) + 1;
        else
            return -1;
    }
    int findmax(int a,int b){
        return a>b?a:b;
    }

102. Binary Tree Level Order Traversa

Binary Tree Level Order Traversal - LeetCode

层次遍历:可以抽象为BFS,左右结点为本结点的相邻结点

迭代解法:队列+双层循环模板(内循环是本层结点个数)

递归解法:

107. Binary Tree Level Order Traversal II

Binary Tree Level Order Traversal II - LeetCode

c++给的返回值是vector,头插法对于动态数组是不是时间复杂度过高?

如果不能用头插法,则可以使用反转。

vector<vector<int>> levelOrderBottom(TreeNode* root) {
        if(root==NULL)  return{};
        vector<vector<int>> res;
        helper(res,root,0);
        reverse(res.begin(),res.end());
        return res;
    }
    void helper(vector<vector<int>> &res,TreeNode* root,int depth){
        if(root==NULL)  return ;
        if(depth>=res.size())   res.push_back({});
        
        res[depth].push_back(root->val);
        helper(res,root->left,depth+1);
        helper(res,root->right,depth+1);
        
    }

Binary Tree Zigzag Level Order Traversal - LeetCode103. Binary Tree Zigzag Level Order TraversalBinary Tree Zigzag Level Order Traversal - LeetCode

depth为奇数则需要反转,在递归中用头插法——动态数组复杂度高

本题使用迭代法

vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        if(root==NULL)  return {};
        vector<vector<int>> vresult;
        queue<TreeNode*> q;
        q.push(root);
        bool change_flag = false;//第一层left→right,不改变方向,标记flag==false
        while(!q.empty()){
            int n = q.size();
            vector<int> v;
            for(int i=0;i<n;i++){
                TreeNode* cur = q.front();
                q.pop();
                v.push_back(cur->val);
                if(cur->left)   q.push(cur->left);
                if(cur->right)  q.push(cur->right);
            }
            if(change_flag)//改变方向
                reverse(v.begin(),v.end());
            change_flag= !change_flag;//改变方向
            vresult.push_back(v);
        }
        return vresult;
    }

或直接利用数组的特性设置下标:

vector<vector<int> > zigzagLevelOrder(TreeNode* root) {
    if (root == NULL) {
        return vector<vector<int> > ();
    }
    vector<vector<int> > result;

    queue<TreeNode*> nodesQueue;
    nodesQueue.push(root);
    bool leftToRight = true;

    while ( !nodesQueue.empty()) {
        int size = nodesQueue.size();
        vector<int> row(size);
        for (int i = 0; i < size; i++) {
            TreeNode* node = nodesQueue.front();
            nodesQueue.pop();

            // find position to fill node's value
            int index = (leftToRight) ? i : (size - 1 - i);

            row[index] = node->val;
            if (node->left) {
                nodesQueue.push(node->left);
            }
            if (node->right) {
                nodesQueue.push(node->right);
            }
        }
        // after this level
        leftToRight = !leftToRight;
        result.push_back(row);
    }
    return result;
}

104. Maximum Depth of Binary Tree

(14) Maximum Depth of Binary Tree - LeetCode

递归思路:

DFS分成左右子树两部分递归(分而治之)——后序遍历

 int maxDepth(TreeNode* root) {
        if(root==NULL)  return 0;   //递归基
        int hleft = maxDepth(root->left);//递归函数帮忙求左子树高度
        int hright = maxDepth(root->right);//递归函数帮忙求右子树高度
        return max(hleft,hright)+1;//本结点计算本结点为根的子树高度
    }

BFS层次遍历思路

int maxDepth(TreeNode* root) {
        if(root==NULL)  return 0;
        
        int n = 0;
        std::queue<TreeNode*> q;
        q.push(root);
        int h = 0;
        while(!q.empty()){//队不空——本层存在,故树高+1
            n = q.size();
            h++;
            while(n-->0){//本层全部结点的邻接结点(子结点)——下一层入队
                TreeNode* t = q.front();
                q.pop();
                if(t->left!=NULL)   q.push(t->left);
                if(t->right!=NULL)  q.push(t->right);
            }
        }
       return h;
    }

故实现思路:

(1)遍历每个结点

如何遍历每个结点?——树的遍历思路:前中后遍历

if前序遍历:

###  visit(root);//求每个结点的直径,更新最大直径记录max

transverse(root->left);

transverse(root->right);

(2)###  visit(root);//求每个结点的直径,更新最大直径记录max的具体实现思路

求本结点的左子树最大深度

求本结点的右子树的最大深度

计算本结点的直径mydiameter  

int maxDiameter = 0;//最大直径记录
    int diameterOfBinaryTree(TreeNode* root) {
        //遍历二叉树的每个结点——遍历的思路(前中后遍历)
        tranverse(root);
        return maxDiameter;
    }
    
    void tranverse(TreeNode* root){
        if(root==NULL)  return ;//递归基
        //遍历每个结点:求每个结点的直径——遍历的要求visit(root)
        int left = maxDepth(root->left);
        int right = maxDepth(root->right);
        //计算本结点的直径
        int myDiameter = left+right;
        //更新最大直径
        maxDiameter = max(myDiameter,maxDiameter);
        
        tranverse(root->left);
        tranverse(root->right);
        
    }

缺点:时间复杂度高

对于每个结点,需要调用maxDepth(),时间复杂度为O(n),由于共n个结点,故时间复杂度为O(n^2)

递归思路

本结点的直径 = 左子树的最大深度+右子树的最大深度

求所有结点中的最大直径:使用记录器maxDiameter记录最大直径,每求一个结点的直径就更新一次

if后序遍历

 int diameterOfBinaryTree(TreeNode* root) {
        int maxDiameter = 0;
        transverse(maxDiameter,root);
        return maxDiameter;
    }
    
    int transverse(int &maxDiameter,TreeNode* root){
        if(root==NULL)   return 0;//递归基
        
        //本结点直径 = 左子树最大深度+右子树最大深度
        int left = transverse(maxDiameter,root->left);//递归函数帮忙求左子树的最大深度
        int right = transverse(maxDiameter, root->right);//递归函数帮忙求右子树的最大深度
        
        maxDiameter = max(left+right,maxDiameter);//更新最大直径visit(root)
        
        return max(left,right)+1;//本结点的最大深度
    }

652. Find Duplicate Subtrees

Find Duplicate Subtrees - LeetCode

先考虑每个结点的任务再放在遍历框架中:

每个当前结点要查看整颗树中是否存在与以本结点为根的子树相重复的子树,有则加入返回结果中

要确定本树的形状需确定左右子树的形状,故本题应该选择后序遍历框架

public:
    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
        vector<TreeNode*> res;
        map<string,int> mtree;
        helper(res,root,mtree);
        return res;
    }
    
    string helper(vector<TreeNode*> &res,TreeNode* cur,map<string,int> &mtree){
        //递归基
        if(cur==NULL)   return  "#";
        //本结点确认本树的形状
        string left = helper(res,cur->left,mtree);
        string right = helper(res,cur->right,mtree);
        string now = left+","+right+","+to_string(cur->val);//后序遍历,记住隔开左右结点
        //放入容器map中并判断是否重复且需要返回
        if(mtree[now]++ == 1) res.push_back(cur);
        return now;
    }

1373. Maximum Sum BST in Binary Tree

Maximum Sum BST in Binary Tree - LeetCode

每个结点的任务:判断本结点为根的子树是否为BST+计算结点值的和

本结点:

if(左子树是BST&&右子树是BST){

        本子树是否为BST?——(与左子树最右left_max和右子树最左right_min比较大小)

                是:计算sum=leftsum+cur->val+rightsum

                否:不是BST,不用再往上判断

}

由此可建立递归思路:由于需要左右子树的参数和结果才能继续本结点的递归,故考虑后序遍历

但是需要许多参数,故考虑建立一个数组传递多个参数,返回一个数组。也可以建立一个结构进行传递

class Solution {
public:
    int max = 0;
    int maxSumBST(TreeNode* root) {
        helper(root);
        return max;
    }
    
    vector<int> helper(TreeNode* root){
        //递归基——本树非BST||空树
        vector<int> res(4);//res[0]是否BST;res[1]本子树的min;res[2]本子树的max;res[3]本子树的sum
        if(root==NULL){
            res[0] = 1;
            res[1] = INT_MAX;
            res[2] = INT_MIN;
            res[3] = 0;  
            return res;
        } 
        vector<int> left  = helper(root->left);
        vector<int> right = helper(root->right);
        if(left[0]==1 && right[0]==1)//判断子树是否为BST
        {
            if(root->val > left[2] && root->val < right[1]){//判断本树是否为BST:大于左子树的max与小于右子树的min
                res[0] = 1;
                res[1] = (root->left!=NULL)?left[1]:root->val;//本子树作为左子树的min
                res[2] = (root->right!=NULL)?right[2]:root->val;//本子树作为右子树时的max
                res[3] = left[3]+ root->val +right[3];
                if(max<res[3]) max = res[3];
            }
        } 
        return res;
    }
};

树的遍历与回溯算法

前序遍历迭代实现

 vector<int> preorderTraversal(TreeNode* root) {
        //前序遍历迭代实现——DFS利用栈实现
        vector<int> v; 
        if(root==NULL)  return v;
        std::stack<TreeNode*> s;
        s.push(root);
        
        while(!s.empty()){
            TreeNode* t = s.top();
            v.push_back(t->val);
            s.pop();
            //当前结点的邻接结点入栈
            if(t->right!=NULL)  s.push(t->right);
            if(t->left!=NULL)   s.push(t->left);
        }
        return v;
    }

前序遍历是不断往左并深入直到最底再回溯,这里可以借鉴DFS,故迭代实现需要使用栈

前序遍历的自顶向下处理有时候也存在优势,可实现迭代算法:

226. Invert Binary Tree

(14) Invert Binary Tree - LeetCode

自底向上的DFS递归思路——后序遍历

TreeNode* invertTree(TreeNode* root) {
        if(root==NULL)  return NULL;//递归基
        
       //递归实现——自底向下思路
        invertTree(root->right);//让递归函数帮忙翻转右子树
        invertTree(root->left);//让递归函数帮忙翻转右子树
        //获得已翻转的左右子树后本结点进行反转
        TreeNode* temp = root->left;
        root->left = root->right;
        root->right = temp;
        return root;
    }

改为DFS迭代模式

TreeNode* invertTree(TreeNode* root) {
        if(root==NULL)  return NULL;
        
        std::stack<TreeNode*> s;
        s.push(root);
        
        while(!s.empty()){
            TreeNode* temp = s.top();
            s.pop();
            TreeNode* left = temp->left;
            if(temp->left!=NULL)    s.push(temp->left);
            if(temp->right!=NULL)   s.push(temp->right);
            temp->left = temp->right;
            temp->right = left;
        }
        return root;
    }

自顶向下的迭代思路——层次遍历BFS

TreeNode* invertTree(TreeNode* root) {
        if(root==NULL) return NULL;
        
        std::queue<TreeNode*> q;
        q.push(root);
        while(!q.empty()){
            //二级循环:一次处理一层
            for(int i=0; i < q.size(); i++){
                TreeNode* t = q.front();
                q.pop();
                //翻转
                TreeNode* left = t->left;
                t->left = t->right;
                t->right = left;
                //邻接结点(左右结点)入队
                if(t->left)   q.push(t->left);
                if(t->right)  q.push(t->right);
            }
        }
        return root;
    }

114. Flatten Binary Tree to Linked List

(14) Flatten Binary Tree to Linked List - LeetCode

递归思路

void flatten(TreeNode* root) {
       //DFS递归实现
        if(root==NULL)  return ;
        
        flatten(root->left);
        flatten(root->right);
        
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        TreeNode* cur = root;
        root->left = NULL;
        root->right = left;
    
        while(cur->right!=NULL)
            cur = cur->right;
        cur->right = right;
    }

DFS迭代实现

 void flatten(TreeNode* root) {
        //DFS迭代实现——利用栈
        if(root==NULL)  return;
        
        std::stack<TreeNode*> s;
        s.push(root);
        while(!s.empty()){
            TreeNode* t = s.top();
            s.pop();
            if(t->right!=NULL)  s.push(t->right);
            if(t->left!=NULL){
                s.push(t->left);
                t->right = t->left;
                t->left = NULL;
            }
            
            if(t->left==NULL && t->right==NULL)  {
                if(!s.empty())  t->right = s.top();//叶子结点连接防止断链
            }
        }
    } 

利用拉平后链表特性

按照前序遍历拉平后的链表:根左右

链表逆序:右左根

再逆序:根左右

TreeNode* prev = NULL;//逆序时首元素的prev为NULL
    void flatten(TreeNode* root) {
        if(root==NULL)  return;
        //按右左根顺序遍历树拉平链表
        flatten(root->right);
        flatten(root->left);
        //逆序连接
        root->left = NULL;
        root->right = prev;
        prev = root;   
    }

//链表的逆转递归实现,二叉树遍历顺序递归理解?

从链的角度考虑:

建立一条新链表,一直表头结点为根。寻找下一结点,不断插入新链中

从前序顺序考虑思路:

一次遍历完所有的结点,对于每个结点:

        根左右——先将右子树拉到左子树的最右,使得树变成只有左子树的单结点树,再将左子树            放到根的右边

        重复上述操作直到处理完全部的结点

void flatten(TreeNode* root) {
       while(root){//当结点未遍历完时
           //找到左链的最右位置,将右链拉到左链最右
           TreeNode* prev = root->left;
           if(prev!=NULL){
                while(prev->right!=NULL)  prev = prev->right;
                prev->right = root->right;
                root->right = root->left;
                root->left = NULL;
           }
           root = root->right;//下一结点
       }
    }

116. Populating Next Right Pointers in Each Node

(14) Populating Next Right Pointers in Each Node - LeetCode

常规思路:层次遍历

 Node* connect(Node* root) {
        if(root==NULL)  return NULL;
        
        //层次遍历实现
        std::queue<Node*> q;
        q.push(root);
        while(!q.empty()){
            int n = q.size();
            while(n>=1){
                Node* pre = q.front();
                q.pop();
                if(pre->left!=NULL)   q.push(pre->left);
                if(pre->right!=NULL)  q.push(pre->right);
                if(n==1){//处理本层仅单个元素时NULL——队列空时,q.front()返回值未定义
                    pre->next = NULL;
                    break;
                }
                pre->next = q.front();
                n--;
            }
        }
        return root;
    }

trick:discussion里有将NULL结点push()的做法

缺点:需要O(n)space和O(n)time

能否优化空间复杂度?

对于每一结点r,r->next有三种可能:

r为父p的左孩子:r->next = p->right;

r为父p的右孩子:

当p->next为NULL时,r->next = NULL;

当p->next不空时,r->next = p->next->left;

利用上一层已经建立的链表指针连接本层

Node* connect(Node* root) {
        if(root==NULL)  return NULL;
        Node* p = root;
        root->next = NULL;
        while(p->left){
            Node* r = p->left;
            while(p){//处理一层
                p->left->next = p->right;
                if(p->next) p->right->next = p->next->left;
                p = p->next;
          }
            p = r;//处理下一层
     }
        return root;

需要O(1)space和O(n)time

自顶向下递归思路

 Node* connect(Node* root) {
        if(root==NULL)  return NULL;
        //自顶向下递归思路
        if(root->left){
            root->left->next = root->right;
            if(root->next)
                root->right->next = root->next->left;
        }//连接本结点的左右子树
        
        connect(root->left);
        connect(root->right);
        return root;
  }

O(1)space+O(n)time

递归怎么理解?关键是右子树next的连接:

把二叉树每层结点都连接起来——把二叉树每个结点的左右指针的next指针都填充好

使用遍历模板递归的把二叉树每个结点遍历完成:

本结点只处理自己的左右指针

让递归函数帮忙处理左右子树的指针(分而治之)

summup:递归的技巧是把问题细化到每个结点要完成的任务,再将此任务放到遍历框架中

前后层次中序遍历框架的选择

654. Maximum Binary Tree

(14) Maximum Binary Tree - LeetCode

递归思路:

 TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return buildMaxTree(nums,0,nums.size()-1);
    }
    TreeNode* buildMaxTree(vector<int>& nums, int start,int end){
        if(start>end)   return NULL;//递归基
        
        int place = FindMax(nums,start,end);
        TreeNode* root = new TreeNode(nums[place]);
        root->left = buildMaxTree(nums,start,place-1);
        root->right = buildMaxTree(nums,place+1,end);
        return root;
        
    }
    int FindMax(vector<int>& nums,int start,int end){
        int max = start;
        for(int i=start+1; i<=end;i++){
            if(nums[i]>nums[max])   max = i;
        }
        return max;
    }

基于前序遍历框架

只有确定根的位置,才能确定左右子树的界限,从而让递归函数帮忙建立左右子树

每个结点的任务:

找出本结点的位置place——找到余下数组中的max作为本结点

余下数组中place左边建立左子树——让递归函数帮忙处理找到左儿子

余下数组中place右边建立右子树——让递归函数帮忙处理找到右儿子

sumup:数组&树考虑利用传递不同数组边界给递归函数来建立递归算法

有没有更优时间和空间复杂度的方案?

106. Construct Binary Tree from Inorder and Postorder Traversal

Construct Binary Tree from Inorder and Postorder Traversal - LeetCode


    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        return build(inorder,0,inorder.size()-1,postorder,0,postorder.size()-1);
    }
    
    TreeNode* build(vector<int>& inorder,int instart,int inend,vector<int>& postorder, int postart,int poend){
        if(instart>inend)   return NULL;
        int val = postorder[poend];
        int place = instart;
        while(inorder[place]!=val && place<=inend)  place++;
        TreeNode* root = new TreeNode(val);
        root->left = build(inorder,instart,place-1,postorder,postart,postart+place-instart-1);
        root->right = build(inorder,place+1,inend,postorder,postart+place-instart,poend-1);
        return root;
    }

能否用非递归方式实现?

时间复杂度?

同类题

105. Construct Binary Tree from Preorder and Inorder Traversal

Construct Binary Tree from Preorder and Inorder Traversal - LeetCode

889. Construct Binary Tree from Preorder and Postorder Traversal

Construct Binary Tree from Preorder and Postorder Traversal - LeetCode

从数组边界理解:

TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
        return build(preorder,0,preorder.size()-1,postorder,0,postorder.size()-1);
    }
    
    TreeNode* build(vector<int>& preorder,int prestart,int preend,vector<int>& postorder,int postart,int poend){
        if(prestart>preend) return NULL;//递归基
        
        int val = preorder[prestart];
        TreeNode* root = new TreeNode(val);
        if((prestart+1)<=preend){
            //存在左和右孩子
            //在postorder中找左根
            int left = preorder[prestart+1];
            int i = postart;
            while(postorder[i]!=left)   i++;
           root->left = build(preorder,prestart+1,prestart+i-postart+1,postorder,postart,i);
           root->right = build(preorder,prestart+i-postart+2,preend,postorder,i+1,poend-1); 
        }
        return root;
    }

前序:根左右

后序:左右根

依据前序遍历——根左右的特点建立数组,由于后序遍历根在数组尾部,故如果当前前序遍历节点与后序指针指向的元素不等,说明未到达后序根,即未到达结尾,整棵树未建立完成

 int preIndex = 0, posIndex = 0;\\Index作为遍历结束标志
    TreeNode* constructFromPrePost(vector<int>& pre, vector<int>& post) {
        TreeNode* root = new TreeNode(pre[preIndex++]);
        if (root->val != post[posIndex])
            root->left = constructFromPrePost(pre, post);\\让递归函数帮忙建左子树
        if (root->val != post[posIndex])
            root->right = constructFromPrePost(pre, post);\\让递归函数帮忙建右子树
        posIndex++;\\root->val==post[Index]说明到达当前树尾,指向下一树的根
        return root;
    }

迭代的理解?栈的作用?

[C++/Java/Python] One Pass, Real O(N) - LeetCode Discuss

进一步理解二叉树的遍历

297. Serialize and Deserialize Binary Tree

Serialize and Deserialize Binary Tree - LeetCode

反序列化的思路:

二叉树为二维结构,利用一维的序列构造一棵二叉树需要两个 序列

前+中/后+中/

若要利用单个序列确定一颗二叉树需要添加额外信息记录 空结点

假设序列化时已经实现添加额外信息记录 空结点建立了一个序列,如何利用这个序列建立一个二叉树?jianshu

这个序列是一个一维结构,如果基于一维数组/链表的思路,就是思考一维迭代如何实现二叉树

由于要建立一颗二叉树,也可以从建树角度思考如何利用已知信息递归遍历建树

前序遍历解法

class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        
        ostringstream oss;
        sehelper(oss,root);
        return oss.str();
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        istringstream iss(data);//输入流中存储字符串data—正好以空格为分割符 
        return dehelper(iss);
    }
    
    void sehelper(ostringstream &oss,TreeNode* root){
        //前序遍历二叉树——遇到null则在字符串中记录“#”,非null则直接转换为字符串
        if(root==NULL){
            oss<<"#"<<" ";//使用空格分隔字符串方便之后istringstream进行处理
            return;
        }
        oss<<root->val<<" ";
        sehelper(oss,root->left);
        sehelper(oss,root->right);
    }
    TreeNode* dehelper(istringstream &iss){
        //递归基——1、字符串已空   2、字符串为“#”
        //1、使用rdbuf()->in_avail()返回字符串长度
        if(iss.rdbuf()->in_avail()==0)  return NULL;
        //2、检查首字符若为“#”则返回null
        string str;
        iss>>str;//从输入流提取一个字符
        if(str=="#")    return NULL;
        
        TreeNode* root = new TreeNode(stoi(str));//利用字符串处理函数将字符转换为数值
        root->left = dehelper(iss);
        root->right = dehelper(iss);
        return root;
    }
};

后序遍历解法

需要理解后序遍历与递归

如前所述:遍历的实质是访问完二叉树所有结点的函数,递归的技巧是把问题细化到每个结点(当前结点)要完成的任务,再将此任务放到遍历框架中

后序遍历:左右根

去序列化时:递归时每个结点(当前结点)就是当前树的根要完成的是让递归函数帮忙建立左子树和右子树,本结点 再建立自己的根。

每次遍历的结点都是根,这与前述889. Construct Binary Tree from Preorder and Postorder Traversal的思路相同,需要由后→前遍历序列从而找到子树的根

为了让递归函数帮忙建立左右子树,需要哪些信息?——左右子树的序列范围

这些信息是否需要在递归函数接口进行传递?

递归序列时从后→前(根-右-左),处理完右后剩下的序列为左,故不需要传递左右子树的界限信息 ,只需要右子树正确的处理右(从右子树根开始)

所以对后序递归的理解并不仅仅是位置的问题

c++如何实现从后→前访问字符串?并pop()这个已经访问的字符串

层次遍历

利用一维迭代的思路

 // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        if(root==NULL)  return " ";
        ostringstream oss;
        std::queue<TreeNode*> qt;
        qt.push(root);
        while(!qt.empty()){
            TreeNode* cur = qt.front();
            qt.pop();
            if(cur==NULL)   oss<<"#"<<" ";
            else{
                oss<<cur->val<<" ";
                qt.push(cur->left);
                qt.push(cur->right);
            } 
        }
        return oss.str();
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        istringstream iss(data);
        string str;
        iss>>str;
        if(str.size()==0)    return NULL;
        //由于保存了空结点信息,可以认为保存了一颗满二叉树层次遍历信息
        std::queue<TreeNode*> qs;
        TreeNode* root = new TreeNode(stoi(str));
        qs.push(root);
        while(!qs.empty()){
            int n = qs.size();
            for(int i=0;i<n;i++){
                TreeNode* cur = qs.front();
                qs.pop();
                iss>>str;
                if(str=="#")    cur->left = NULL;
                else {
                    cur->left = new TreeNode(stoi(str));
                    qs.push(cur->left);
                }
                iss>>str;
                if(str=="#")    cur->right = NULL;
                else {
                    cur->right = new TreeNode(stoi(str));
                    qs.push(cur->right);
                }
            }
            if(iss.rdbuf()->in_avail()==0)  break;
        }
        return root;
   }

递归→迭代:用栈

后序遍历先访问左右孩子时需要两次访问到根

 vector<int> postorderTraversal(TreeNode* root) {
        vector<int> v;
        std::stack<TreeNode*> s;
        TreeNode* last = NULL;
        goAlongLeft(root,s);
        while(!s.empty()){
            TreeNode* cur = s.top();
            if((cur->left==NULL || cur->left==last)&&cur->right!=last){
                //左边已遍历
                goAlongLeft(cur->right,s);
            }
            if(cur->right==NULL||cur->right==last){
                v.push_back(cur->val);
                s.pop();
                last = cur;
            }
        }
        return v;
    }
    void goAlongLeft(TreeNode* root,stack<TreeNode*> &s){
        while(root){
            s.push(root);
            root = root->left;
        }
    }

递归到迭代的思路

void traverse(TreeNode* root) {
    if (root == null) return;
    traverse(root.left);
    traverse(root.right);
    /* 后序遍历代码位置 */
}

递归使用到函数调用栈,

假设计算机运行函数 A,就会把 A 放到调用栈里面,如果 A 又调用了函数 B,则把 B 压在 A 上面,如果 B 又调用了 C,那就再把 C 压到 B 上面……

当 C 执行结束后,C 出栈,返回值传给 BB 执行完后出栈,返回值传给 A,最后等 A 执行完,返回结果并出栈,此时调用栈为空,整个函数调用链结束。

故运行transverse(root)时会把root放入栈中,调用transverse(root->left),运行transverse(root->left)时会把root->left放入栈中,调用root->left->left....直到递归基(左孩子为NULL)。

故递归→迭代第一步是由根开始一路向左直到左孩子为空——goAlongLeft

接着,由于遇NULL会结束本层调用transverse(NULL),于是出栈(回溯到栈顶结点cur),然后检查右子树是否空,不空则重复goAlongLeft。所以回溯可能是从左子树/右子树回到cur的

对于cur,查看cur在递归中相当于查看以cur为根的子树。故在栈中当我们查看栈顶时相当于我们在处理以当前cur为根的子树。后序遍历中,对于cur结点的处理前提是左右子树已处理。

故需检查:

(if左子树NULL或已遍历+右子树未遍历)——遍历右子树goALongLeft(cur->right)

(if右子树NULL或已遍历)——遍历根cur并pop()

如何标记左右子树是否已经遍历?

后序:左右根(cur)

左子树已遍历==>左l右l左根,则左根已遍历

右子树已遍历==>左r右r右根,则右根已遍历

第一次从栈顶访问cur时是由左根返回时,此时不能pop出cur,需要访问完右子树,从右根第二次从栈顶访问cur再pop出cur。

由后序遍历的线性遍历特征,本次查看cur,上一次必是访问了cur的左子树左l根或右子树的右r根

故使用一个记录其记录上一次遍历的子树的根结点(相当于递归中递归函数)

if(last==cur->left)——第一次从栈顶访问,需要访问右结点

if(cur==cur->right)——第二次从栈顶访问,已访问完右结点,可访问根

中序遍历沿用上述思路

 vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        helper(root,v);
        return v;
    } 
    void helper(TreeNode* root,vector<int> &v){
        if(root==NULL)  return;
        helper(root->left,v);
        v.push_back(root->val);
        helper(root->right,v);
    }

同理需要goAlongLeft模拟递归栈,接着回溯到cur(查看栈顶)——相当于transverse(cur->left)结束,回溯到了根cur,故下一步是visit(cur),接着重复处理右子树。

vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        std::stack<TreeNode*> s;
        goAlongLeft(s,root);
        while(!s.empty()){
            TreeNode* cur = s.top();
            v.push_back(cur->val);
            s.pop();
            goAlongLeft(s,cur->right);
        }
        
        return v;
    } 
    
    void goAlongLeft(stack<TreeNode*> &s,TreeNode* root){
        while(root){
            s.push(root);
            root = root->left;
        }
    }

也可以这样理解:

goAlongLeft()结束==transverse(cur->left);结束

回溯——查看cur:

(if左子树NULL或已遍历+右子树未遍历)—visit(root)+遍历右子树goALongLeft(cur->right)

(if右子树NULL或已遍历)—也就是transverse(cur->right);结束,所以本层结束,故可以pop出cur,记录本层递归函数结束:visited = cur;

 vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        TreeNode* visited = new TreeNode(-1);
        std::stack<TreeNode*> s;
        goAlongLeft(s,root);//transverse(root->left);
        while(!s.empty()){
            TreeNode* cur = s.top();
            if((cur->left==NULL || cur->left==visited)&&cur->right!=visited){
                v.push_back(cur->val);//visit(root);
                goAlongLeft(s,cur->right);//transverse(root->right);
            }
            if(cur->right==NULL || cur->right==visited){//transverse(root->right);已结束
                visited = cur;//cur所标志的递归层已结束
                s.pop();//本层递归函数已结束,可以pop()
            }
        }
        
        return v;
    } 

中序遍历——BST

对BST的操作:增删改查

查找:

700. Search in a Binary Search Tree

Search in a Binary Search Tree - LeetCode

查找常规递归思路:利用了二叉树左<中<右的特性

TreeNode* searchBST(TreeNode* root, int val) {
        if(root==NULL) return NULL;
        
        if(val < root->val)   return searchBST(root->left,val);
        if(val > root->val)   return searchBST(root->right,val);
        return root;
    }

能否改为迭代?

TreeNode* searchBST(TreeNode* root, int val) {
        while(root){
            if(root->val==val)  return root;
            root = val < root->val ? root->left:root->right;
        }
        return NULL;
    }

230. Kth Smallest Element in a BST

Kth Smallest Element in a BST - LeetCode

思路一:利用中序遍历序列化,需要o(n)time和O(n)space

 int kthSmallest(TreeNode* root, int k) {
        int i = 0;
        int val;
        std::stack<TreeNode*> s;
        goAlongLeft(s,root);
        while(!s.empty()){
            TreeNode* cur = s.top();
            if(++i==k){
                val = cur->val;
                break;
            }
            s.pop();
            goAlongLeft(s,cur->right);
        }
        return val;
    }

基于递归思维改写:

public:
    int rank = 0;
    int val;
    int kthSmallest(TreeNode* root, int k) {
       transverse(root,k); 
        return val;
    }
    void transverse(TreeNode* root,int k){
        if(root==NULL) return;
        
        transverse(root->left,k);//让递归函数帮忙查找左子树
        //本结点查找自己是否符合条件
        rank++;
        if(rank==k) val = root->val;
        transverse(root->right,k);//让递归函数帮忙查找右子树
    }

但是BST使用o(n)time和O(n)space代价高,能否达到log(n)?

BST的查找特性是按值查找

if(val<cur->value)        往左子树查找

if(val>cur->value)    往右子树查

else        查找成功

现在是查找第k个,类比按值查找的思路,如果结点能提供自身的排行m相关的信息,则也能实现二叉查找。

struct TreeNode{
    int val;
    int rank;
    TreeNode* left;
    TreeNode* right;
};
class Solution {
public:
   
    int val;
    int kthSmallest(TreeNode* root, int k) {
       transverse(root,k); 
        return val;
    }
    void transverse(TreeNode* root,int k){
        if(root==NULL) return;
        //本结点查找自己是否符合条件
        if(cur->rank>k)  transverse(root->left,k);//往左走
        else if(cur->rank<k)  transverse(root->right,k);//让递归函数帮忙查找右子树
        else    val = root->val;
    }
};

//使用前提是能修改树的结构,增加rank字段 

98. Validate Binary Search Tree

Validate Binary Search Tree - LeetCode

比较简单的解法仍是利用中序遍历

 bool isValidBST(TreeNode* root) {
        //中序遍历获得序列
        vector<int> v;
        std::stack<TreeNode*> s;
        goAlongLeft(root,s);
        while(!s.empty()){
            TreeNode* cur = s.top();
            s.pop();
            v.push_back(cur->val);
            goAlongLeft(cur->right,s);
        }
        //依次比较是否升序
        int n = v.size();
        for(int i=0;i<n-1;i++){
            if(v[i]>=v[i+1]) return false;
        }
        return true;
    }

进一步优化考虑边生成中序遍历边判断,减少一个循环

bool isValidBST(TreeNode* root) {
        //中序遍历获得序列
        
        std::stack<TreeNode*> s;
        goAlongLeft(root,s);
        TreeNode* pre = NULL;
        while(!s.empty()){
            TreeNode* cur = s.top();
            s.pop();
            if(pre && pre->val>=cur->val)   return false;
            pre = cur;
            goAlongLeft(cur->right,s);
        }
        return true;
    }
    
    void goAlongLeft(TreeNode* root,stack<TreeNode*> &s){
        while(root){
            s.push(root);
            root = root->left;
        }
    }

仍是O(n)time和O(n)space,能否降低o(lgn)?

从递归考虑,利用BST的性质

每个节点的任务:

让递归函数帮忙判断左/右子树是否满足BST;

本节点判断自己为根的子树是否满足BST,在满足的情况下继续递归,不满足时马上退出

本结点判断方法:

cur->val < 右子树的最小值

cur->val > 左子树的最大值

bool isValidBST(TreeNode* root) {
        return helper(root,NULL,NULL);
    }
    
    bool helper(TreeNode* root, TreeNode* min_node, TreeNode* max_node){
        if(root==NULL)  return true;
        
        if(min_node && root->val <= min_node->val) return false;
        if(max_node && root->val >= max_node->val) return false;
        return helper(root->left,min_node,root)&&helper(root->right,root,max_node);
    }

易错思路:认为只需判断cur与左右结点的值之间的关系

由于cur必须获知整个左右子树的最值关系,而不仅仅是左右结点关系,故需要增加递归传递参数

538. Convert BST to Greater Tree

Convert BST to Greater Tree - LeetCode

二叉搜索树通常考虑中序遍历,本题的要求是通过计算累加值修改整颗树,具体到递归就是基于中序遍历框架修改每个结点的值。

考虑每个结点cur的任务:计算比cur大的结点值和+cur->val从而更新cur->val

比cur大的值在中序遍历中就是cur后面的结点,要更新cur的值必须先获得cur后面的值

既cur->val = cur->val + 本结点右子树的累加值+本结点的根及根的右子树的累加值。

这在中序框架中是行不通的,因为无法获得本结点的根及根的右子树的累加值

if(root==NULL)return;

transverse(root->left);

sum = transverse(root->right);//本结点右子树的累加值

cur->val = cur->val+sum;//不正确

由于必须先获得cur后面的值,也就是比cur大的值,从递归角度应该右后往前累加计算大→小。

BST的中序顺序是小→大。

故应该对BST逆序,实现大→小累加。所以遍历顺序为右左根。(理解递归对sum的作用与逆向遍历实现累加的必要性——递归与线性序列

每个结点的任务:

让递归函数修改右子树的值(计算右子树的累加值sum)。

本结点计算本结点的值

让递归函数修改左子树的值

public:
    int sum = 0;
    TreeNode* convertBST(TreeNode* root) {
        transverse(root);
        return root;
    }
    
    void transverse(TreeNode* root){
        if(root==NULL)  return ;
        transverse(root->right);
        sum = root->val + sum;
        root->val = sum;
        transverse(root->left);
    }

插入:

701. Insert into a Binary Search Tree

Insert into a Binary Search Tree - LeetCode

递归思路

 TreeNode* insertIntoBST(TreeNode* root, int val) {
        if(root==NULL)  return new TreeNode(val);
        
        if(val<root->val)   
                root->left = insertIntoBST(root->left,val);//让递归函数的在左子树中插入
        if(val>root->val)   
                root->right = insertIntoBST(root->right,val);//让递归函数的在右子树中插入
        return root;
    }

改为迭代方式:

 TreeNode* insertIntoBST(TreeNode* root, int val) {
        TreeNode* todo = new TreeNode(val);
        if(root==NULL)  return todo;
        TreeNode* cur = root;
        //找到插入子树的根结点
        while(true){
            if(val < cur->val)//往左子树走
            {
                if(cur->left==NULL){//在cur的左子树插入
                    cur->left = todo;
                    break;
                }
                else{
                    cur = cur->left;
                    continue;
                }  
            }
            else  //往右子树走
            {
                if(cur->right==NULL)
                {
                    cur->right = todo;
                    break;
                }
                else
                {
                    cur = cur->right;
                    continue;
                }
            }
        }
        return root;
    }

删除:

450. Delete Node in a BST

Delete Node in a BST - LeetCode

查找到再删除

删除时的逻辑:

非叶结点:找直接前驱(左子树最右)或直接后继(右子树最左),交换后删除直接前驱/后继

直接前驱/后继必只有一个结点(直接前驱只可能有左孩子,直接后继只可能有右孩子)或为叶结点

叶结点删除:需找到父结点改变指向

 TreeNode* parent = NULL;
    TreeNode* deleteNode(TreeNode* root, int key) {
        //BST查找
        TreeNode* cur = find(root,key,parent);
        if(cur){//查找成功
            TreeNode* direct = cur;
            if(cur->left){//存在左子树
                parent = cur;
                direct = cur->left;
                while(direct->right){
                    parent = direct;
                    direct = direct->right;
                }//找到直接前驱及其父结点                
                cur->val = direct->val;
                if(parent==cur) parent->left = direct->left;
                else parent->right = direct->left;
                return root;
            }
            else if(cur->right){//存在右子树
                parent = cur;
                direct = cur->right;
                while(direct->left){
                    parent = direct;
                    direct = direct->left;
                }//找到直接后继及其父结点
                cur->val = direct->val;
                if(parent==cur) parent->right = direct->right;
                else parent->left = direct->right;
                return root;
            }
            else{//叶结点
                if(parent){//只有一个结点的叶结点(根结点&叶结点)
                    if(parent->left==cur)   parent->left = NULL;
                    else parent->right = NULL;
                }
                else root = NULL;
            }
        }
        return root;
    }
    
    TreeNode* find(TreeNode* root, int key,TreeNode* proot){
        if(root==NULL)  return NULL;
        
        if(key < root->val)         return find(root->left,key,root);
        else if(key > root->val)    return find(root->right,key,root);
        parent = proot;
        return root;
    }

缺点:

①需要处理的特殊情况多

②在实际应用中,BST 节点内部的数据域是用户自定义的,可以非常复杂,而 BST 作为数据结构,其操作应该和内部存储的数据域解耦,所以我们更倾向于使用指针操作来交换节点,根本没必要关心内部数据。一般不会通过修改节点内部的值来交换节点。

基于二叉树特性,能不能在递归中完成删除?

TreeNode* deleteNode(TreeNode* root, int key) {
        //递归基
        if(root==NULL)  return NULL;
        
        if(key<root->val) root->left = deleteNode(root->left,key);
        else if(key>root->val)  root->right = deleteNode(root->right,key);
        else{//查找成功
            
            //叶结点\只有一个非空子节点
            if(root->left==NULL)    return root->right;
            if(root->right==NULL)   return root->left;
            
            //有两个非空结点-找直接前驱/直接后继替代
            TreeNode* direct = root->left;
            while(direct->right)   direct = direct->right;
            //让递归函数帮忙删除交换后的直接前驱
            root->left = deleteNode(root->left,direct->val);
            //直接前驱与欲删除的root交换
            direct->left = root->left;
            direct->right = root->right;
            root = direct;
        }
        return root;
    }

递归的优化

96. Unique Binary Search Trees

Unique Binary Search Trees - LeetCode

从1开始到n,每次选定一个i作为根,i左边构建左子树,i右边构建右子树,每个i所得BST数目g(i-1)+g(n-i)。

   int numTrees(int n) {
        if(n <= 1) return 1;
        int ans = 0;
        for(int i = 1; i <= n; i++) 
            ans += numTrees(i-1) * numTrees(n-i);
        return ans;
    }

此递归容易出错,数组问题一定要联系有没有正确处理数组边界。

 int numTrees(int n) {
        return helper(1,n);
    }
    int helper(int lo,int hi){
        if(lo >hi) return 1;
        
        int sum = 0;
        for(int i=lo; i<=hi; i++){//每一个结点作为根结点时的情况
            sum = sum+helper(lo,i-1)*helper(i+1,hi);//存在很多不必要的重复递归,时间复杂度太高了
        }
        return sum;
    }

由于存在许多重复递归,时间复杂度过高,未通过编译?

优化递归的思路:重复问题可以添加一个备忘录。

递归函数每次处理前先检查备忘录中是否已存在,存在则直接返回,不用递归。

递归函数每次处理后会存入本次计算的值。

int dp[20]{};
    int numTrees(int n) {
        if(n <= 1) return 1;
        if(dp[n]) return dp[n];
        for(int i = 1; i <= n; i++) 
            dp[n] += numTrees(i-1) * numTrees(n-i);
        return dp[n];
    }

基于上述公式可以利用DP?

DP解决方案在6行与解释。F(i, n) = G(i-1) * G(n-i) - LeetCode 讨论

✅ [C++/Python] 5 Easy Solutions w/ Explanation | Optimization from Brute-Force to DP to Catalan O(N) - LeetCode Discuss

由于已经有序,还可以考虑卡特兰数

同类题

95. Unique Binary Search Trees II

Unique Binary Search Trees II - LeetCode

vector<TreeNode*> generateTrees(int n) {
        return build(1,n);
    }
    vector<TreeNode*> build(int lo,int hi){
        vector<TreeNode*> v;
        if(lo>hi){
            v.push_back(NULL);
            return v;
        }
        
        for(int i=lo;i<=hi;i++){
            //以i为根时,求出其左右子树的所有可能
            //再建立根i与他们的连接
            vector<TreeNode*> leftTree = build(lo,i-1);
            vector<TreeNode*> rightTree = build(i+1,hi);
            int n = leftTree.size();
            int m = rightTree.size();
            for(int k=0;k<n;k++){//左右子树不同连接
                TreeNode* left = leftTree[k];
                for(int j=0;j<m;j++){
                    TreeNode* right = rightTree[j];
                    TreeNode* root = new TreeNode(i);
                    root->left = left;
                    root->right = right;
                    v.push_back(root);
                }
            }
        }
        return v;
    }

递归思路:

从每个结点的任务出发

已经完成的左右子树出发考虑每个结点还要做什么。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值