由前序遍历和中序遍历还原二叉树

1.由前序遍历和中序遍历还原二叉树

对应letecode链接:

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode) (leetcode-cn.com)

题目描述:

给定一棵树的前序遍历 preorder 与中序遍历  inorder。请构造二叉树并返回其根节点。

示例 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
示例 2:

Input: preorder = [-1], inorder = [-1]
Output: [-1]

提示:

1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder 和 inorder 均无重复元素
inorder 均出现在 preorder
preorder 保证为二叉树的前序遍历序列
inorder 保证为二叉树的中序遍历序列

解题思路:

二叉树前序遍历的顺序为:

先遍历根节点;

随后递归地遍历左子树;

最后递归地遍历右子树。

二叉树中序遍历的顺序为:

先递归地遍历左子树;

随后遍历根节点;

最后递归地遍历右子树。

 我们可以发现中序遍历可以给我们区分出左子树和右子树。左子树 根 右子树 于是我们可以在中序里面找到根所在的位置,将其划分为左子树和右子树的创建

由前序遍历和中序遍历还原二叉树_第1张图片

前序数组怎么切分呢?注意看下面这张图,根节点是橘色,绿色部分是左子树,蓝色部分是右子树 

由前序遍历和中序遍历还原二叉树_第2张图片

 

前序数组的 左子树部分+根节点 是 1,2,4,5,中序数组的 左子树部分+根节点 是 4,2,5,1。这两者的数组长度是一样的。
我们可以根据中序数组的中间位置 1,来确定前序数组的左右部分,由于前序数组第一个是根节点,
所以其左边部分是:[1:mid_index],右半部分是 [mid_index+1:]
这里的 mid_index 是中序数组的中间下标位置。
递归函数实现如下:

终止条件:前序和中序数组为空
根据前序数组第一个元素,拼出根节点,再将前序数组和中序数组分成两半,递归的处理前序数组左边和中序数组左边,递归的处理前序数组右边和中序数组右边。
动画演示如下:

由前序遍历和中序遍历还原二叉树_第3张图片

对应代码:

class Solution {
public:
    TreeNode* buildTree(vector& preorder, vector& inorder) {
                      if(preorder.size()==0)return nullptr;//结束条件
                      TreeNode*root=new TreeNode(preorder[0]);//根节点的创建
                      int index=0;
                      for(int i=0;ipreleft(preorder.begin()+1,preorder.begin()+index+1);//划分左子树
              vectorinleft(inorder.begin(),inorder.begin()+index);
              root->left=buildTree(preleft,inleft);//创建左子树

              vectorpreright(preorder.begin()+index+1,preorder.end());//划分右子树
              vectorinright(inorder.begin()+index+1,inorder.end());

              root->right=buildTree(preright,inright);//创建右子树
              return root;//返回结果
    }
};

要注意的是vector的拷贝是begin()到end()内的数据,但是不包括end()这是需要注意的 。

 但是这样的效率很低:

事实上,我们不需要真的把 preorder 和 inorder 切分了,只需要用分别用两个指针指向开头和结束位置即可。注意下边的两个指针指向的数组范围是包括左边界,不包括右边界。对于下边的树的合成。

 

由前序遍历和中序遍历还原二叉树_第4张图片

 左子树:

由前序遍历和中序遍历还原二叉树_第5张图片

右子树:

由前序遍历和中序遍历还原二叉树_第6张图片

 对应代码:


class Solution {
public:
    TreeNode* buildTree(vector& preorder, vector& inorder) {
                              if(preorder.size()==0)return nullptr;
           return BuildTreeHelper(preorder,0,preorder.size(),inorder,0,inorder.size());
    }
    TreeNode*BuildTreeHelper(vector&preorder,int p_start,int p_end,vector&inorder,int i_start,int i_end ){
          if(p_start==p_end)return nullptr;

     int root_val = preorder[p_start];
     TreeNode *root = new TreeNode(root_val);
      //在中序遍历中找到根节点的位置
       int i_root_index=0;
         for(int i=i_start;ileft = BuildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
  //  递归的构造右子树
root->right = BuildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
  
                    return root;
    }
};

