前言
?:CSDN的小伙伴们大家好,今天跟大家分享一个二叉树的应用案例——二叉排序树。如果这篇文章对你有用,麻烦给我点个小赞以示鼓励吧?
?:博客主页:空山新雨后的java知识图书馆
☀️:晴朗的天气
?做了好事受到指责而仍坚持下去,这才是奋斗者的本色。——巴尔扎克?
?上一篇文章:【数据结构与算法】——赫夫曼树?
?欢迎大家一起学习,进步。加油?
文章目录
一、二叉排序树的概念
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当
前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
例如数据(7, 3, 10, 12, 5, 1, 9)转换为二叉排序树就是,如下图
如果再插入一个元素2,那么就应该插入到1的右子结点位置

二、二叉排序树的相关操作
2.1、二叉排序树的创建和遍历
二叉排序树的创建设计思想:
设计二叉排序树无非就是按照他的概念将大于当前结点的放到其右子结点,将小于其的放到右子结点即可。使用递归的方式完成。
首先是结点类的设计
Node类
| 属性 | 说明 |
|---|---|
| int value | 结点类的值 |
| Node left | 左子结点 |
| Node right | 右子结点 |
| 方法 | 说明 |
| void add(Node node) | 添加一个结点的方法 |
| void infixOrder() | 中序遍历二叉排序树的方法 |
BinarySortTree结点类
| 属性 | 说明 |
|---|---|
| Node root | 根结点 |
| 方法 | 说明 |
| void add(Node node) | 添加一个新的结点的方法 |
| void infixOrder() | 中序遍历二叉排序树 |
代码演示Node结点类
public class Node {
/**结点的值*/
private int value;
/**左子结点*/
private Node left;
/**右子结点*/
private Node right;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
/**
* 添加结点的方法
*
* @param node
*/
public void add(Node node) {
//首先判断传入结点是否为空,不符合要求,
if (node == null) {
return;
}
//根据二叉排序树的定义,如果传入的结点的值小于当前结点的值,将它放到当前结点的左子结点
if (node.value < this.value) {
if (this.left == null) {
//如果为空,直接放到后面即可
this.left = node;
} else {
//如果不为空,继续递归,决定放到右子树还是左子树
this.left.add(node);
}
} else {
//如果传入结点的值大于等于当前结点的值,就把他放到当前结点的右子结点,当然,如果等于你也可以放到左子结点
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
/**
* 中序遍历二叉树
*/
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
二叉排序树结点类,BInarySortTree
package com.studySelf.tree.BinarySortTree;
/**
* @author wang
* @version 1.0
* @packageName com.studySelf.tree.BinarySortTree
* @className BinarySortTree
* @date 2022/6/1 20:30
* @Description 二叉排序树
*/
public class BinarySortTree {
/**根结点*/
private Node root;
/**
* @param node
* @Date 2022/6/2 15:18
* @Param
* @Return void
* @MetodName add
* @Author wang
* @Description 添加结点的方法,按二叉排序树的规则
*/
public void add(Node node) {
//如果根结点为空,直接将结点添加到根结点即可
if (root == null) {
root = node;
} else {
root.add(node);
}
}
/**
* @Date 2022/6/2 15:18
* @Param
* @Return void
* @MetodName infixOrder
* @Author wang
* @Description 中序遍历二叉排序树
*/
public void infixOrder() {
if (root == null) {
System.out.println("根节点点为空");
} else {
root.infixOrder();
}
}
}
测试
package com.studySelf.tree.BinarySortTree;
/**
* @author wang
* @version 1.0
* @packageName com.studySelf.tree.BinarySortTree
* @className BinarySortTreeDemo
* @date 2022/6/1 20:33
* @Description 二叉排序树测试类
*/
public class BinarySortTreeDemo {
public static void main(String[] args) {
BinarySortTree binarySortTree = new BinarySortTree();
int[] arr = {7,3,10,12,5,1,9};
// int [] arr1 = {4,9,6,8,1,3,2,5,7};
for (int i : arr) {
binarySortTree.add(new Node(i));
}
System.out.println("案例");
binarySortTree.infixOrder();
}
}
/*
* 案例
Node{value=1}
Node{value=3}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=10}
Node{value=12}*/
2.2、二叉排序树的删除操作
删除二叉排序树相对较复杂,要考虑到几种不同的情况。
1、删除的结点是叶子结点。例如下图中的2,5,9,12。
2、删除的结点只有一颗子树,如1结点
3、删除的结点右两颗子树,如7,3,10

2.2.1、二叉排序树删除叶子结点
思路:
1、要想删除某个结点,首先应该找到这个结点,targetNode
2、找到targetNode的父节点parentNode,因为到时候通过该结点进行删除
3、找到targetNode是parentNode的左子结点还是右子结点,
4、 如果是左子结点,那么parentNode.setLeft(null);
如果是右子结点,那么parentNode.setRight(null);
代码演示
查找目标结点的方法Node search(int value),该方法在Node结点类中
这里的判断条件有些多,需要考虑的情况比较多,容易漏掉。
特别注意的是:在进行递归查找之前,一定要判断当前结点对应的递归结点是否为空,如果为空,直接就返回空就可以了,说明目标结点不在该二叉排序树中。
/**
* 查找目标删除的结点,
*
* @param value 该值是你要删除的结点的值
* @return 返回删除的目标结点
*/
public Node search(int value) {
//如果当前结点的值就是传入的value,那么该结点就是目标结点
if (this.value == value) {
return this;
}
//如果传入的值小于当前结点的值,那么就应该向左子树递归查找
if (value < this.value) {
//如果当前结点的左子树为空,那么返回null,也就是传入的值不在二叉排序树中
if (this.left == null) {
return null;
}
return this.left.search(value);
} else {
//如果传入的值大于等于当前结点的值,那么就应该向右子树进行递归查找,
//这里有一个可能为传入的值与当前结点的值相同,为何把他放到右子树查找,是因为我们在构建
//二叉排序树的时候将相同的值放到了右子树,因此这里把相同的值放到右子树递归,但是我们应该尽量避免出现相同的值
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
查找目标结点的父节点的方法Node searchParent(int value);该方法在Node结点类
该方法需要考虑的就是几个方面
首先需要判断当前结点是否就是我们需要找的父节点,如果是的,直接返回就可以了。
如果不是,那么就根据传入的值是小于当前结点的,根据二叉排序树的定义,比当前结点小的是放在左子树的,就递归向左子树进行递归。反之向右子树递归。如果三个条件都不满足,那么说明该结点是根结点,没有父节点。
/**
* 查找当前结点的父节点
*
* @param value 通过当前结点的值
* @return 返回目标父节点
*/
public Node searchParent(int value) {
//如果当前结点的左子结点不为空,且左子结点的值等于传进来的值,右子结点同理,那么当前结点就是要找的目标父节点
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
return this;
} else {
//如果当前结点不是目标父节点。并且传入的值小于当前结点的值,并且当前结点的左子树不为空,那么想左子树递归查找
if (value < this.value && this.left != null) {
return this.left.searchParent(value);
//向右递归同理
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value);
} else {
//如果上述所有条件都不满足,返回空,例如跟结点就没有父节点。
return null;
}
}
}
删除指定结点的方法delNode(int value);目前实现的功能仅仅是删除叶子结点
该方法在BinarySortTree里面
思路
首先应该判断根结点不为空,其次找到目标结点和父节点。
其次这里需要判断一个特殊情况就是如果该二叉排序树只有一颗根结点,且根节点就是我们要删除的结点,那么直接将该结点置空即可。
之后就按照我们分析的,判断是父节点的哪个子结点就可以对应的对父节点的那个子结点进行置空即可删除。
/**
* @param value
* @Date 2022/6/2 15:41
* @Param
* @Return void
* @MetodName delNode
* @Author wang
* @Description 删除指定的结点的方法
*/
public void delNode(int value) {
//如果该二叉排序树的根结点为空,直接返回
if (root == null) {
return;
} else {
//先去找目标结点
Node targetNode = search(value);
//如果目标结点也为空,那么直接返回即可。
if (targetNode == null) {
return;
}
//如果二叉排序树中只有一颗根结点,那,并且targetNode找到了,说明目标节点就是根结点,直接将根结点置空即可
if (root.getRight() == null && root.getRight() == null) {
root = null;
return;
}
//找到目标结点的父节点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.getLeft() == null && targetNode.getRight() == null) {
//判断targetNode是父节点的左子结点,还是右子结点,如果要删除的结点能匹配,那么将对应的结点置空即可。
if (parent.getLeft() != null && parent.getLeft().getValue() == value) {
parent.setLeft(null);
} else if (parent.getRight() != null && parent.getRight().getValue() == value) {
parent.setRight(null);
}
}
}
}
测试
public class BinarySortTreeDemo {
public static void main(String[] args) {
BinarySortTree binarySortTree = new BinarySortTree();
int[] arr = {7,3,10,12,5,1,9};
// int [] arr1 = {4,9,6,8,1,3,2,5,7};
for (int i : arr) {
binarySortTree.add(new Node(i));
}
System.out.println("案例");
binarySortTree.infixOrder();
//删除叶子结点1
binarySortTree.delNode(1);
binarySortTree.delNode(5);
binarySortTree.delNode(9);
binarySortTree.delNode(12);
System.out.println("删除之后");
binarySortTree.infixOrder();
}
}
/**
案例
Node{value=1}
Node{value=3}
Node{value=5}
Node{value=7}
Node{value=9}
Node{value=10}
Node{value=12}
删除之后
Node{value=3}
Node{value=7}
Node{value=10}
*/
2.2.2、删除只有一颗子树的结点
思路:
删除该结点的思路相对较为复杂。
1、前两步是相同的,找到对应的targetNode目标结点,和目标结点的父节点parentNode
2、确定targetNode的子结点是左子结点还是右子结点。
3、如果targetNode的子结点是左子结点,
3.1、那么如果targetNode是parentNode的左子结点,那么就操作parentNode.left = targetNode.left;
3.2、如果targetNode是parent的右子结点,那么就操作parentNode.right = targetNode.left;
4、如果targetNode的子结点是右子结点
4.1、如果targetNode是parentNode的左子结点,那么就操作parentNode.left = targetNode.right;
4.2、如果targeNode是parentNode的右子结点,那么就操作parentNode.right = targetNode.right;
代码演示。该代码是void delNode(int value) 方法的一部分
/**
* @param value
* @Date 2022/6/2 15:41
* @Param
* @Return void
* @MetodName delNode
* @Author wang
* @Description 删除指定的结点的方法
*/
public void delNode(int value) {
//如果该二叉排序树的根结点为空,直接返回
if (root == null) {
return;
} else {
//先去找目标结点
Node targetNode = search(value);
//如果目标结点也为空,那么直接返回即可。
if (targetNode == null) {
return;
}
//如果二叉排序树中只有一颗根结点,那,并且targetNode找到了,说明目标节点就是根结点,直接将根结点置空即可
if (root.getRight() == null && root.getRight() == null) {
root = null;
return;
}
//找到目标结点的父节点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.getLeft() == null && targetNode.getRight() == null) {
//判断targetNode是父节点的左子结点,还是右子结点,如果要删除的结点能匹配,那么将对应的结点置空即可。
if (parent.getLeft() != null && parent.getLeft().getValue() == value) {
parent.setLeft(null);
} else if (parent.getRight() != null && parent.getRight().getValue() == value) {
parent.setRight(null);
}
} else if (targetNode.getLeft() == null && targetNode.getRight() == null) {
//这个是删除的目标结点有两个子树的情况
} else {
//这个是删除的目标结点拥有一个子树的情况
if (targetNode.getLeft() != null) {
//如果目标结点的左子树不为空
if (parent.getLeft().getValue() == value) {
//如果目标结点父节点的左子结点等于目标结点,也就是说目标结点是父节点的左子结点,那就让目标结点的左子结点指向父节点的左子结点
//这样就删除了目标结点
parent.setLeft(targetNode.getLeft());
} else {
//那么这个就是目标结点是父节点的右子结点,就吧目标结点的左子结点指向父节点的右子结点
parent.setRight(targetNode.getLeft());
}
} else if (targetNode.getRight() != null) {
//删除的目标结点拥有右子结点
if (parent.getLeft().getValue() == value) {
parent.setLeft(targetNode.getRight());
} else {
parent.setRight(targetNode.getRight());
}
}
}
}
}
2.2.3、删除的有两个子树的结点
思路:
1、删除有两个子树的二叉树首先也是需要找到目标结点targetNode和目标节点的父节点parentNode
2、从targetNode的右子树进行向左循环查找,找到最小的结点minNode。(当然,也可以队targetNode的左子树进行循环向右查找,这样可以查找到最大的结点maxNode,也是可以完成队targetNode结点的删除的。)
3、将这个minNode放到一个临时变量temp进行保存
4、删除这个minNode
5、targetNode.value= minNode
代码演示
方法:int removeMinNode(Node node)
/**
* @Date 2022/6/6 16:17
* @Param
* @param node 传入的是node结点(当做需要操作的二叉排序树的根结点)是目标结点的右子树
* @Return int 返回的以node为根结点的二叉排序树的值
* @MetodName removeMinNode
* @Author wang
* @Description 删除最小的结点
*/
public int removeMinNode(Node node) {
//将结点放入临时变量保存
Node temp = node;
//循环查找左子结点
while (temp.getLeft() != null) {
//当循环结束以后,就找到了这颗二叉排序树的最小值
temp = temp.getLeft();
}
//在总的二叉排序树中删除这个最小值的结点
delNode(temp.getValue());
//返回这个最小值的结点的值
return temp.getValue();
}
方法 int removeMaxNode(Node node);
/**
* @Date 2022/6/6 16:56
* @Param
* @param node 传入的结点是目标结点的左子树,看做一个二叉排序树
* @Return int 返回最大的结点的值
* @MetodName removeMaxNode
* @Author wang
* @Description 删除最大的结点,辅助删除用
*/
public int removeMaxNode(Node node) {
Node temp = node;
//查找目标结点的左子树,循环查找该左子树的右子结点,得到最大的结点
while (temp.getRight() != null) {
temp = temp.getRight();
}
delNode(temp.getValue());
return temp.getValue();
}
方法delNode(int value)中进行调用,这里调用了两种方式,一种是左找大,一种是右找小,两种都是可以的。
} else if (targetNode.getLeft() != null && targetNode.getRight() != null) {
//这个是删除的目标结点有两个子树的情况
//循环向目标结点的右子树进行查找找到最小的结点的值
// int minNodeVal = removeMinNode(targetNode.getRight());
//向目标结点的左子树查找最大的节点返回
int maxNode = removeMaxNode(targetNode.getLeft());
//将找到的这个值放到被删除的目标结点,那么就删除了目标结点。
// targetNode.setValue(minNodeVal);
targetNode.setValue(maxNode);
}
完整的delNode(int value) 方法
/**
* @param value
* @Date 2022/6/2 15:41
* @Param
* @Return void
* @MetodName delNode
* @Author wang
* @Description 删除指定的结点的方法
*/
public void delNode(int value) {
//如果该二叉排序树的根结点为空,直接返回
if (root == null) {
return;
} else {
//先去找目标结点
Node targetNode = search(value);
//如果目标结点也为空,那么直接返回即可。
if (targetNode == null) {
return;
}
//如果二叉排序树中只有一颗根结点,那,并且targetNode找到了,说明目标节点就是根结点,直接将根结点置空即可
if (root.getRight() == null && root.getRight() == null) {
root = null;
return;
}
//找到目标结点的父节点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.getLeft() == null && targetNode.getRight() == null) {
//判断targetNode是父节点的左子结点,还是右子结点,如果要删除的结点能匹配,那么将对应的结点置空即可。
if (parent.getLeft() != null && parent.getLeft().getValue() == value) {
parent.setLeft(null);
} else if (parent.getRight() != null && parent.getRight().getValue() == value) {
parent.setRight(null);
}
} else if (targetNode.getLeft() != null && targetNode.getRight() != null) {
//这个是删除的目标结点有两个子树的情况
//循环向目标结点的右子树进行查找找到最小的结点的值
// int minNodeVal = removeMinNode(targetNode.getRight());
//向目标结点的左子树查找最大的节点返回
int maxNode = removeMaxNode(targetNode.getLeft());
//将找到的这个值放到被删除的目标结点,那么就删除了目标结点。
// targetNode.setValue(minNodeVal);
targetNode.setValue(maxNode);
} else {
//这个是删除的目标结点拥有一个子树的情况
if (targetNode.getLeft() != null) {
//如果目标结点的左子树不为空
if (parent.getLeft().getValue() == value) {
//如果目标结点父节点的左子结点等于目标结点,也就是说目标结点是父节点的左子结点,那就让目标结点的左子结点指向父节点的左子结点
//这样就删除了目标结点
parent.setLeft(targetNode.getLeft());
} else {
//那么这个就是目标结点是父节点的右子结点,就吧目标结点的左子结点指向父节点的右子结点
parent.setRight(targetNode.getLeft());
}
} else if (targetNode.getRight() != null) {
//删除的目标结点拥有右子结点
if (parent.getLeft().getValue() == value) {
parent.setLeft(targetNode.getRight());
} else {
parent.setRight(targetNode.getRight());
}
}
}
}
}
2.2.3.1、删除两个子树的两种方式图解
接下来来解释一下删除的细节。
例如我们删除的树是下面这颗树,删除的目标结点是3

