二叉树常用算法

77 阅读23分钟

1 二叉树算法核心纲领

1.1 前言

二叉树解题的思维模式分为两类:

1、是否可以通过遍历一遍二叉树得到答案?

  • 如果可以,用一个tranverse函数配合外部变量实现,这个叫遍历的思维模式。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?

  • 写出递归函数的定义,充分利用这个函数的返回值,此之谓分解问题的思维模式。

无论使用上述的哪种思维模式,都需要思考这样的问题:

  • 对于当前这个二叉树节点,它需要做什么事情?需要在哪个位置做?(前序|中序|后续)?

1.2 深入理解前中后序

本节的开始,需要思考以下三个问题:

  • 前中后序有什么不同,仅仅是三个顺序不同的List吗?
  • 后续遍历有什么特别之处?
  • 为什么多叉树没有中序遍历呢?

二叉树的深度优先遍历框架为:

void tranverse(TreeNode root) {
    if (root == null) return;
    
    // 前序位置
    
    tranverse(root.left);
    
    // 中序位置
    
    tranverse(root.right);
    
    // 后序位置
}

只要是递归形式的遍历,都可以根据位置分为前序遍历和后序遍历,区别在于遍历的位置在递归之前还是递归之后。

从上面的代码可以看出,前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点,绝不是三个顺序不同的List

  • 前序位置在代码刚刚进入一个二叉树节点的时候执行
  • 后序位置在代码将要离开一个二叉树节点的时候执行
  • 中序位置在而发货数节点左子树都遍历完,即将开始遍历右子树的时候执行

综上所述:

二叉树所有的问题,是在前中后序合适的位置注入巧妙的代码逻辑,达到自己的目的。只需要思考每一个节点应该做什么,其他的无需考虑,抛给二叉树遍历框架,递归会在所有的节点上做相同的操作。

1.3 两种解题思路

前言中所言,二叉树问题可以分为两种思路:遍历分解问题

这里我们从LeetCode第104题[二叉树最大深度]入手,分析解决二叉树问题的思维方式。

分解问题的解法如下:

public int maxDepth(TreeNode root) {
    if (root == null) return 0;

    // 获取左子树的最大深度
    int maxLeft = maxDepth(root.left);

    // 获取右子树的最大深度
    int maxRight = maxDepth(root.right);

    // 当前子树的深度为
    return Math.max(maxLeft, maxRight) + 1;
}

这是我们最容易想到的方法,通过分解问题,当前子树的深度为左右子树的最大深度加上本节点的占用的深度1,即

cur=max(left,right)+1cur = \max(left, right) + 1

遍历解法

遍历解法的思路为:

  • 遍历整颗二叉树,记录每个节点离根节点的距离
  • 遍历到叶子结点,就更新最大深度的值
class Solution {
    // 记录最大深度
    int maxDepth;
    
    // 记录当前节点所在的深度
    int curDepth = 0;
    
    public int maxDepth(TreeNode root) {
        traverse(root);
        return maxDepth;
    }
    
    void traverse(TreeNode root) {
        if (root == null) {
            return;
        }
        
        // 前序位置
        curDepth++;
        
        if (root.left == null && root.right == null) {
            // 到达了叶子节点,更新最大深度
            maxDepth = Math.max(maxDepth, curDepth);
        }
        
        traverse(root.left);
        traverse(root.right);
        
        curDepth--; // 后序位置
    }
}

对比以上两种解法我们发现:

  • 遍历的方式,从根节点出发,遍历到叶子结点之后,更新树的最大深度,这种解法逻辑在前序位置
  • 分解的方式,从叶子节点开始,叶子节点的深度为0,一层一层得到结果,直到根节点,这种写法的逻辑在后续位置

通过上述分析,LeetCode144题[二叉树前序遍历]的问题也迎刃而解。

对于二叉树的前序遍历,我们很容易通过遍历的方式得到答案,但是如果使用问题分解的方式,如何去解题呢?

这里回想一下前序遍历的特点?先是根节点,接着是左子树的前序遍历,接着是右子树的遍历方式。