 使用hash表保存根节点:

class Solution {
public:
               unordered_maphash;//保存根节点
    TreeNode* buildTree(vector& preorder, vector& inorder) {
                              if(preorder.size()==0)return nullptr;
                              for(int i=0;i&preorder,int p_start,int p_end,vector&inorder,int i_start,int i_end ){
          if(p_start==p_end)return nullptr;
             
     int root_val = preorder[p_start];
     TreeNode *root = new TreeNode(root_val);
      //在中序遍历中找到根节点的位置
       int i_root_index=hash[root_val];
      int leftNum = i_root_index - i_start;

   root->left = BuildTreeHelper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
  //  递归的构造右子树
    root->right = BuildTreeHelper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
  
                    return root;
    }
};

方法三:非递归

先序遍历构造二叉树,借助栈来实现,定义一个指针指向中序遍历的数组

遍历二叉树的先序序列的数组,如果栈顶节点和当前指针指向的值不相等,那么当前节点就是栈顶节点的左孩子,并将左孩子入栈
如果栈顶节点和当前指针指向的值相等,说明此时已经访问到了最左端的节点,那么当前节点是栈中某个节点的右孩子。此时将栈中的节点依次出栈,指向中序遍历序列的指针依次往右移动,如果当前栈顶节点和指针指向的节点的值不相等,停止出栈,那么说明当前节点是刚刚出栈节点的右孩子,将右孩子入栈。

对应代码:

class Solution {
public:
    TreeNode* buildTree(vector& preorder, vector& inorder) {
        //非递归形式,借助栈
        //根据先序遍历的顺序构造二叉树
        stack st;
        if(preorder.size() == 0)
            return NULL;
        TreeNode *root = new TreeNode(preorder[0]);
        st.push(root);
        int index = 0;
        for(int i = 1; i < preorder.size(); ++i){
            TreeNode *cur = st.top();
            if(cur->val != inorder[index]){
                cur->left = new TreeNode(preorder[i]);
                st.push(cur->left);
            }
            else{//此时说明栈中某个节点的右子树出现了
                //如果节点没有右子树,那么中序遍历和先序遍历恰好是逆序的形式
                //如果当前节点和栈中的顶部的节点不相等,那么这个节点是上次出栈的右孩子
                while(!st.empty() && st.top()->val == inorder[index]){
                    cur = st.top();
                    st.pop();
                    index++;
                }
                cur->right = new TreeNode(preorder[i]);
                st.push(cur->right);
            }
            
        }
        return root;
    }
};

方法四国外大佬的解法:

1.变量 prepos 保存当前要构造的树的 root
2.变量 inpos 保存 inorder 数组中可以成为 root 的数字们的开头那个
3.对于当前要构造的树,有一个停止点 stop ,inorder 数组中第 inpos 项到第 stop 项是要构造的树的节点值们
每次递归调用,都会确定出一个停止点,它告诉了子调用在哪里停止,把自己的根节点值作为左子树调用的停止点,自己的(父调用给下来的)停止点作为右子树的停止点

lass Solution {
public:
    TreeNode* buildTree(vector& preorder, vector& inorder) {
        int inPos = 0;
        int prePos = 0;
        return build(preorder, inorder, INT_MIN, inPos, prePos);
    }
    TreeNode* build(vector& preorder, vector& inorder, int stop, int& inPos, int& prePos){
        if(prePos >= preorder.size()) return NULL;
        if(inorder[inPos] == stop) {inPos++; return NULL;}
        TreeNode* node = new TreeNode(preorder[prePos]);
        prePos++;
        node->left = build(preorder, inorder, node->val, inPos, prePos);
        node->right = build(preorder, inorder, stop , inPos, prePos);
        return node;
    }
};

2.从中序与后序遍历序列构造二叉树

对对应letecode链接:

https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/

题目描述:

根据一棵树的中序遍历与后序遍历构造二叉树。

注意:
你可以假设树中没有重复的元素。

例如,给出

中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7

 解题思路:

1.首先在后序遍历序列中找到根节点(最后一个元素)
2.根据根节点在中序遍历序列中找到根节点的位置
3.根据根节点的位置将中序遍历序列分为左子树和右子树
4.根据根节点的位置确定左子树和右子树在中序数组和后续数组中的左右边界位置
5.递归构造左子树和右子树
6.返回根节点结束

7.关键是算出左子树和右子树的长度

由前序遍历和中序遍历还原二叉树_第7张图片

