LeetCode 105.根据前序和中序遍历构造二叉树(递归+迭代)

文章目录

      • 递归解法
      • 迭代解法
      • follow up 889.
      • 106.根据中序和后序遍历构造二叉树

前序遍历是按照: [根节点 - 左子树 - 右子树] 的顺序访问节点的

中序遍历是按照:[左子树 - 根节点 - 右子树] 的顺序访问节点的

我们可以先根据前序遍历序列中的第一个元素,找到根节点,然后在中序遍历序列中,定位到根节点的位置,然后把中序遍历的区间,从根节点的位置,一分为二,左侧是左子树的全部节点,右侧是右子树的全部节点,这样我们就得到了左子树节点的数量,和右子树节点的数量。再把这个信息带回到前序遍历的序列中,就能确定左子树的区间,和右子树的区间。然后我们分别对每个区间,进行递归处理即可。

递归解法

那么,定义这样一个递归函数:

TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir);

这个函数,指定了前序遍历序列的区间起止位置,指定了中序遍历序列的区间起止位置。我们可以保证两个遍历序列的区间长度是相同的。那么递归退出的条件是区间长度为0。

容易写出如下的代码

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }
    
    private TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir) {
        if (pl > pr) return null; // reach end
        int rootVal = preorder[pl]; // 前序遍历的第一个元素为根节点
        int k = il; // 从中序遍历的序列中, 定位到根节点的下标
        while (k <= ir) {
            if (inorder[k] == rootVal) break;
            k++;
        }
        int leftLen = k - il; // 左子树的节点数量
        int rightLen = ir - k; // 右子树的节点数量
        
        TreeNode root = new TreeNode(rootVal);
        root.left = build(preorder, pl + 1, pl + leftLen, inorder, il, k - 1); // 递归构建
        root.right = build(preorder, pl + leftLen + 1, pr, inorder, k + 1, ir); // 递归构建
        return root;
    }
}

哈希表优化

观察可知,性能开销主要在于:在中序遍历序列中定位根节点,我们使用的是遍历的方式。

在极端情况下,比如二叉树退化成一个链表,每个节点都只含有左儿子。此时,前序遍历序列和中序遍历序列,恰好是逆序的关系。每次在中序遍历序列中定位根节点,都要走最远的距离,时间复杂度 O ( n 2 ) O(n^2) O(n2)

我们可以引入一个哈希表,将中序遍历序列中的值和下标的映射,存储起来,这样在中序遍历序列中定位根节点,只需要 O ( 1 ) O(1) O(1) 的时间复杂度。

class Solution {
    
    Map<Integer, Integer> index = new HashMap<>();
    
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        
        for (int i = 0; i < inorder.length; i++) index.put(inorder[i], i);
        
        return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
    }
    
    private TreeNode build(int[] preorder, int pl, int pr, int[] inorder, int il, int ir) {
        if (pl > pr) return null;
        int rootVal = preorder[pl];
        int k = index.get(rootVal);
        int leftLen = k - il;
        TreeNode root = new TreeNode(rootVal);
        root.left = build(preorder, pl + 1, pl + leftLen, inorder, il, k - 1);
        root.right = build(preorder, pl + leftLen + 1, pr, inorder, k + 1, ir);      
        return root;
    }
    
}

引入哈希表,虽然整体的时间复杂度优化为了 O ( n ) O(n) O(n),但是空间复杂度也由原先的 O ( 1 ) O(1) O(1) 上升成了 O ( n ) O(n) O(n)

最优解

下面介绍时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1) 的解法

根据国外大佬Stephan的思路, 可以通过引入一个stop变量,而不需要引入哈希表,来取得 O ( n ) O(n) O(n) 的时间复杂度,并保持 O ( 1 ) O(1) O(1) 的空间复杂度。

在介绍这种方法之前,先上一个我自己的解法。由于前序遍历总是会先访问根节点,我们可以用一个全局的指针p,结合前序遍历序列,来获取当前的根节点。每次都通过这个指针p来建立节点

class Solution {
    
    int p = 0; // 当前根节点的位置
    
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, inorder, 0, inorder.length - 1);
    }
    
    // [l, r] 仅仅是 中序遍历的区间范围
    private TreeNode build(int[] preorder, int[] inorder, int l, int r) {
        if (l > r) return null; // reach end
        int rootVal = preorder[p++]; // 取得当前根节点, 并将p后移
        int k = l; // 在中序遍历中定位根节点
        while (k <= r) {
            if (inorder[k] == rootVal) break;
            k++;
        }
        TreeNode root = new TreeNode(rootVal);
        // 由于是先递归建立左子树, 那么建立左子树时, 根据前序遍历取到的根节点, 就是左子树的根节点
        root.left = build(preorder, inorder, l, k - 1);
        root.right = build(preorder, inorder, k + 1, r);
        return root;
    }
}

接下来介绍Stephan大佬的解法,使用两个指针pi,分别用来指示前序遍历序列当前访问的位置,和中序遍历序列当前访问的位置。并新增一个stop变量,用来指示中序遍历的边界。原贴 点这里,也可以参考这篇 图解。