如此一来,就可以得到问题的分解方法:前序遍历结果 = 根节点 + 左子树遍历结果 + 右子树遍历结果

实现代码为:

List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;
    
    // 前序遍历的结果
    res.add(root.val);
    
    // 左子树前序遍历的结果
    res.addAll(preorderTraversal(root.left));
    
    // 右子树遍历的结果
    res.addAll(preorderTraversal(root.right));
    
    return res;
}

1.4 后序位置的特殊之处

通过对二叉树的已有了解,我们可以发现:

前中后序位置的代码,其所掌握的信息量依次增大

  • 前序位置只能获得来自父节点传递而来的数据
  • 中序位置不仅获得父节点的数据,还能获得左子树通过函数返回值传递回来的数据
  • 后序位置可以获得父节点的信息,也能获得左右两个子节点的信息

一旦发现题目和子树有关,那么大概率要给函数设置合理的定义和返回值,在后续位置写代码。

根据上面的结论,分析一下LeetCode第543题[二叉树的直径]

该题的具体思路为:

  • 获取左右子树的深度
  • 两个子树相加,即为当前节点的直径,判断是否需要更新最大直径

1.5 以树的视角看动态规划|回溯|DFS算法的区别和联系

动态规划|DFS|回溯算法都可以看作是二叉树问题的扩展,只是他们关注的重点不同

  • 动态规划:属于分解问题的思路,他的关注点在于整棵子树
  • 回溯算法:遍历,关注点在于节点之间的树枝
  • DFS算法:遍历,关注点在于单个节点

1.5.1 分解问题的思想

一棵二叉树,如何计算出它总有多少个节点呢?

使用分解问题的思想就是,当前节点的数量 = 左子树节点数量 + 右子树节点数量 + 1

代码为:

int count(TreeNode root) {
    if (root == null) return 0;
    
    // 左子树的节点数量
    int leftCounts = count(root.left);
    
    // 右子树的节点数量
    int rightCounts = count(root.right);
    
    // 后序位置
    return leftCount + rightCount + 1;
}

动态规划分解问题的思路,着眼点为结构相同的子问题上,类比到二叉树就是子树

1.5.2 回溯算法|DFS的思想

// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
void dfs(Node root) {
    if (root == null) return;
    // 做选择
    print("我已经进入节点 %s 啦", root);
    for (Node child : root.children) {
        dfs(child);
    }
    // 撤销选择
    print("我将要离开节点 %s 啦", root);
}

// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
void backtrack(Node root) {
    if (root == null) return;
    for (Node child : root.children) {
        // 做选择
        print("我站在节点 %s 到节点 %s 的树枝上", root, child);
        backtrack(child);
        // 撤销选择
        print("我将要离开节点 %s 到节点 %s 的树枝上", child, root);
    }
}

1.6 层序遍历

层序遍历利用了队列,使用迭代遍历实现,较为简单。

void levelTraverse(TreeNode root) {
	if (root == null) {
        return;
    }
    
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    
    while (!q.isEmpty) {
        int sz = q.size();
        
        // 从左到右遍历每一个子节点
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            // 将下一层放入队列
            if (cur.left != null) {
                q.offer(cur.left);
            }
            
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
    }
}

2 二叉树构造问题

二叉树二造问题一般都是使用【分解问题】的思路:构造整棵二叉树 = 根节点 + 构造左子树 + 构造右子树

2.1 构造最大二叉树

题目描述

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的 *最大二叉树*

示例

image-20241021095455264

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
        - 空数组,无子节点。
        - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
            - 空数组,无子节点。
            - 只有一个元素,所以子节点是一个值为 1 的节点。
    - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
        - 只有一个元素,所以子节点是一个值为 0 的节点。
        - 空数组,无子节点。

我的思路

  • 根据示例中的描述,过程很像快排
  • 递归函数为TreeNode construct(nums, start, end)
    • 递归过程中找到当前节点的最大值,新建节点为cur
    • 递归找到左子节点
    • 递归创建右子节点
    • 当前节点的左指针指向左节点,右指针指向右节点

代码见T654-最大二叉树