 对应代码:


class Solution {
public:
                TreeNode*buildTreeHelper(vector&inorder,int inleft,int inright,vector&postorder,int postleft,int postright){
                    if(postleft==postright)return nullptr;
                    TreeNode*root=new TreeNode(postorder[postright-1]);
                         int index=0;//记录下标
                    for(int i=inleft;ileft=buildTreeHelper(inorder,inleft,index,postorder,postleft,postright-inright+index);
            //构建他的右子树
   root->right=buildTreeHelper(inorder,index+1,inright,postorder,postright-inright+index,postright-1);
                              return root;
                }
    TreeNode* buildTree(vector& inorder, vector& postorder) {
         return buildTreeHelper(inorder,0,inorder.size(),postorder,0,postorder.size());
    }
};

同样我们也可以使用和第一题一样的方法拷贝数组:

由前序遍历和中序遍历还原二叉树_第8张图片

对应代码:

class Solution {
public:
    TreeNode* buildTree(vector& inorder, vector& postorder) {
                         if(inorder.size()==0)return nullptr;
                         TreeNode*root=new TreeNode(postorder[postorder.size()-1]);
                         int index=0;
                         for(int i=0;iinleft(inorder.begin(),inorder.begin()+index);
                vectorpostleft(postorder.begin(),postorder.begin()+index);
               //构建左树 root->left=buildTree(inleft,postleft);
                vectorinriht(inorder.begin()+index+1,inorder.end());
                vectorpostright(postorder.begin()+index,postorder.end()-1);
                //构建右树root->right=buildTree(inriht,postright);
                return root;
    }
};

