剑指offer(第二版)——重建二叉树

PS:《剑指offer》是很多同学找工作都会参考的一本面试指南,同时也是一本算法指南(为什么它这么受欢迎,主要应该是其提供了一个循序渐进的优化解法,这点我觉得十分友好)。现在很多互联网的算法面试题基本上可以在这里找到影子,为了以后方便参考与回顾,现将书中例题用Java实现(第二版),欢迎各位同学一起交流进步。

GitHub: https://github.com/Uplpw/SwordOffer

完整题目链接: https://blog.csdn.net/qq_41866626/article/details/120415258

目录

1 题目描述

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如:先序遍历为[3,9,20,15,7],中序遍历为 [9,3,15,20,7],其对应的二叉树如下

		  3
	    /   \
	  9 	20
           /  \
         15    7

leetcode链接: 重建二叉树(以下代码已测试,提交通过)

2 测试用例

一般是考虑功能用例,特殊(边缘)用例或者是反例,无效测试用例这三种情况。甚至可以从测试用例寻找一些规律解决问题,同时也可以让我们的程序更加完整鲁棒。

(1)功能用例:完全与非完全二叉树。

(2)边缘用例:只有一个节点,只有左节点,只有右节点。

(3)无效用例:树为空,对应数组为空。

3 思路

分析:

从数据结构中可知,根据二叉树的先序与中序遍历结果可以唯一的重建二叉树(另外根据后序与中序也可以唯一的重建二叉树)。需要注意的是:前提条件是,树中的任意两个节点值不能相同

重建二叉树,最直接的想法是找到根节点,以及左右子树的节点集合,然后对每个节点不断递归即可。下面考虑如何找到根节点以及左右子树节点集合,并以上例举例(先序遍历为[3,9,20,15,7],中序遍历为 [9,3,15,20,7])。

  • 根节点:根据先序遍历的特点,第一个节点即是根节点(值为3的节点为根节点)。
  • 左右子树集合:根据中序遍历特点,根节点左边的节点集合为左子树([ 9 ]),根节点右边的节点集合为右子树([15,20,7])

下面是递归解法思路与具体过程、以及时间空间复杂度比较。

解法1:递归实现

  • 递归终止条件:当输入先序、中序遍历结果数组没有元素时,说明左/右子树构建完成,直接返回null即可。
  • 划分左右子树:根据先序遍历的第一个节点,找到中序遍历与之相同的节点,左边是左子树,右边是右子树
  • 构建根节点:根据先序遍历的第一个节点构建新节点,即当前根节点root
  • 递归构建左右子树:更新先序、中序遍历结果数组递归调用。
  • 返回值:最后回溯返回的根节点即整个二叉树的根节点。

注意: 比较简单直接地方式是每次递归左右子树时,可以新建左右子树数组传进去进行处理,但会浪费时间与空间,优化方式是使用下标以及左右子树节点数量来代替,具体细节见代码。

时间空间复杂度: O ( n ) , O ( n ) O(n),O(n) O(n),O(n)

4 代码

二叉树结构类:

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

算法实现:

public class ConstructBinaryTree {

    /**
     * @param preorder:先序遍历结果
     * @param inorder:中序遍历结果
     * @return : 构建完成的二叉树根节点
     */
    public static TreeNode buildTree(int[] preorder, int[] inorder) {
        // 无效数据判断
        if (preorder == null || inorder == null || preorder.length == 0 || inorder.length == 0 || preorder.length != inorder.length) {
            return null;
        }
        // 递归调用
        return constructCore(preorder, 0, inorder, 0, preorder.length);
    }

    /**
     * @param preorder:当前子树的先序遍历结果(实际上还是整个结果,使用preStart作为标志)
     * @param preStart:记录子树先序遍历的开始
     * @param inorder:当前子树的中序遍历结果(实际上还是整个结果,使用inStart作为标志)
     * @param inStart:记录子树中序遍历的开始
     * @param length:当前子树的节点数量
     * @return
     */
    public static TreeNode constructCore(int[] preorder, int preStart, int[] inorder, int inStart, int length) {
        // 左右子树集合为空,构建完成直接返回
        if (length == 0) {
            return null;
        }
        int index = -1;
        // 查找根节点在中序遍历中的索引位置
        for (int i = inStart; i < inStart + length; i++) {
            if (preorder[preStart] == inorder[i]) {
                index = i;
                break;
            }
        }
        // 左子树的节点数量
        int preorder_length = index - inStart;
        // 构建根节点
        TreeNode root = new TreeNode(preorder[preStart]);

        /*
            递归左子树构建
            参数preStart:由于是构建左子树,先序第一个节点是根节点,所以左子树集合开始索引需要+1
            参数inStart:由于中序遍历左边结果一定是左子树,所以不进行更新
            参数length:左子树节点数量=preorder_length
         */
        root.left = constructCore(preorder, preStart + 1, inorder, inStart, preorder_length);

        /*
            递归右子树构建
            参数preStart:由于是构建右子树,右子树集合开始的索引在 根节点开始索引+左子树节点数量+1
            参数inStart:由于中序遍历中根节点右边的数据一定是右节点,所以右子树开始索引是 index+1
            参数length:右子树节点数量=length - preorder_length - 1(当前子树节点数量-左子树节点数量-根节点)
         */
        root.right = constructCore(preorder, preStart + preorder_length + 1, inorder, index + 1,
                length - preorder_length - 1);
        return root;
    }
}

参考
在解决本书例题时,参考了一些大佬的题解,比如leetcode上的官方、K神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!

本例题参考:
https://www.jianshu.com/p/ddc50561dda5

本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!

上一篇:Nginx配置指令之listen


下一篇:Docker网络