2.2 通过前序和中序遍历构造二叉树

题目描述

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例

image-20241021101248536

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

我的思路

  • 题目说inorder中元素是无重复的,我们可以将其值和索引存储在map
  • 遍历preorder中的每个元素,从inorder中寻找该元素的位置,然后将inorder划分为两个子数组
  • 递归地从左子数组找到构件好的左子树
  • 从右子数组中找到构建好的右子树
  • 构建当前节点的左右子树
  • 返回

这道题就是考察对前序遍历和中序遍历中,根节点,左子节点范围和右子节点范围在两个数组中位置的把控,通过画图可以得到三部分在前序数组和中序数组的具体位置,这样就可以递归构建了。

代码见T105-从前序和中序中遍历序列构造二叉树

2.3 通过后序中序遍历结果构造二叉树

题目描述

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

我的思路

  • 整体思路和2.2节相似,也是将中序和后序数组分为三个部分
    • 当前节点
    • 左子节点部分
    • 右子节点部分

题解略

2.4 通过前序遍历和后序遍历构造二叉树

题目描述

给定两个整数数组,preorderpostorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。

如果存在多个答案,您可以返回其中 任何 一个。

解题思路

通过前序中序、中序后序可以唯一确定二叉树,这是因为可以通过前序|后序确定根节点,通过中序确定左右子树的节点,但是通过前序后序无法确定唯一的原始二叉树。

但是解题思路是相似的,都是通过寻找左右子树的索引范围来构建的:

  • 将前序遍历结果的第一个值,作为树的根节点
  • 将前序遍历的第二个值,作为左子树的根节点的值
  • 在后续遍历中,找到左子树根节点的值,确定左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可

为什么通过前序|后序遍历无法唯一确定一颗二叉树呢?

关键在于int leftRootVal = preorder[preStart + 1]

说不定根本就没有左根节点,但是我们还是将它当做是左节点了。

3 搜索树特性

3.1 前言

二叉搜索树(Binary Search Tree,BST)的特性:

  • 对于BST的每一个节点node,左子树的节点的值都要比node值小,右子树节点的值都要比node值大
  • 对于BST的每个节点node,他的左侧子树和右子树都是BST
  • BST的中序遍历结果是有序的(升序)

3.2 寻找第K小的元素

见LeetCode第230题,[二叉搜索树中第K小的元素]

题目描述

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

示例

image-20241021213038362

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

我的思路

  • 前文讲到,BST的中序遍历是一个升序数组
  • 初始化两个实例变量,int count = 0, target = k, res = -1;
  • 中序遍历树,每当遍历一个子节点,count++
  • 如果count == target,则res = cur.val;

3.3 BST转换为累加树

见LeetCode第538题[累加树]

题目描述

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

  • 节点的左子树仅包含键 小于 节点键的节点。
  • 节点的右子树仅包含键 大于 节点键的节点。
  • 左右子树也必须是二叉搜索树。

示例

image-20241021215320483

输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]

我的思路

  • 维护一个实例变量curSum
  • 中序遍历:右孩子->根节点->左孩子
  • 更新实例变量curSum的值curSum += root.val
  • 更新当前节点的值:root.val = curSum

4 搜索树基本操作

上节主要介绍了BST的一些基本特性,并利用中序遍历的有序性解决了几个问题。本节主要介绍BST的一些基本操作:判断BST的合法性、增删查。

BST的基础操作主要依赖于【左小右大】的特性,可以在二叉树做类似于二分搜索的操作,寻找一个元素的效率很高。

在BST中搜索某个元素的基本逻辑为:

void BST(TreeNode root, int target) {
    if (root.val == target) {
        // do something
    } else if (root.val < target) {
        // 搜索右子树
        BST(root.right, target);
    } else if (root.val > target) {
        // 搜索左子树
        BST(root.left, target);
    }
}

4.1 判断BST的合法性

见LeetCode第91题[验证二叉搜索树]

题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

我的思路

  • BST的中序遍历是个升序数组
  • 可以遍历树结构,找出中序遍历
  • 然后判断中序遍历是否是单调递增的(必要条件)