  当然我们还可以使用hash表来优化上述程序:

class Solution {
public:
                 unordered_maphash;//保存根节点
     TreeNode*buildTreeHelper(vector&inorder,int inleft,int inright,vector&postorder,int postleft,int postright){
                    if(postleft==postright)return nullptr;
                    TreeNode*root=new TreeNode(postorder[postright-1]);
                         int index=hash[postorder[postright-1]];
                    
                    //构建他的左子树
   root->left=buildTreeHelper(inorder,inleft,index,postorder,postleft,postright-inright+index);
            //构建他的右子树
   root->right=buildTreeHelper(inorder,index+1,inright,postorder,postright-inright+index,postright-1);
                              return root;
                }
    TreeNode* buildTree(vector& inorder, vector& postorder) {
                   for(int i=0;i

非递归:

迭代法是一种非常巧妙的实现方法。迭代法的实现基于以下两点发现。

如果将中序遍历反序,则得到反向的中序遍历,即每次遍历右孩子,再遍历根节点,最后遍历左孩子。
如果将后序遍历反序,则得到反向的前序遍历,即每次遍历根节点,再遍历右孩子,最后遍历左孩子。

反向」的意思是交换遍历左孩子和右孩子的顺序,即反向的遍历中,右孩子在左孩子之前被遍历。

因此可以使用和「105. 从前序与中序遍历序列构造二叉树」的迭代方法类似的方法构造二叉树。

对于后序遍历中的任意两个连续节点 u 和 v(在后序遍历中,u在 v的前面),根据后序遍历的流程,我们可以知道 u 和 v 只有两种可能的关系:

u 是 v 的右儿子。这是因为在遍历到 uu 之后,下一个遍历的节点就是 uu的双亲节点,即 v;

v 没有右儿子,并且 u 是 v 的某个祖先节点(或者 vv 本身)的左儿子。如果 vv 没有右儿子,那么上一个遍历的节点就是 vv 的左儿子。如果 vv 没有左儿子,则从 v 开始向上遍历 v 的祖先节点,直到遇到一个有左儿子(且 v 不在它的左儿子的子树中)的节点 v_a,那么 u就是 v a的左儿子。

算法总结:

我们用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(后序遍历的最后一个节点),指针指向中序遍历的最后一个节点;

我们依次枚举后序遍历中除了第一个节点以外的每个节点。如果 index 恰好指向栈顶节点,那么我们不断地弹出栈顶节点并向左移动 index,并将当前节点作为最后一个弹出的节点的左儿子;如果 index 和栈顶节点不同,我们将当前节点作为栈顶节点的右儿子;

无论是哪一种情况,我们最后都将当前的节点入栈。

 

class Solution {
public:
    TreeNode* buildTree(vector& inorder, vector& postorder) {
        if (postorder.size() == 0) {
            return nullptr;
        }
        auto root = new TreeNode(postorder[postorder.size() - 1]);
        auto s = stack();
        s.push(root);
        int inorderIndex = inorder.size() - 1;
        for (int i = int(postorder.size()) - 2; i >= 0; i--) {
            int postorderVal = postorder[i];
            auto node = s.top();
            if (node->val != inorder[inorderIndex]) {
                node->right = new TreeNode(postorderVal);
                s.push(node->right);
            } else {
                while (!s.empty() && s.top()->val == inorder[inorderIndex]) {
                    node = s.top();
                    s.pop();
                    inorderIndex--;
                }
                node->left = new TreeNode(postorderVal);
                s.push(node->left);
            }
        }
        return root;
    }
};

 根据前序遍历和后序遍历还原二叉树

对应letecode链接:

https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/

题目描述:

返回与给定的前序和后序遍历匹配的任何二叉树。

 pre 和 post 遍历中的值是不同的正整数。

示例:

输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]

提示:

1 <= pre.length == post.length <= 30
pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列
每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。

由前序遍历和中序遍历还原二叉树_第9张图片

前+后
首先我们可以显然知道当前根节点为pre[pre_start],并且它在后序中的位置为post_end,因此这里我们需要找到能区分左右子树的节点。
我们知道左子树的根节点为pre[pre_start+1],因此只要找到它在后序中的位置就可以分开左右子树(index的含义)
前+中
首先我们可以显然知道当前根节点为pre[pre_start],只用找出它在中序中的位置,就可以把左右子树分开(index的含义)
中+后
首先我们可以显然知道当前根节点为post[post_end],只用找出它在中序中的位置,就可以把左右子树分开(index的含义)

这一部分运用了一个技巧是 “两种遍历中,同一子树的节点数目是相同的”
需要说明的是在"前+后","前+中"我们都运用到了“右子树起始位置为左子树终止位置+1”,其实这个也可以运用上面这个技巧来计算出起始位置

前+后
后序遍历中,我们知道 左子树:[post_start,index], 右子树:[index+1, post_end-1]
在前序遍历中,左子树起始位置为pre_start+1,左子树个数一共有(index - post_start)个,因此左子树:[pre_start+1, pre_start+1 + (index - post_start)]
右子树起始位置为左子树终止位置+1,终止位置为pre_end,因此右子树:[ pre_start+1 + (index - post_start) + 1, pre_end]
前+中
中序遍历中,我们知道 左子树:[inorder_start,index-1], 右子树:[index+1, inorder_end]
在前序遍历中,左子树起始位置为pre_start+1,左子树一共有(index-1 - inorder_start)个,因此左子树:[pre_start+1, pre_start+1 + (index-1 - inorder_start)]
右子树起始位置为左子树终止位置+1,终止位置为pre_end,因此右子树:[ pre_start+1 + (index-1 - inorder_start) + 1, pre_end]
中+后
中序遍历中,我们知道 左子树:[inorder_start,index-1], 右子树:[index+1, inorder_end]
在后序遍历中,左子树起始位置为post_start,左子树一共有(index-1 - inorder_start)个,因此左子树:[post_start, post_start + (index-1 - inorder_start)]
右子树的终止位置为post_end - 1,右子树一共有(inorder_end - (index+1))个,因此右子树:[post_end - 1 - (inorder_end - (index+1)), post_end - 1]

 对应代码:

class Solution {
public:
    TreeNode* constructFromPrePost(vector& pre, vector& post) {
        return helper(pre, post, 0, pre.size() - 1, 0, post.size() - 1);
    }

    TreeNode* helper(vector& pre, vector& post, int prestart, int preend, int poststart, int postend) {
        if (prestart > preend) {
            return NULL;
        }
        TreeNode* root = new TreeNode(pre[prestart]);
        if (prestart == preend) { //只有一个,直接返回
            return root;
        }
        int i = poststart; 
        while (i < postend && post[i] != pre[prestart + 1]) { //确定分界点,左子树的根节点
            i++; 
        }
        int len = i - poststart + 1; //左子树长度
        root->left = helper(pre, post, prestart + 1, prestart + len, poststart, i);
        root->right = helper(pre, post, prestart + 1 + len, preend, i + 1, postend - 1);
        return root;
    }
};

前序遍历构造二叉搜索树

对应letecode链接:

https://leetcode-cn.com/problems/construct-binary-search-tree-from-preorder-traversal/

题目描述:

返回与给定前序遍历 preorder 相匹配的二叉搜索树(binary search tree)的根结点。

(回想一下,二叉搜索树是二叉树的一种,其每个节点都满足以下规则,对于 node.left 的任何后代,值总 < node.val,而 node.right 的任何后代,值总 > node.val。此外,前序遍历首先显示节点 node 的值,然后遍历 node.left,接着遍历 node.right。)

题目保证,对于给定的测试用例,总能找到满足要求的二叉搜索树。

示例:

输入:[8,5,1,7,10,12]
输出:[8,5,10,1,7,null,12]

提示:

1 <= preorder.length <= 100
1 <= preorder[i] <= 10^8
preorder 中的值互不相同

 解题思路:

二分构造:

们知道输入的数据是二叉树的先序遍历,那么第一个节点肯定是头结点,比他小的是他左子树的节点值,比他大的是他右子树的节点值,我们就拿上面的[8,5,1,7,10,12]来说,8是根节点,比8小的[5,1,7]是他左子树上的值,比他大的[10,12]是他右子树上的值。所以可以参照二分法查找的方式,把数组分为两部分,他是这样的

由前序遍历和中序遍历还原二叉树_第10张图片

然后左边的[5,1,7]我们再按照上面的方式拆分,5是根节点,比5小的1是左子节点,比5大的7是右子节点。同理右边的[10,12]中10是根节点,比10大的12是右子节点,这样我们一直拆分下去,直到不能拆分为止,所以结果是下面这样

由前序遍历和中序遍历还原二叉树_第11张图片

 对应代码:


class Solution {
public:
    TreeNode* bstFromPreorder(vector& preorder) {
                return buildBSTree(preorder,0,preorder.size()-1);
    }
    TreeNode*buildBSTree(vector&preorder,int left,int right){
                  if(left>right)return nullptr;//区间不存在
                TreeNode*root=new TreeNode(preorder[left]);
                 if(left==right)//无法在分
                   return root;

                   int i = left;
        //拆分为两部分,一部分是比preorder[left]大的,一部分是比preorder[left]小的
        while (i + 1 <= right && preorder[i + 1] < preorder[left])
            i++;
        //区间[left + 1,i]所有元素都在root节点的左子树
        //区间[i + 1,right]所有元素都在root节点的右子树
        root->left = buildBSTree(preorder, left + 1, i);
        root->right = buildBSTree(preorder, i + 1, right);
        return root;
    }
};

题解法比较多,再来看最后一种解题思路。我们还可以使用一个栈来维护二叉搜索树中的节点,栈中存放的是已经构建好的二叉搜索树的结点(但不是全部,有些可能已经出栈了),其中栈中元素从栈底到栈顶是递减的,我们遍历数组的时候如果当前值小于栈顶元素的值,我们直接让当前值成为栈顶元素节点的左子节点,然后压栈。如果当前元素的值大于栈顶元素的值,我们就让栈顶元素出栈,直到当前元素的值小于栈顶元素的值为止(或者栈为空为止)。而前一个比当前元素值小的节点就是当前元素的父节点。而当前元素是他父节点的右子节点。

解惑:

这里如果思路不是很清晰的可能会有点疑问,出栈的时候把小于当前元素的值出栈了,如果再遇到比出栈的元素还要小的值那不是完蛋了,因为那个值已经出栈了,找不到了。其实有这个想法是正确的,但这种想法有点多余了,我们就拿下面的图来说吧[8,5,1,7,10,12]

由前序遍历和中序遍历还原二叉树_第12张图片

比如当我们插入节点7的时候,节点1,5都已经全部出栈,但7后面无论如何都不会再出现比1或者5还小的值了,因为他是二叉搜索树,5的右节点的所有值都是比5大的。我们来画个简单的图看下

由前序遍历和中序遍历还原二叉树_第13张图片

 

                                   

由前序遍历和中序遍历还原二叉树_第14张图片

 

所以我们看到后面无论走到哪一步都不可能在遇到比出栈元素更小的值了,最后我们再来看下完整代码 

 

class Solution {
public:
    TreeNode* bstFromPreorder(vector& preorder) {
                    stackstk;
                    TreeNode*root=new TreeNode(preorder[0]);
                    stk.push(root);
                    for(int i=1;ival>node->val){
                                    stk.top()->left=node;
                            }
                            else{
                       TreeNode *parent = stk.top();
                //栈从栈底到栈顶是递减的
                while (!stk.empty() && preorder[i] > stk.top()->val) {
                          parent = stk.top();
                          stk.pop();
                     }
                parent->right = node;
                            }
                          stk.push(node);  
                    }  
                    return root;
    }
};

本文来自大佬的题解和自己的一些理解:

觉得有帮助的铁子可以点个赞

 

你可能感兴趣的:(leetcode,算法,数据结构)