二叉搜索树(BST, Binary Search Tree)的性质:
01.若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉搜索树;
02.二叉搜索树中序遍历的结果是有序序列;
03.自上向下,第一个在两个目标节点构成的闭区间范围内的节点即为二者的最近公共祖先;
01.二叉搜索树的查找、插入
利用性质01,进行分流查找,最多遍历一条路径,普通树的搜索需要最多需要遍历整棵树;
练习题:
递归法,判断目标值所在的分支,只递归目标分支;
// 递归法,分流查找
public TreeNode searchBST(TreeNode root, int val) {
// 终止条件,当前路径遍历结束,未找到目标节点
if (root == null) {
return null;
}
// 单层操作,当前节点的值一定存在
if (root.val > val) { // 在左子树上
return searchBST(root.left, val);
} else if (root.val < val) { // 在右子树上
return searchBST(root.right, val);
} else { // 找到目标节点
return root;
}
}
迭代法,辅助队列版本,只将目标分支的根节点入队;
// 迭代法
public TreeNode searchBST(TreeNode root, int val) {
// 题干表明为非空树,可以不处理空树
Queue<TreeNode> queue = new LinkedList<>(); // 创建辅助队列
// 初始化辅助队列
queue.offer(root);
// 循环条件是队列非空
while (!queue.isEmpty()) {
TreeNode currNode = queue.poll();
if (currNode.val > val) { // 在左子树上
// 填充非空的孩子节点
if (currNode.left != null) {
queue.offer(currNode.left);
}
} else if (currNode.val < val) { // 在右子树上
// 填充非空的孩子节点
if (currNode.right != null) {
queue.offer(currNode.right);
}
} else { // 找到目标节点
return currNode;
}
}
// 当前路径遍历结束,未找到目标节点
return null;
}
迭代法,更新当前节点版本;
// 迭代法,更新当前节点版本
public TreeNode searchBST(TreeNode root, int val) {
// 循环条件是当前节点非空
while (root != null) {
if (root. val > val) { // 在左子树上
root = root.left;
} else if (root. val < val) { // 在右子树上
root = root.right;
} else { // 当前节点就是目标节点
return root;
}
}
// 遍历结束,未找到目标值
return null;
}
递归法,通过分流方式查找到待插入路径的末尾,创建新节点返回,通过递归连接;
// 递归法,利用二叉搜索树"左孩子<当前<右孩子"的特点,遍历到目标位置,目标位置的原节点为空
// 01.确定输入参数:当前节点、待添加的节点值 和 返回值:添加的节点
// 02.终止条件:当前节点为空
// 03.单层操作
public TreeNode insertIntoBST(TreeNode root, int val) {
// 终止条件:当前节点为空
if (root == null) {
// 创建待添加的节点,并返回
TreeNode newNode = new TreeNode(val);
return newNode;
}
// 单层操作,当前节点存在,负责父子关系的连接
if (root.val > val) { // 添加到左子树
root.left = insertIntoBST(root.left, val);
} else { // 添加到右子树
root.right = insertIntoBST(root.right, val);
}
return root;
}
迭代法,通过 currNode = currNode.left 实现接续,一直遍历到末尾;
// 迭代法,通过记录父节点,方便新节点的添加
public TreeNode insertIntoBST(TreeNode root, int val) {
// 处理空树
if (root == null) {
return new TreeNode(val);
}
// 否则非空树,需要遍历
// 初始化当前节点和父节点
TreeNode currNode = root;
TreeNode parentNode = null;
// 循环条件是当前节点非空
while (currNode != null) { // 找到待添加位置的父节点
// 记录父节点
parentNode = currNode;
if (currNode.val > val) {
currNode = currNode.left;
} else {
currNode = currNode.right;
}
}
// 添加新节点
if (parentNode.val > val) {
parentNode.left = new TreeNode(val);
} else {
parentNode.right = new TreeNode(val);
}
return root;
}
自上向下,第一个在两个目标节点构成的闭区间范围内的节点即为二者的最近公共祖先,即转换成找一个在闭区间范围内的节点;如果当前节点大于两个节点,则最近公共祖先节点在左子树上;如果当前节点小于两个节点,则最近公共祖先节点在右子树上;
02.二叉搜索树的验证、最小绝对值和众数
利用性质2,中序遍历的结果是有序的(单调递增),即当前节点的值大于历史最大值, 双指针记录历史最大值,中序遍历的框架;
练习题:
递归法,需要获取连续的历史最大值,创建成员对象记录历史最大值,递归后,有一个分支返回false,则后面就不需要操作和递归了;
// 验证二叉搜索树,利用其中序遍历的数值是单调递增的特性
// 递归法,在过程中判断单调递增,即中序遍历时,当前值始终大于历史最大值(前一节点的取值)
// 保持一个变量在递归过程的连续性,两种方法:01.创建为类的成员变量 或 02.使用数组作为参数,过程中修改数组元素的取值
// 创建存储前一节点的成员变量
TreeNode preNdoe = null;
// 递归法,中序遍历,每一个节点都需要判断
public boolean isValidBST(TreeNode root) {
// 终止条件,当前节点为空,返回 true
if (root == null) {
return true;
}
// 单层操作,左中右
// 左
boolean left = isValidBST(root.left);
// 有一个不符合,则不符合,后面不需要处理了
if (!left) {
return left;
}
// 中,如果前一节点存在,且当前节点不满足单调递增的特性,则不符合条件,返回 false
if ((preNdoe != null) && (root.val <= preNdoe.val)) {
return false;
}
// 更新前一节点
preNdoe = root;
// 右,所有节点都需要遍历,所有左右孩子全部递归
boolean right = isValidBST(root.right);
// 有一个不符合,则不符合
if (!right) {
return right;
}
return left && right;
}
迭代法,中序遍历的统一框架;
// 利用性质中序遍历下,中序遍历的数值单调递增,即当前节点的值始终大于历史最大值,否则不符合
// 迭代法,辅助栈,中序遍历的框架
public boolean isValidBST(TreeNode root) {
// 如果根节点为空,则为空树,符合条件
if (root == null) {
return true;
}
// 否则树非空,需要判断
Deque<TreeNode> stack = new LinkedList<>();
// 初始化辅助栈
stack.push(root);
// 初始化历史最大值的节点
TreeNode max = null;
// 循环条件是栈非空
while (!stack.isEmpty()) {
// 获取当前节点
TreeNode currNode = stack.pop();
// 如果当前节点非空,则按“右中左”顺序入栈,中序,出栈左中右
if (currNode != null) {
// 注意存入非空的孩子节点
if (currNode.right != null) {
stack.push(currNode.right);
}
stack.push(currNode);
stack.push(null);
if (currNode.left != null) {
stack.push(currNode.left);
}
} else { // 如果当前节点为空,则下一节点是待处理的节点
currNode = stack.pop();
// 如果存在历史最大值,则当前值应该大于历史最大值
if ((max != null) && (currNode.val <= max.val)) {
return false;
}
// 更新历史最大
max = currNode;
}
}
// 遍历结束,如果全部满足,则返回 true
return true;
}
中序遍历的框架,使用成员变量记录历史最大值,使用成员遍历记录前一节点;
中序遍历的框架,使用成员对象记录前一节点;
计数器的更新规则:第一次出现(前一节点不存在),取值为1;前后节点不相同,计数器复位为1;前后相等,累积计数;
众数更新规则:如果当前频次大于历史最大频次,则更新最大频次,先清空众数列表,再记录当前节点;如果当前频次等于历史最大频次,则增加当前节点值(存在多个众数);
03.二叉搜索树的累加树
利用DFS遍历的框架;
练习题:
深度遍历的框架,按题干调整顺序即可,右中左;
04.二叉搜索树的删除
二叉搜索树搜索目标值的框架,找到目标值后,分情况获取补位节点;
练习题:
递归法,可以实现补位节点与上一层的连接;
// 罗列每一种情况,在处理,否则编写后修改会比较混乱
// 待删除节点不存在
// 01.待删除的节点不存在,返回待删除的节点,即当前节点(null)
// 待删除节点存在,则关键是补位问题
// 02.待删除节点的左右孩子节点都为空,无需补位,直接删除,返回 null
// 03.待删除节点的左孩子为空,右孩子非空,右孩子补位,返回右孩子
// 04.待删除节点的左孩子非空,右孩子为空,左孩子补位,返回左孩子
// 05.待删除节点的左右孩子都非空,先将左孩子添加到右子树最左侧位置,再右孩子补位,返回右孩子
// 递归法:二叉搜索树搜索目标值的框架
// 01.确定输入参数:当前节点、待删除节点的取值 和 返回值:待删除节点
// 02.如果当前节点为空,表示情况01.待删除的节点不存在,返回待删除的节点,即当前节点(null)
// 03.单层操作,分流递归,找到待删除节点,分情况删除
public TreeNode deleteNode(TreeNode root, int key) {
// 如果当前节点为空,表示情况01.待删除的节点不存在,返回待删除的节点,即当前节点(null)
if (root == null) {
return root;
}
// 否则当前节点存在
if (root.val > key) { // 在左子树上
root.left = deleteNode(root.left, key);
} else if (root.val < key) { // 在右子树上
root.right = deleteNode(root.right, key);
} else { // 当期节点是待删除节点
// 02.待删除节点的左右孩子节点都为空,无需补位,直接删除,返回 null, 无需操作
if ((root.left == null) && (root.right == null)) {
return null; // 返回补位节点
} else if ((root.left == null) && (root.right != null)) {
// 03.待删除节点的左孩子为空,右孩子非空,右孩子补位,返回右孩子
return root.right; // 返回补位节点
} else if ((root.left != null) && (root.right == null)) {
// 04.待删除节点的左孩子非空,右孩子为空,左孩子补位,返回左孩子
return root.left;
} else { // 05.待删除节点的左右孩子都非空,先将左孩子添加到右子树最左侧位置,再右孩子补位,返回右孩子
// 找到右子树最左侧节点
TreeNode leftBound = root.right; // 初始化右子树的左边界
while (leftBound.left != null) {
leftBound = leftBound.left;
}
// 将左子树连接到右子树的左边界
leftBound.left = root.left;
return root.right; // 返回补位节点
}
}
return root;
}
迭代法,找到目标节点,需要记录父节点,每次更新当前节点前注意更新父节点指针,注意处理待删除节点是根节点的情况;
// 需要记录前一节点,分流找到目标节点,将目标节点的左孩子分支添加到右孩子的最左边,再用右孩子补位
// 迭代法,二叉搜索树搜索的框架
public TreeNode deleteNode(TreeNode root, int key) {
TreeNode preNode = null;
TreeNode currNode = root;
while (currNode != null) {
if (currNode.val > key) { // 在左子树上
preNode = currNode; // 更新前一节点
currNode = currNode.left;
} else if (currNode.val < key){ // 在右子树上
preNode = currNode; // 更新前一节点
currNode = currNode.right;
} else { // 找到目标节点
break; // 结束遍历
}
}
// 如果找到目标节点
if (currNode != null) {
// 分类获取补位节点
TreeNode coverNode;
if ((currNode.left == null) && (currNode.right == null)) {
coverNode = null;
} else if ((currNode.left != null) && (currNode.right == null)) {
coverNode = currNode.left;
} else if ((currNode.left == null) && (currNode.right != null)) {
coverNode = currNode.right;
} else { // 左右孩子都存在,先将左孩子连接到右子树的最左边
TreeNode leftBound = currNode.right;
while (leftBound.left != null) {
leftBound = leftBound.left;
}
leftBound.left = currNode.left;
coverNode = currNode.right;
}
// 连接补位节点
if (preNode != null) { // 如果前一节点存在,则可以连接补位节点
if (preNode.val > key) { // 目标节点原来是左子树
preNode.left = coverNode;
} else {
preNode.right = coverNode;
}
} else { // 否则删除的就是根节点,直接返回补位节点构成的子树
return coverNode;
}
}
// 否则没找到,不用删除
return root;
}
05.二叉搜索树的修剪
修剪为删除区间范围外的节点:
01.如果当前节点值小于low,则当前节点及其左子树全部裁掉,当前节点的右子树需要继续递归,将递归值赋值替换当前节点,通过递归实现与当前节点的父节点的连接;
02.当前节点值大于high,则当前节点及其右子树全部裁掉,当前节点的左子树需要继续递归判断;
03.当前节点的值在[low, high]的区间范围内,则左右子树都需要继续递归,将递归值替换当前节点对应的孩子节点;
练习题:
669. 修剪二叉搜索树](https://leetcode-cn.com/problems/delete-node-in-a-bst/)
递归法,将递归值赋值给当前节点,可实现与当前节点的父节点连接;
// 递归法:
// 01.确定输入参数:当前节点、区间范围 和 返回值:裁剪后的子树根节点
// 02.终止条件:当前节点为空,遍历结束,即裁剪结束,返回 null
// 03.单层操作,罗列所有情况:
// 01.当前节点值小于low,则当前节点及其左子树全部裁掉,当前节点的右子树需要继续递归判断
// 02.当前节点值大于high,则当前节点及其右子树全部裁掉,当前节点的左子树需要继续递归判断
// 03.当前节点的值在[low, high]的区间范围内,则左右子树都需要继续递归判断
public TreeNode trimBST(TreeNode root, int low, int high) {
// 终止条件:当前节点为空,遍历结束,即裁剪结束,返回 null
if (root == null) {
return null;
}
// 单层操作,罗列所有情况:
if (root.val < low) { // 01.当前节点值小于low,则当前节点及其左子树全部裁掉,当前节点的右子树需要继续递归判断
root = trimBST(root.right, low, high); // 将递归值作为当前节点返回给上一层
} else if (root.val > high) {
// 02.当前节点值大于high,则当前节点及其右子树全部裁掉,当前节点的左子树需要继续递归判断
root = trimBST(root.left, low, high);
} else { // 03.当前节点的值在[low, high]的区间范围内,则左右子树都需要继续递归判断
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
}
return root;
}
05.利用有序数组构造高度平衡的二叉搜索树(AVL)
有序数组的选节点作为根节点可以保证二叉搜索树的性质,选中间值可以保证平衡;
练习题:
递归法,拆分的框架,利用递归值进行连接;
// 利用最大二叉树中拆分思想,以中位数作为子树的根节点即为高度平衡的二叉树
// 递归法,拆分数组
// 01.确定输入参数:目标数组 和 返回值:当前子树的根节点
// 02.终止条件:当前数组的长度为空,不可拆分,返回 null
// 03.单层操作,以中位数创建根节点,递归左右子数组
public TreeNode sortedArrayToBST(int[] nums) {
// 终止条件:当前数组的长度为空,不可拆分,返回 null
if (nums.length == 0) {
return null;
}
// 单层操作,以中位数创建根节点,递归左右子数组
int rootIndex = nums.length / 2;
TreeNode currRoot = new TreeNode(nums[rootIndex]);
// 拆分数组
int[] left = Arrays.copyOfRange(nums, 0, rootIndex);
int[] right = Arrays.copyOfRange(nums, rootIndex + 1, nums.length);
// 递归左右子数组
currRoot.left = sortedArrayToBST(left);
currRoot.right = sortedArrayToBST(right);
return currRoot;
}
06.二叉搜索树的公共最近祖先
自上向下,第一个在两个目标节点构成的闭区间范围内的节点即为二者的最近公共祖先,即转换成找一个在闭区间范围内的节点;如果当前节点大于两个节点,则最近公共祖先节点在左子树上;如果当前节点小于两个节点,则最近公共祖先节点在右子树上;
练习题:
递归法,从上向下,如果当前节点在p、q形成的区间范围内,即为最近公共祖先节点;
// 递归法,从上向下,如果当前节点在p、q形成的区间范围内,即为最近公共祖先节点
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if ((root.val > p.val) && (root.val > q.val)) { // 如果当前节点大于两个节点,则目标节点一定在左子树上
// 递归左子树
return lowestCommonAncestor(root.left, p, q);
} else if ((root.val < p.val) && (root.val < q.val)) { // 如果当前节点小于两个节点,则目标节点一定在右子树上
// 递归右子树
return lowestCommonAncestor(root.right, p, q);
} else { // 找到最近公共祖先节点
return root;
}
}
迭代法,根据转换后的问题,找在区间范围内的一个节点,搜索的框架;
// 迭代法,通过更新当前节点,实现向下遍历的接续
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 初始化当前节点
TreeNode currNode = root;
while (currNode != null) {
if((currNode.val > p.val) && (currNode.val > q.val)) { // 目标节点在左子树
currNode = currNode.left;
} else if ((currNode.val < p.val) && (currNode.val < q.val)) { // 目标节点在右子树
currNode = currNode.right;
} else { // 如果当前节点在两个目标节点的区间范围之内,则结束循环
break;
}
}
return currNode;
}