思路二

  • BST的是哪个条件为:
    • 当前节点的值大于左子树的所有值,即root.val > leftMax
    • 当前节点的值小于右子树的所有制,即root.val < rightMin
    • 并且所有的右子树和左子树都是二叉搜索树

思路二不合理之处在于,如果求leftMax,和rightMin,需要从树的底层开始求得,但是树的递归遍历是从顶层开始的,这样就很复杂,如果自顶向下该如何求解呢?

可以给左子树设定一个上界,即root.val,右子树值的下界也是root.val,在递归遍历的时候,传入两个界值,分别判断左右子树是否满足这两个界值

  • 定义递归遍历函数为valid(TreeNode root, TreeNode min, TreeNode max)
  • 返回条件为root == null return true
  • BST的三个必要条件:
    • root.val > min.val
    • root.val < max.val
    • valid(root.left) && valid(root.right)

实现代码见[T98-验证搜索二叉树]

4.2 在BST中搜索元素

见LeetCode第700题[二叉搜索树中搜索元素]

题目描述

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

我的思路

  • 根据二叉搜索树的特性
    • if root.val > target then search(root.left)
    • if root.val < target then search(root.right)
    • if root.val == target then return root
    • if root == null then return null

4.3 在BST中插入一个数

我们知道,对于数据结构的操作就是遍历访问,遍历就是查询,而访问 就是修改。想要往某个数据结构中插入一条数据,就是要先找到插入的位置,然后进行插入操作。

涉及到二叉树的修改,就类似于二叉树的构造问题,方法的返回值TreeNode类型,并且需要对递归调用的返回值进行接收

见LeetCode第701题[二叉搜索树的插入操作]

题目描述

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

示例

image-20241022110308352

我的思路

  • 根据BST的特性,寻找插入的位置
  • if root.val > val
    • if root.left == null then root.left = val
    • if root.left != null then search(root.left)
  • if root.val < val
    • if root.right == null then root.right = val
    • if root.left != null then search(root.left)

4.4 在BST中删除目标节点

见LeetCode第450题[删除二叉搜索树中的节点]

题目描述

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

示例

image-20241022112916428

我的思路

  • 想要完成删除操作,我们需要找到四个节点:目标节点,父节点,左孩子和右孩子
  • 左右孩子都是BST,需要将两个孩子合并为一棵BST
  • 判断子树BST的根节点值和父节点的值的大小,来确定父节点的左指针还是右指针指向孩子

上述思路不合理的地方在于:

  • 如果当前节点不是目标节点,应当怎么做?
    • 根据当前节点的和目标节点的大小,从孩子节点中删除,然后返回当前节点curNode
  • 如果当前节点是目标节点,返回值应当是什么?
    • parentNode == null,表示当前节点是根节点,返回值为child
    • parentNode != null,将parentNode -> child,返回parentNode

具体代码实现见:[T450-删除二叉搜索树的节点]

二叉树习题

T104-二叉树最大深度

题目描述

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

我的思路

  • 采用递归的方式:
    • 先获取左子树的最大深度maxLeft
    • 在获取右子树的最大深度maxRight
  • 在后序位置上,当前节点的最大深度为Math.max(maxLeft, maxRight) + 1

T144-二叉树的前序遍历

题目描述

给你二叉树的根节点 root ,返回它节点值的 前序 遍历

我的思路

  • 牢记三种遍历方式,代码逻辑位置

T543-二叉树的直径

题目描述

给你一棵二叉树的根节点,返回该树的 直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

示例

image-20241020170533215

输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3][5,2,1,3] 的长度。

我的思路

  • 通过分析可以发现,所谓的直径,就是左右两个子树的深度之和
  • 可以采用分解问题的方式,分别求出左右两个子树的最大深度
  • 然后求得树的直径,更新当前节点的最大深度

T226-反转二叉树

题目描述

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例

image-20241020212710389

我的思路

  • 使用分解问题的方法:
    • 先得到反转后的左子树和右子树
    • 然后将父节点的左指针和右指针分别指向右子树和左子树
  • 返回父节点

