1. 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
- 不允许存在相同的结点
如:一个int a [] = {5,3,4,1,7,8,2,6,0,9}; 数组组成二叉排序树
定义二叉搜索树的结构:
static class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
public TreeNode root;
2. 操作 - 查找
/*
搜索
搜索的时间复杂度最好:O(logN)
搜索的时间复杂度最坏:O(N)
*/
public boolean search(int key){
TreeNode cur = root;
while(cur != null){
//找到key,返回true
if(cur.val == key) {
return true;
}
//到右树寻找
else if(cur.val < key){
cur = cur.right;
}
//到左树寻找
else{
cur = cur.left;
}
}
//没有找到值为key的结点
return false;
}
3. 操作 - 插入
(1) 如果树为空树,即根 == null,直接插入
(2)如果树不是空树,按照查找逻辑确定插入位置,插入新结点
/*
插入:都是插入到叶子结点
插入的时间复杂度最好:O(logN)
插入的时间复杂度最坏:O(N)
*/
public void insert(int val) throws Exception {
TreeNode newNode = new TreeNode(val);
if (root == null) {
root = newNode;
return;
}
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
parent = cur;
//判断是否有重复元素进入
if (cur.val == val) {
throw new Exception("有重复元素进入!");
}
//如果要插入的结点的值,大于当前结点,就应该在cur的右子树
if (cur.val < val) {
cur = cur.right;
}
//如果要插入的结点的值,小于当前结点,就应该在cur的左子树
else if (cur.val > val) {
cur = cur.left;
}
}
//如果要插入的结点小于父节点,就应该接在父节点的左子树
if (parent.val > val) {
parent.left = newNode;
}
//如果要插入的结点大于父节点,就应该接在父节点的右子树
else {
parent.right = newNode;
}
}
搜索和插入的运行结果:
4. 操作 - 删除 ⭐⭐⭐⭐⭐
分情况讨论,如下所示:
设待删除结点为 cur, 待删除结点的双亲结点为 parent
(1)cur.left == null
① cur 是 root,则 root = cur.right
② cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
③ cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
(2)cur.right == null
① cur 是 root,则 root = cur.left
② cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
③ cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
(3)cur.left != null && cur.right != null
需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被 删除节点中,再来处理该结点的删除问题。
这部分的代码为(有bug):
//3.cur两边的子树都不为空
else{
TreeNode t = cur.left;
TreeNode tp = null;
while(t.right != null){
tp = t;
t = t.right;
}
cur.val = t.val;
//这个就变成了删除t指向的结点,且t结点的右子树为空
tp.right = t.left;
}
原来的图:
第一步进行分析和改进:
第二步进行分析和改进:
树类的方法:
//删除结点的操作
public void remove(int val) {
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
//去右子树寻找
if (cur.val < val) {
parent = cur;
cur = cur.right;
} else if (cur.val > val) {
parent = cur;
cur = cur.left;
}
//找到了
else {
removeNode(cur, parent);
return;
}
}
}
private void removeNode(TreeNode cur, TreeNode parent) {
//1.cur的左子树为空
if (cur.left == null) {
//(1)cur 是 root,则 root = cur.right
if (cur == root) {
root = cur.right;
}
//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
else if (cur == parent.left) {
parent.left = cur.right;
}
//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
else {
parent.right = cur.right;
}
}
//2.cur的右子树为空
else if (cur.right == null) {
//(1)cur 是 root,则 root = cur.left
if (cur == root) {
root = cur.left;
}
//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
else if (cur == parent.left) {
parent.left = cur.right;
}
//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
else {
parent.right = cur.right;
}
}
//3.cur两边的子树都不为空
else {
TreeNode t = cur.left;
TreeNode tp = cur;
while (t.right != null) {
tp = t;
t = t.right;
}
cur.val = t.val;
if (cur == tp) {
tp.left = t.left;
} else {
//这个就变成了删除t指向的结点,且t结点的右子树为空
tp.right = t.left;
}
}
}
测试类:
public class Test {
public static void main(String[] args) throws Exception {
BinarySearchTree binarySearchTree = new BinarySearchTree();
binarySearchTree.insert(5);
binarySearchTree.insert(3);
binarySearchTree.insert(7);
binarySearchTree.insert(1);
binarySearchTree.insert(4);
binarySearchTree.insert(6);
binarySearchTree.insert(8);
binarySearchTree.insert(0);
binarySearchTree.insert(9);
binarySearchTree.remove(3);
}
}
5. 二叉搜索树的完整代码
public class BinarySearchTree {
static class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
public TreeNode root;
/*
搜索
搜索的时间复杂度最好:O(logN)
搜索的时间复杂度最坏:O(N)
*/
public boolean search(int key) {
TreeNode cur = root;
while (cur != null) {
//找到key,返回true
if (cur.val == key) {
return true;
}
//到右树寻找
else if (cur.val < key) {
cur = cur.right;
}
//到左树寻找
else {
cur = cur.left;
}
}
//没有找到值为key的结点
return false;
}
/*
插入:都是插入到叶子结点
插入的时间复杂度最好:O(logN)
插入的时间复杂度最坏:O(N)
*/
public void insert(int val) throws Exception {
TreeNode newNode = new TreeNode(val);
if (root == null) {
root = newNode;
return;
}
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
parent = cur;
//判断是否有重复元素进入
if (cur.val == val) {
throw new Exception("有重复元素进入!");
}
//如果要插入的结点的值,大于当前结点,就应该在cur的右子树
if (cur.val < val) {
cur = cur.right;
}
//如果要插入的结点的值,小于当前结点,就应该在cur的左子树
else if (cur.val > val) {
cur = cur.left;
}
}
//如果要插入的结点小于父节点,就应该接在父节点的左子树
if (parent.val > val) {
parent.left = newNode;
}
//如果要插入的结点大于父节点,就应该接在父节点的右子树
else {
parent.right = newNode;
}
}
//删除结点的操作
public void remove(int val) {
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
//去右子树寻找
if (cur.val < val) {
parent = cur;
cur = cur.right;
} else if (cur.val > val) {
parent = cur;
cur = cur.left;
}
//找到了
else {
removeNode(cur, parent);
return;
}
}
}
private void removeNode(TreeNode cur, TreeNode parent) {
//1.cur的左子树为空
if (cur.left == null) {
//(1)cur 是 root,则 root = cur.right
if (cur == root) {
root = cur.right;
}
//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
else if (cur == parent.left) {
parent.left = cur.right;
}
//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
else {
parent.right = cur.right;
}
}
//2.cur的右子树为空
else if (cur.right == null) {
//(1)cur 是 root,则 root = cur.left
if (cur == root) {
root = cur.left;
}
//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
else if (cur == parent.left) {
parent.left = cur.right;
}
//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
else {
parent.right = cur.right;
}
}
//3.cur两边的子树都不为空
else {
TreeNode t = cur.left;
TreeNode tp = cur;
while (t.right != null) {
tp = t;
t = t.right;
}
cur.val = t.val;
if (cur == tp) {
tp.left = t.left;
} else {
//这个就变成了删除t指向的结点,且t结点的右子树为空
tp.right = t.left;
}
}
}
}
6. 性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。 对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度 的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
- 最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以,是二叉搜索树的性能最佳?
答:可以,这就涉及到后面的AVL树和红黑树了,后期的文章会继续讨论AVL树和红黑树。
7. 和Java类集的关系
TreeMap 和 TreeSet 即 Java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。