1.二叉搜索树
1.1 概念
二叉搜索树又称为二叉排序树,它是一颗空树或者具有以下性质的树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
例如对上图二叉树采用中序遍历,得到的结果为:{0,1,2,3,4,5,6,7,8,9}
即一颗二叉搜索树的中序遍历一定是有序的,且是从小到大进行排列
2.二叉搜索树的操作
创建节点,这里使用静态内部类来创建
代码示例:
calss BinarySearchTree {
static class Node {
public int val;
public Node left;
public Node right;
public Node(int val) {
this.val = val;
}
}
//定义根节点为外部类的成员变量
Node root = null;
}
2.1 插入
定义插入的值为val,则插入节点要考虑两种情况:
-
树为空树,即root == null,直接插入即可
-
树不为空树,则先比较root的值和val大小关系,如果小于插入值,则说明去root的右边寻找;如果大于val,则说明去root的左边寻找;等于说明找到了,拼接即可
代码示例:
//插入节点
public void put(int val) {
Node node = new Node(val);
//第一次插入,root == null
if(root == null) {
root = node;
return;
}
//不是第一次插入 root != null
Node pre = null;//用来指向cur的父亲节点
Node cur = root;
while (cur != null) {
//去右边找
if (cur.val < val) {
pre = cur;//记录下此时cur
cur = pre.right;//cur向右节点移动
}else if (cur.val > val) {//该去左边找
pre = cur;
cur = pre.left;
}else {//已经有相等的值,则不做任何处理,直接退出
return;
}
}
//此时cur = null,pre = cur的根节点
//判断当前val和pre的val大小,决定插入左边还是右边
if (pre.val < val) {//插入到右节点
pre.right = node;
}else {//插入到左节点
pre.left = node;
}
}
2.2 查找
查找的大体思路跟插入差不多,也是从根节点的值开始比较,大的就往右边找,小的往左边找,直到找到或者找不到,也就是说循环退出会有两种结果,一种是找到节点并返回,一种是没找到返回null
代码示例:
public Node get(int val) {
Node cur = root;
while (cur != null) {
//找到该节点
if (cur.val == val) {
return cur;
}else if (cur.val < val) {//去右边找
cur = cur.right;
}else {//去左边找
cur = cur.left;
}
}
//说明没找到
return null;
}
2.3 删除(难点)
对于二叉搜索树来说,插入和查询都很方便,因为这两个操作只有插入会用到父亲节点,而父亲节点自上而下遍历很容易寻找;但是对于删除操作来说,我们要找到当前需要删除节点的下一个节点,并且让这个节点覆盖掉需要删除的节点,这样才能完成删除操作,当然这是大体上的思路
设当前节点为cur,当前节点的父亲节点为pre,我们需要考虑一下3种情况
- cur.left == null
① cur == root,则 root = cur.right
② cur == pre.left,则 pre.left = cur.right
③ cur == pre.right,则 pre.right = cur.right - cur.right == null
① cur == root,则 root = cur.left
② cur == pre.left,则 pre.left = cur.left
③ cur == pre.right,则 pre.right = cur.left - cur.left != null && cur.right == null
① 找到cur的下一个节点 minCur
② cur = minCur 覆盖掉cur
③ 再覆盖掉之前的minCur,因此还需要定义一个minCur的根节点minPre
代码示例:
public boolean remove(int val) {
Node cur = root;//当前结点
Node pre = null;//当前节点的根节点
//先找到要删除的节点
while (cur != null) {
if (cur.val == val) {
break;
}else if (cur.val < val) {
pre = cur;
cur = pre.right;
}else {
pre = cur;
cur = pre.left;
}
}
//判断是否找到
//cur为null,说明没有找到
if (cur == null) return false;
//说明找到了cur,开始判断cur的位置
if (cur.left == null) {
if (cur == root) {//是根节点
root = cur.right;//替换
}else {//不是根节点
if (cur == pre.left) {
pre.left = cur.right;
}else {
pre.right = cur.right;
}
}
}else if (cur.right == null) {
if (cur == root) {
root = cur.left;
}else {
if (cur == pre.left) {
pre.left = cur.left;
}else {
pre.right = cur.left;
}
}
}else {//cur的左右节点都不为null
Node minPre = cur;//指向cur下一节点的根节点
Node minCur = cur.right;//cur的下一个节点
while (minCur.left != null) {
minPre = minCur;
minCur = minPre.left;
}
cur.val = minCur.val;//先覆盖掉原来节点
//判断下一节点在根节点的哪一边
if (minCur == minPre.left) {
minPre.left = minCur.right;//再覆盖掉原来的下一节点
}else {
minPre.right = minCur.right;
}
}
return true;
}
2.4 演示
将刚刚的代码加上中序遍历整合到一起,然后给定一组测试数据来测试代码
代码示例:
public class HomeWork {
public static void main(String[] args) {
int[] arr = {7,5,45,9,26,41,2,6};
BinarySearchTree binarySearchTree = new BinarySearchTree();
for (int i = 0; i < arr.length; i++) {
binarySearchTree.put(arr[i]);
}
binarySearchTree.inOrder(binarySearchTree.root);
System.out.println();
binarySearchTree.preOrder(binarySearchTree.root);
System.out.println();
try {
System.out.println(binarySearchTree.get(45).val);
}catch (NullPointerException e) {
e.printStackTrace();
System.out.println("没有该节点");
}
System.out.println(binarySearchTree.remove(45));
binarySearchTree.inOrder(binarySearchTree.root);
}
}
class BinarySearchTree {
static class Node {
public int val;
public Node left;
public Node right;
public Node(int val) {
this.val = val;
}
}
Node root = null;
public void put(int val) {
Node node = new Node(val);
//第一次插入,root == null
if(root == null) {
root = node;
return;
}
//不是第一次插入 root != null
Node pre = null;//用来指向cur的父亲节点
Node cur = root;
while (cur != null) {
//去右边找
if (cur.val < val) {
pre = cur;//记录下此时cur
cur = pre.right;//cur向右节点移动
}else if (cur.val > val) {//该去左边找
pre = cur;
cur = pre.left;
}else {//已经有相等的值,则不做任何处理,直接退出
return;
}
}
//此时cur = null,pre = cur的根节点
//判断当前val和pre的val大小,决定插入左边还是右边
if (pre.val < val) {//插入到右节点
pre.right = node;
}else {//插入到左节点
pre.left = node;
}
}
public Node get(int val) {
Node cur = root;
while (cur != null) {
//找到该节点
if (cur.val == val) {
return cur;
}else if (cur.val < val) {//去右边找
cur = cur.right;
}else {//去左边找
cur = cur.left;
}
}
//说明没找到
return null;
}
//删除
public boolean remove(int val) {
Node cur = root;//当前结点
Node pre = null;//当前节点的根节点
//先找到要删除的节点
while (cur != null) {
if (cur.val == val) {
break;
}else if (cur.val < val) {
pre = cur;
cur = pre.right;
}else {
pre = cur;
cur = pre.left;
}
}
//判断是否找到
//cur为null,说明没有找到
if (cur == null) return false;
//说明找到了cur,开始判断cur的位置
if (cur.left == null) {
if (cur == root) {//是根节点
root = cur.right;//替换
}else {//不是根节点
if (cur == pre.left) {
pre.left = cur.right;
}else {
pre.right = cur.right;
}
}
}else if (cur.right == null) {
if (cur == root) {
root = cur.left;
}else {
if (cur == pre.left) {
pre.left = cur.left;
}else {
pre.right = cur.left;
}
}
}else {//cur的左右节点都不为null
Node minPre = cur;//指向cur下一节点的根节点
Node minCur = cur.right;//cur的下一个节点
while (minCur.left != null) {
minPre = minCur;
minCur = minPre.left;
}
cur.val = minCur.val;//先覆盖掉原来节点
//判断下一节点在根节点的哪一边
if (minCur == minPre.left) {
minPre.left = minCur.right;//再覆盖掉原来的下一节点
}else {
minPre.right = minCur.right;
}
}
return true;
}
//中序遍历
public void inOrder(Node root) {
if (root == null) return;
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
//前序遍历
public void preOrder(Node root) {
if (root == null) return;
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
}
结果演示
2 5 6 7 9 26 41 45
7 5 2 6 45 9 26 41
45
true
2 5 6 7 9 26 41
以上就是二叉搜索树的插入,查找和删除,此博客最想让大家掌握的是二叉搜索树的删除操作,希望各位小伙伴都能多多练习!
2.5 性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:logN
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:2/N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以使二叉搜索树的性能最佳?关于这个改进我将再写一篇博客来讲解