T116-填充每个二叉树节点的右侧指针

题目描述

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

示例

image-20241020213644842

我的思路

  • 这一题很像二叉树的层序遍历
  • 将每一层的节点放在队列尾部
  • 记录当前队列的size
  • 如果i == size说明到达队尾,node.next = null
  • 否则的话为cur.next = q.peek()

递归思路

  • 站在cur节点上,我们应当考虑什么?该该怎么做?
  • cur是一个非叶子节点
    • cur.left.next -> cur.right
    • cur.right.next -> cur.next.left || null

T114-二叉树展开为链表

题目描述

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例

image-20241020221330173

前序遍历思路

  • 通过前序遍历,将前序遍历的节点存储到队列中
  • 然后根据节点重新构建单链表

T654-最大二叉树

实现代码

/**
 * 创建最大二叉树
 * @param nums
 * @return
 */
public TreeNode constructMaximumBinaryTree(int[] nums) {
    
    return construct(nums, 0, nums.length - 1);

}

/**
 * 递归构建最大子树
 * @param nums
 * @param start
 * @param end
 * @return
 */
private TreeNode construct(int[] nums, int start, int end) {
    
    if (start < end) return null;
    
    // 寻找当前范围最大值
    int maxVal = Integer.MIN_VALUE;
    int maxIndex = -1;

    for (int i = start; i <= end; i++) {
        if (nums[i] > maxVal) {
            maxVal = nums[i];
            maxIndex = i;
        }
    }
    
    // 创建当前节点
    TreeNode cur = new TreeNode(maxVal);
    
    // 递归构建左子树
    TreeNode left = construct(nums, start, maxIndex - 1);
    // 递归构建右子树
    TreeNode right = construct(nums, maxIndex + 1, end);
    
    cur.left = left;
    cur.right = right;
    
    return cur;
    
}

T105-从前序和中序中遍历序列构造二叉树

代码实现

Map<Integer, Integer> mapIn = new HashMap<>();


/**
 * 从前序和中序中构建树
 * @param preorder
 * @param inorder
 * @return
 */
public TreeNode buildTree(int[] preorder, int[] inorder) {

    // 将中序遍历的值和索引存储到inorder中
    for (int i = 0; i < inorder.length; i++) {
        mapIn.put(inorder[i], i);
    }

    int preStart = 0;
    int preEnd = preorder.length - 1;
    int inStart = 0;
    int inEnd = inorder.length - 1;
    return construct(preorder, preStart, preEnd,  inorder, inStart, inEnd);

}

private TreeNode construct(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {

    if (preStart > preEnd) return null;

    // 创建根节点
    int rootVal = preorder[preStart];
    TreeNode root = new TreeNode(rootVal);
    // 创建左子树的根节点
    // 左子树在中序遍历中的范围为
    int rootIndex = mapIn.get(rootVal);
    int lInStart = inStart;
    int lInEnd = rootIndex - 1;
    int sz = lInEnd >= lInStart ? lInEnd - lInStart + 1 : 0; // 左子树节点的个数
    // 左子树在前序遍历的范围为
    int lPreStart = preStart + 1;
    int lPreEnd = preStart + sz;
    // 获取左子树的根节点
    TreeNode leftNode = construct(preorder, lPreStart, lPreEnd, inorder, lInStart, lInEnd);

    // 同理,右子树在中序遍历的节点范围为
    int rInStart = rootIndex + 1;
    int rInEnd = inEnd;
    int rPreStart = preStart + sz + 1;
    int rPreEnd = preEnd;
    TreeNode rightNode = construct(preorder, rPreStart, rPreEnd, inorder, rInStart, rInEnd);

    root.left = leftNode;
    root.right = rightNode;


    return root;
}

T889-前序|后序构建二叉树

实现代码

HashMap<Integer, Integer> postMap = new HashMap<>();
/**
 * 根据前序遍历和后序遍历构造二叉树
 * @param preorder
 * @param postorder
 * @return
 */
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
    // 对postMap赋值
    for (int i = 0; i < postorder.length; i++) {
        postMap.put(postorder[i], i);
    }

    int preStart = 0;
    int preEnd = preorder.length - 1;
    int postStart = 0;
    int postEnd = postorder.length - 1;

    return build(preorder, preStart, preEnd, postorder, postStart, postEnd);
}