class Solution {
    int p = 0, i = 0;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        // 初始时, 传入一个树中不存在的节点, 作为指示中序遍历的边界
        return build(preorder, inorder, Integer.MAX_VALUE);
    }
    
    // stop 用来指示中序遍历的停止位置
    private TreeNode build(int[] preorder, int[] inorder, int stop) {
        if (p == preorder.length) return null; // 所有节点都用完了
        if (inorder[i] == stop) { // 停止
            i++;
            return null;
        }
        int rootVal = preorder[p++];
        TreeNode root = new TreeNode(rootVal);
        // 由于对于中序遍历序列中, 是先访问左子树, 再访问根节点, 所以对左子树的构造, stop 设为根节点 
        root.left = build(preorder, inorder, rootVal); // 构建左子树时, 中序遍历的边界, 遇到根节点时停止
        // 对于右子树, 其结束的位置, 和根节点结束的位置相同, 所以将原本的 stop 传入即可
        root.right = build(preorder, inorder, stop);
        return root;
    }
}

其实思路就是手动模拟了递归调用时,栈的情况。

稍加观察,可以发现,我们用于构造二叉树的递归函数,也是按照前序遍历的顺序,进行构造的。即,先访问并创建出了根节点,再递归的建立左子树和右子树。构造的方式,和前序遍历保持一致,即可保证每一次根据指针p都能取到当前位置的根节点。

整个过程大概就是,先从根节点一头扎下去,一直往左走,走到整棵树最左侧的节点。(其实这也是前序遍历的访问顺序)

当一个节点不存在左儿子时,从这个节点开始的前序遍历序列,和中序遍历序列,第一个访问的都是该节点本身。这样,再对这个节点的左儿子进行递归调用时,会遇到stop,直接返回null。

前序是:根 - 左 - 右 ;中序是:左 - 根 - 右。

当左儿子为空,则前序和中序,第一个访问的节点都是根节点。

迭代解法

如果能较好的理解递归解法的最后一种,那么也就能较为容易的理解迭代的方式,其思路是非常相似的。

迭代法中,需要用一个栈,来手动模拟递归调用产生的栈,和回溯的过程。参考这篇 题解

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int p = 0, i = 0; // 同样用2个指针来指示, 前序序列的当前位置, 中序序列的当前位置
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode root = new TreeNode(preorder[p++]);
        TreeNode cur = root;
        stack.push(root);
        
        while (p < preorder.length) {
            // 一头扎下去, 扎到整棵树的最左侧节点, 并构建左儿子的关系
            while (cur.val != inorder[i]) {
                cur.left = new TreeNode(preorder[p++]);
                stack.push(cur.left);
                cur = cur.left;
            }
            
            if (p == preorder.length) break; // 已经结束, 提前break
            
            // 已经到最左侧节点了, 弹栈, 找到第一个需要拥有右儿子的节点
            while (!stack.isEmpty() && stack.peek().val == inorder[i]) {
                cur = stack.pop();
                i++;
            }
            
            // 构造其右儿子
            cur.right = new TreeNode(preorder[p++]);
            stack.push(cur.right);
            cur = cur.right;
        }
        
        return root;
    }
}

迭代的解法,其实更符合计算机的思考方式,而不符合人类大脑的思考模式。

要完整的掌握迭代的解法,需要自己画图,一步一步跟,反复模拟。才能理解整个递归和回溯的过程。反正我今天是一整天只研究了这一道题(哭

follow up 889.

根据前序和后序遍历构造二叉树

后序遍历的特点是,根节点最后访问。考虑用递归来做,核心的思路还是找到分界点,能够将数组一分为二。

class Solution {
    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        return build(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1);
    }

    private TreeNode build(int[] preorder, int il, int ir, int[] postorder, int jl, int jr) {
        if (il > ir) return null;
        int rootVal = preorder[il];
        TreeNode root = new TreeNode(rootVal);
        if (il == ir) return root; // 当前preorder是最后一个了
        // 获取当前根节点的左子树的根节点
        int leftVal = preorder[il + 1];
        int k = jl;
        while (postorder[k] != leftVal) k++;
        // 左子树的根节点, 是后序遍历中左子树最后一个访问的
        // 可以根据这个节点, 将后序遍历的数组一分为二
        int leftLen = k - jl + 1;
        root.left = build(preorder, il + 1, il + leftLen, postorder, jl, k);
        root.right = build(preorder, il + leftLen + 1, ir, postorder, k + 1, jr - 1);
        return root;
    }
}

迭代法:待补充。TODO

另:根据前序和后序,无法唯一确定一颗二叉树。

106.根据中序和后序遍历构造二叉树

除了基本的划分做法。仍然可以用105的迭代思路来做(引入stop变量)。具体的做法是:
将中序遍历序列和后序遍历序列逆序看待。
然后我们将原二叉树做一下左右镜像翻转。容易得知:
原先的中序遍历序列的逆序,为镜像翻转后的二叉树的中序遍历;
原先的后序遍历序列的逆序,为镜像翻转后的二叉树的前序遍历。
则我们仍然通过引入一个stop变量来做,但是先构造右子树,再构造左子树(镜像反转)

class Solution {
    int p = 0, i = 0;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        p = i = postorder.length - 1;
        return build(postorder, inorder, Integer.MAX_VALUE);
    }

    private TreeNode build(int[] postorder, int[] inorder, int stop) {
        if (p < 0) return null; // end
        if (inorder[i] == stop) {
            i--;
            return null;
        }
        TreeNode root = new TreeNode(postorder[p--]);
        root.right = build(postorder, inorder, root.val);
        root.left = build(postorder, inorder, stop);
        return root;
    }
}

你可能感兴趣的:(算法,leetcode,算法,数据结构,二叉树,递归)