二叉搜索树
什么是二分搜索树
二分搜索树(Binary Search Tree),也称二叉搜索树、有序二叉树,是一种以二叉树为基础的数据结构。
二叉搜索树(Binary Search Tree),也称二叉查找树。有序二叉树(Ordered Binary tree)、排序二叉树(Sorted Binary Tree)都是同一个东西。
性质: 可以看到左节点永远小于根节点,右节点永远大于根节点。
二分搜索树的特点
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树。
常见操作
二分搜索树的常见操作:
- 插入节点,按照二分查找树的规则来插入元素
- 索引节点,查找指定元素,从头遍历按值来选择左右节点
- 删除节点,删除节点后需要把它下面的节点进行顺序调整
使用场景
- 用于快速查找数据。因为其分层结构,可以将查找范围缩小一半,从而实现log(n)的查询复杂度。
- 实现排序。可以通过中序遍历得到有序的结果。
- AVL 树、红黑树都是基于二分搜索树进行平衡和调整的。
- 树状数组。通过将每个节点的值记作区间范围来实现。
手写BinarySearchTree
完成后的整体结构
这个是二分搜索类的基本框架,从上往下来介绍:
- 根节点root
- 比较器comparator
- 构造方法
- 添加方法
- 添加方法(递归形式,方法重载)
- 中序遍历
- 搜索方法
- 删除方法
- 删除最大节点
- 查询最大节点
静态节点类
kotlin
static class Node<T> {
private final T value;
private Node<T> left;
private Node<T> right;
Node(T value) {
this.value = value;
}
}
里面维护了一个
- 泛型T value
- 左子节点
- 右子节点
- 构造方法
增加节点的方法
增加节点就是按照节点的大小来进行插入:
对根节点进行遍历:
csharp
public void add(T value) {
root = add(root, value);
}
csharp
private Node<T> add(Node<T> node, T value) {
if (node == null) {
return new Node<>(value);
}
// 添加的元素小于当前元素, 向左递归
if (comparator.compare(node.value, value) > 0) node.left = add(node.left, value);
else node.right = add(node.right, value);
return node;
}
- 如果当前节点比要插入的节点大, 就往要插入节点的左边递归
- 如果当前节点比要插入的节点小, 就往要插入节点的右边递归
- 如果递归过程中某个节点为空,表示没有数据,此时把数据返回。
下面是一个比较的例子,参考一下:
中序遍历
scss
public void interOrder(Node<T> node) {
if (node == null) return;
interOrder(node.left);
System.out.println(node.value);
interOrder(node.right);
}
这里是使用中序遍历的方法来打印这个树,当然也可以使用前序和后序遍历,这里是递归的写法。
- 首先对节点的左节点进行递归操作,如果左子节点不为null,则一直堆栈,直到左子节点为空
- 方法栈里不断调用弹出方法,从最后进入的开始,依次打印当前节点的值,当然先打印的当前节点肯定是左子节点,然后递归当前节点的右子节点,最终它就是一个中序遍历的输出。
删除节点的方法
删除是这里面最难的步骤:
ini
public void delete(T value) {
delete(root, value);
}
private Node<T> delete(Node<T> node, T value) {
if (node == null) {
return null;
}
// node.value > value
if (comparator.compare(node.value, value) > 0) {
node.left = delete(node.left, value);
return node;
// node.value < value
} else if (comparator.compare(node.value, value) < 0) {
node.right = delete(node.right, value);
return node;
} else { // value == node.value
// 待删除节点左子树为空的情况
// 这里返回的节点替换原来被删除的节点
if (node.left == null) {
Node<T> rightNode = node.right;
node.right = null;
return rightNode;
}
// 待删除节点右子树为空的情况
if (node.right == null) {
Node<T> leftNode = node.left;
node.left = null;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 查找待删除节点的前继节点
// 用前继节点替换当前待删除节点
// 查找前继节点, 从待删除节点的左子树,查找最大值
Node<T> successor = searchMax(node.left);
removeMax(node ,node.left);
successor.left = node.left;
// 删除左边的最大值
successor.right = node.right;
// 后继节点完成替换, 删除当前节点
node.left = node.right = null;
return successor;
}
}
public void removeMax(Node<T> pre ,Node<T> node){
while (node.right!=null){
pre = node;
node = node.right;
}
assert pre != null;
if(node.left == null){
pre.left = null;
}
else{
pre.left = node.left;
}
}
// 找出比删除节点小的里面的最大的
public Node<T> searchMax(Node<T> node) {
while (node!=null){
if (node.right == null) return node;
node = node.right;
}
return null;
}
首先我们通过递归找到待删除的节点,将其操作之后的节点作为返回结果,最终这个返回结果会作为它前继节点的左子节点或右子节点。
删除分为三种情况:
- 删除的节点为叶子节点: 直接删除
- 删除的节点有一个子节点:用子节点与其替换
- 删除的节点有两个子节点: 寻找待删除节点的左节点中最大的节点来将其填充在删除的位置
这个删除有两种办法: (总之就是这样可以维持二叉搜索树左小右大的结构 )- 前驱转换(predecessor swapping):使用节点的左子树最大值来替换这个节点,然后删除原左子树最大值节点。
- 后继转换(successor swapping):使用节点的右子树最小值来替换这个节点,然后删除原右子树最小值节点。
我这里使用的是前驱转换:
- 首先需要找到待删除节点的左边的最大值,因此使用searchMax()方法,就是寻找它的右子节点,没有就返回当前节点
- 然后删除 待删除节点的左边的最大值:
- 然后又需要判断待删除节点的左边是否有值
- 有值的话就把左边的值作为前继节点的左子节点,没有值的话就把前继节点的left指向null
- 将我们找到的待删除节点的左边的最大值(也就是successor), 让它的左右节点指向待删除节点的左右节点,待删除节点的左右节点指向null,这样就完成了替换。
测试方法
ini
public static void main(String[] args) {
BinarySearchTree<Integer> tree = new BinarySearchTree<>(Integer::compare);
tree.add(10);
tree.add(5);
tree.add(15);
tree.add(3);
tree.add(8);
tree.add(25);
tree.add(6);
tree.interOrder(tree.root);
System.out.println("=====================");
tree.delete(5);
tree.interOrder(tree.root);
}
输出:
3
5
6
8
10
15
25
=====================
3
6
8
10
15
25
可以看到我们正确输出了。
总结
- 二分搜索树是一种树形结构,使用左右节点来维护。
- 二分搜索树的查询速度和插入速度一般
• 插入:O(h),h是树的高度。最差情况下为O(n)
• 删除:O(h),h是树的高度。最差情况下为O(n)
• 搜索:O(h),h是树的高度。最差情况下为O(n) - 如果添加顺序不当,可能会退化为链表
- 手写时在添加和删除都需要知道前继节点,而使用递归方法更方便,但是逻辑很难理解
- 它适用于快速查找数据(前提是添加顺序正确),正序,中序和后序遍历数据。
- 有一个缺点, 该二分查找树可能会退化为链表,时间复杂度退化为O(n)。