【数据结构】详解二叉搜索树
1. 二叉搜索树的概念
二叉搜索树(Binary Search Tree):又被称为二叉排序树,它或者为一颗空树或者满足如下条件:
- 若它的左子树不为空,那么它左子树上的节点值都比根节点的值小
- 若它的右子树不为空,那么它右子树上的节点值都比根节点的值大
- 它的左右子树也分别为二叉搜索树
2. 二叉搜素树的查找
2.1 算法思路
查找策略:根据二叉搜索树的性质我们可以很快发现对于根节点来说,如果待查找值大于当前根节点的值我们从右子树查找,如果待查找值小于根节点的值,我们从左子树查找,如果根节点的值等于待查找值,那么说明此时找到了,但是我们还要考虑查找失败的情况!即如果此时遍历到的根节点为空此时证明该树不存在该查找节点。因此我们可以将查找策略总结如下:
- 如果
根节点为空
,说明当前树为空树,无法继续查找,查找失败! - 此时根节点不为空,比较待查找值与根节点值,如果待查找值小于根节点值继续在左子树中递归查找,反之递归右子树中进行查找。
我们以查找节点值2为例:
如上图所示:step1:从根节点5开始,待查找节点值为2,因此递归左子树继续查找。Step2:此时根节点为3,待查找节点值为2,因此继续递归左子树继续查找。Step3:此时根节点为1,待查找值为2,因此递归右子树继续查找。Step4:此时根节点为2与待查找值相同。此时说明查找成功!
2.2 OJ题练习
相关OJ题:[700. 二叉搜索树中的搜索 - 力扣(LeetCode)](700. 二叉搜索树中的搜索 - 力扣(LeetCode))
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null) {
return null;
}
int rootVal = root.val;
if (val == rootVal) {
return root;
} else if (val < rootVal) {
return searchBST(root.left, val);
} else {
return searchBST(root.right, val);
}
}
}
3. 二叉搜索树的插入
3.1 算法思路
插入策略:首先如果该树是一颗空树,那么可以直接插入,如果不是一颗空树,那么我们需要首先确定插入位置。插入位置的确定其实与查找过程类似:我们首先比较此时根节点的值与待插入节点的值,如果待插入节点的值小于根节点的值那么说明待插入的节点应该位于左子树,因此需要在左子树中完成插入。反之如果待插入节点的值大于根节点的值那么说明待插入的节点应该位于右子树,因此需要在右子树中完成插入。我们可以将算法步骤归纳如下:
- 如果此时
根节点为空
说明是一个空树可以直接插入 - 如果此时根节点不为空可以分为两种情况:
- 根节点值小于待插入节点值,在左子树中完成插入
- 根节点值大于待插入节点值,在右子树中完成插入
我们以插入节点值10为例:
如上图所示:我们需要插入节点值为10的节点,Step1:此时根节点为5,小于待插入节点值,因此在右子树中进行插入。Step2:此时根节点为7,小于待插入节点值,因此在右子树中进行插入。Step3:此时根节点为8,小于待插入节点值,因此在右子树中进行插入。Step4:此时根节点为9,小于待插入值,因此在右子树中进行插入。Step5:此时根节点为空,因此此处根节点就是我们需要找的待插入位置,插入完成(这里会存在一个小问题,在代码部分会进行讲解)
3.2 OJ题练习
相关OJ题:[701. 二叉搜索树中的插入操作 - 力扣(LeetCode)](701. 二叉搜索树中的插入操作 - 力扣(LeetCode))
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
root = new TreeNode(val);
return root;
} else {
TreeNode parent = root;
TreeNode cur = root;
while (cur != null) {
parent = cur;
if (cur.val < val) {
// 待插入位置在右边
cur = cur.right;
} else {
// 待插入位置在左边
cur = cur.left;
}
}
// 此时cur一定为空为待插入位置
if (parent.val > val) {
// 从左边插入
parent.left = new TreeNode(val);
} else {
// 从右边插入
parent.right = new TreeNode(val);
}
}
return root;
}
}
盲区扫描:也许看上面的代码有些小伙伴会很迷糊,比如这里的parent和cur是做什么用的?这是因为按照之前所说的我们能够查找到待插入的节点位置,但是我们无法完成插入操作!!!有些小伙伴会错误的直接将此时root直接赋值为新节点,比如root = new TreeNode(val);但是这样只是改变了当前变量root的引用。并没有直接改变节点之间的链接关系,所以我们如果要想改变节点之间的链接关系,必须知道插入节点的父节点,通过父节点将节点的链接关系改变。
4. 二叉搜索树的删除(重、难点)
4.1 算法思路
二叉搜索树的删除同样遵循:①、先找到待删除节点。②、删除待删除节点 的步骤
删除策略:二叉搜索树的删除需要进行分类讨论:
- 如果待删除节点的左子树为空
- 待删除节点为根节点
- 待删除节点是其父节点的右子节点
- 待删除节点是其父节点的左子节点
- 如果待删除节点的右子树为空
- 待删除节点为根节点
- 待删除节点是其父节点的右子节点
- 待删除节点是其父节点的左子节点
- 如果待删除节点的左右子树都不为空,使用
"替换删除"
策略
下面将进行分情况讨论:
case1
:待删除节点的左子树为空
-
如果待删除节点为根节点,类似于下图情况,由于根节点左子树为空,于是可以直接将根节点替换为它的右子树完成删除
-
如果待删除节点不为根节点且是其父节点的右孩子节点,类似于下图情况,由于待删除节点为其父节点的右孩子节点,所以只要将父节点的右孩子节点替换为待删除节点的右子节点即相当于完成了删除功能。
-
如果待删除节点不为根节点且是其父节点的左孩子节点,类似于下图情况,由于待删除节点为其父节点的左孩子节点,所以只要将父节点的左孩子节点替换为待删除节点的右子节点即相当于完成了删除功能。
case2
:待删除节点的右子树为空
- 待删除节点为根节点,与case1中情况类似,读者自行分析
- 待删除节点是其父节点的右子节点,与case1中情况类似,读者自行分析
- 待删除节点是其父节点的左子节点,与case1中情况类似,读者自行分析
case3
:待删除节点左右子树都不为空
此时我们采用的是替换删除的策略,即为了不影响二叉搜索树中序遍历为有序序列这样的性质。我们可以从待删除节点中左子树中最大值替换根节点的值,然后删除最大值节点。或者从待删除节点右子树中最小值替换根节点的值,然后删除最小值节点。
如上图所示:我们需要删除节点3,其左右子树皆不为空树,因此我们在其左子树中一直找右子树的叶子节点,该节点即为左子树中最大值。也就是我们要找的“替罪羊”。可以发现,我们的策略就是找到左子树中的最大值然后替换待删除节点的值并删除左子树最大值节点。并且此时替罪羊节点符合情况case2
即右子树为空的条件,删除该节点较为容易。
4.2 OJ题练习
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
// 找到待删除节点
TreeNode parent = root;
TreeNode cur = root;
while (cur != null) {
if (cur.val == key) {
// 找到了待删除节点
root = deleteNode(root, parent, cur);
break;
} else if (cur.val < key) {
// 搜索右子树
parent = cur;
cur = cur.right;
} else {
// 搜索左子树
parent = cur;
cur = cur.left;
}
}
return root;
}
private TreeNode deleteNode(TreeNode root, TreeNode parent, TreeNode cur) {
if (cur.left == null) {
// 左子树为空
if (cur == root) {
// 删除节点为根节点
root = root.right;
} else if (parent.left == cur) {
parent.left = cur.right;
} else if (parent.right == cur) {
parent.right = cur.right;
}
} else if (cur.right == null) {
// 右子树为空
if (cur == root) {
// 删除节点为根节点
root = root.left;
} else if (parent.left == cur) {
parent.left = cur.left;
} else if (parent.right == cur) {
parent.right = cur.left;
}
} else {
// 左右子树都不为空
// 找左子树的最右边节点
TreeNode targetParent = cur.left;
TreeNode target = cur.left;
while (target.right != null) {
targetParent = target;
target = target.right;
}
cur.val = target.val;
// 判断删除节点
if (targetParent == target) {
// 删除左子树节点
cur.left = target.left;
} else {
targetParent.right = target.left;
}
}
return root;
}
}
盲区扫描:代码整体流程还是比较清晰易懂的,有些小伙伴可能会对最终删除“替罪羊”节点的步骤比较疑惑?即为什么最后需要判断待删除节点的位置呢?下面将给出解释
// 判断删除节点
if (targetParent == target) {
// 删除左子树节点
cur.left = target.left;
} else {
targetParent.right = target.left;
}
这几行判断代码避免了一个特殊情况:即待删除节点的左节点无右子树
,现在我们根据下图情景代入分析一下
此时我们已经确定节点7为待删除节点,但是其左右子树不为空,我们试图用变量targetParent
保存替换节点的父节点初始化为节点3的位置,target
记录替换节点的位置初始化为节点3的位置,但是此时节点3正是替换节点的位置,如果直接执行targetParent.right = target.left;
是不可取的!因此我们需要特殊处理这种情况!!