/**
 * 从前序遍历和后序遍历递归构建左右子树
 * @param preorder
 * @param preStart
 * @param preEnd
 * @param postorder
 * @param postStart
 * @param postEnd
 * @return
 */
private TreeNode build(int[] preorder, int preStart, int preEnd, int[] postorder, int postStart, int postEnd) {
    if (preStart > preEnd) return null;

    // 创建根节点
    int rootVal = preorder[preStart];
    TreeNode root = new TreeNode(rootVal);

    if (preStart == preEnd) return root;

    // 假定前序遍历的下一个为左子树的根节点
    int lRootVal = preorder[preStart + 1];
    int lPostEnd = postMap.get(lRootVal); // 获取左根节点在后序遍历中的位置
    // 找到左子树在后序遍历中的范围
    int lPostStart = postStart;
    int counts = lPostEnd - lPostStart + 1;
    int lPreStart = preStart + 1;
    int lPreEnd = preStart + counts;

    // 获取左子树
    TreeNode leftNode = build(preorder, lPreStart, lPreEnd, postorder, lPostStart, lPostEnd);

    // 找到右子树节点的范围
    int rPreStart = lPreEnd + 1;
    int rPreEnd = preEnd;
    int rPostStart = lPostEnd + 1;
    int rPostEnd = postEnd - 1;

    // 获取右子树
    TreeNode rightNode = build(preorder, rPreStart, rPreEnd, postorder, rPostStart, rPostEnd);

    root.left = leftNode;
    root.right = rightNode;
    return root;
}

T98-验证搜索二叉树

实现代码

public boolean isValidBST(TreeNode root) {
    
    // 对于根节点,没有大小值的限制
    TreeNode max = null;
    TreeNode min = null;
    return valid(root, max, min);
}

/**
 * 判断以 root 为根节点的树是否为 BST 树
 * @param root
 * @param min 限制当前节点值的最小值
 * @param max 限制当前节点值的最大值
 * @return
 */
private boolean valid(TreeNode root, TreeNode min, TreeNode max) {
    if (root == null) return true;
    
    // 有最小值的限制,说明当前树为右子树,右子树一定大于 min.val
    if (min != null && root.val <= min.val) return false; 
    // 有最大值的限制,说明当前树为左子树,左子树一定小于 max.val
    if (max != null && root.val >= max.val) return false;
    
    // 左子树最大值限制为根值,右子树最小值限定为根值
    return valid(root.left, min, root) && valid(root.right, root, max);
}

T450-删除二叉搜索树的节点

实现代码

/**
 * 删除指定的节点
 * @param root
 * @param key
 * @return
 */
public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    TreeNode parent = null;
    return delete(root, parent, key);
}

/**
 * 找到需要删除的节点,及其父节点和子节点
 * @param root
 * @param parent
 * @param key
 * @return
 */
private TreeNode delete(TreeNode root, TreeNode parent, int key) {
    if (root == null) return null;
    if (root.val == key) {
        // 获取左右孩子
        TreeNode left = root.left;
        TreeNode right = root.right;
        TreeNode child = mergeBST(left, right);
        if (parent == null) {
            return child;
        } else {
            if (root.val < parent.val) {
                parent.left = child;
            } else {
                parent.right = child;
            }
            return parent;
        }
    } else if (root.val < key) {
        delete(root.right, root, key);
    } else {
        delete(root.left, root, key);
    }
    return root;
}

/**
 * 将两颗 BST合并为一棵
 * @param left 左子树的所有值都要比右子树的小
 * @param right
 * @return
 */
private TreeNode mergeBST(TreeNode left, TreeNode right) {
    if (left == null) return right;
    if (right == null) return left;
    if (right.left == null) {
        right.left = left;
    } else {
        mergeBST(left, right.left);
    }
    return right;
}