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,即
遍历解法
遍历解法的思路为:
- 遍历整颗二叉树,记录每个节点离根节点的距离
- 遍历到叶子结点,就更新最大深度的值
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 递归地构建:
- 创建一个根节点,其值为
nums中的最大值。 - 递归地在最大值 左边 的 子数组前缀上 构建左子树。
- 递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 *最大二叉树* 。
示例
输入: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 通过前序和中序遍历构造二叉树
题目描述
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
我的思路
- 题目说
inorder中元素是无重复的,我们可以将其值和索引存储在map中 - 遍历
preorder中的每个元素,从inorder中寻找该元素的位置,然后将inorder划分为两个子数组 - 递归地从左子数组找到构件好的左子树
- 从右子数组中找到构建好的右子树
- 构建当前节点的左右子树
- 返回
这道题就是考察对前序遍历和中序遍历中,根节点,左子节点范围和右子节点范围在两个数组中位置的把控,通过画图可以得到三部分在前序数组和中序数组的具体位置,这样就可以递归构建了。
2.3 通过后序中序遍历结果构造二叉树
题目描述
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
我的思路
- 整体思路和2.2节相似,也是将中序和后序数组分为三个部分
- 当前节点
- 左子节点部分
- 右子节点部分
题解略
2.4 通过前序遍历和后序遍历构造二叉树
题目描述
给定两个整数数组,preorder 和 postorder ,其中 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 开始计数)。
示例
输入: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 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
示例
输入:[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.valroot.val < max.valvalid(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 rootif root == null then return null
4.3 在BST中插入一个数
我们知道,对于数据结构的操作就是遍历和访问,遍历就是查询,而访问 就是修改。想要往某个数据结构中插入一条数据,就是要先找到插入的位置,然后进行插入操作。
涉及到二叉树的修改,就类似于二叉树的构造问题,方法的返回值为TreeNode类型,并且需要对递归调用的返回值进行接收。
见LeetCode第701题[二叉搜索树的插入操作]
题目描述
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例
我的思路
- 根据BST的特性,寻找插入的位置
if root.val > valif root.left == null then root.left = valif root.left != null then search(root.left)
if root.val < valif root.right == null then root.right = valif root.left != null then search(root.left)
4.4 在BST中删除目标节点
见LeetCode第450题[删除二叉搜索树中的节点]
题目描述
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
示例
我的思路
- 想要完成删除操作,我们需要找到四个节点:目标节点,父节点,左孩子和右孩子
- 左右孩子都是BST,需要将两个孩子合并为一棵BST
- 判断子树BST的根节点值和父节点的值的大小,来确定父节点的左指针还是右指针指向孩子
上述思路不合理的地方在于:
- 如果当前节点不是目标节点,应当怎么做?
- 根据当前节点的和目标节点的大小,从孩子节点中删除,然后返回当前节点
curNode- 如果当前节点是目标节点,返回值应当是什么?
parentNode == null,表示当前节点是根节点,返回值为childparentNode != null,将parentNode -> child,返回parentNode
具体代码实现见:[T450-删除二叉搜索树的节点]
二叉树习题
T104-二叉树最大深度
题目描述
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
我的思路
- 采用递归的方式:
- 先获取左子树的最大深度
maxLeft - 在获取右子树的最大深度
maxRight
- 先获取左子树的最大深度
- 在后序位置上,当前节点的最大深度为
Math.max(maxLeft, maxRight) + 1
T144-二叉树的前序遍历
题目描述
给你二叉树的根节点 root ,返回它节点值的 前序 遍历
我的思路
- 牢记三种遍历方式,代码逻辑位置
T543-二叉树的直径
题目描述
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
示例
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
我的思路
- 通过分析可以发现,所谓的直径,就是左右两个子树的深度之和
- 可以采用分解问题的方式,分别求出左右两个子树的最大深度
- 然后求得树的直径,更新当前节点的最大深度
T226-反转二叉树
题目描述
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例
我的思路
- 使用分解问题的方法:
- 先得到反转后的左子树和右子树
- 然后将父节点的左指针和右指针分别指向右子树和左子树
- 返回父节点
T116-填充每个二叉树节点的右侧指针
题目描述
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例
我的思路
- 这一题很像二叉树的层序遍历
- 将每一层的节点放在队列尾部
- 记录当前队列的
size - 如果
i == size说明到达队尾,node.next = null - 否则的话为
cur.next = q.peek()
递归思路
- 站在
cur节点上,我们应当考虑什么?该该怎么做? cur是一个非叶子节点cur.left.next -> cur.rightcur.right.next -> cur.next.left || null
T114-二叉树展开为链表
题目描述
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode,其中right子指针指向链表中下一个结点,而左子指针始终为null。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例
前序遍历思路
- 通过前序遍历,将前序遍历的节点存储到队列中
- 然后根据节点重新构建单链表
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;
}