①、按照第一种方式,选择左子树,循环向右遍历以1为根结点的右子树,找到最大值,删除最大值并返回给temp。
为什么走左子树需要找到该子树的最大值,因为目标结点是要比左子树的所有值大的,删除目标结点,那么就得找到该左子树的最大结点放到被删除的目标结点处,找该子树的最大结点肯定往右遍历。才能让整棵树在被删除之后满足二叉排序树的定义。

那么将temp放到目标结点的位置,删除max,就得到了删除掉目标结点3的一颗二叉排序树。

这种就是通过上面的int removeMaxNode(Node node)方法进行删除。
②、按照第二种方式,也就是我们上面的int removeMinNode(Node node)方法,这次我们不去左子树,我们去右子树,还是下图这颗树,还是目标结点3

那么这次我们走的是右子树,去右子树根据定义应该向左循环找到该树的最小值。因为目标结点的值比右子树小,所以删除目标结点,在右子树就得找最小的值放到目标结点的位置,才能让树保持二叉排序树的定义。

那么将min结点4删除,放到目标结点3处,目标结点3也就完成了删除。

那么其实两种删除的方法最后的遍历结点结果都是一样的。因此选择哪种方式,看自己。主要是明白原理。
中序遍历结果都会是0,1,2,4,5,6,7,9,10,12。
例如我这里的测试删除结点3的结果
案例
Node{value=0}
Node{value=1}
Node{value=2}
Node{value=3}
Node{value=4}
Node{value=5}
Node{value=6}
Node{value=7}
Node{value=9}
Node{value=10}
Node{value=12}
删除后
Node{value=0}
Node{value=1}
Node{value=2}
Node{value=4}
Node{value=5}
Node{value=6}
Node{value=7}
Node{value=9}
Node{value=10}
Node{value=12}
