一、二叉搜索树
1、定义
二叉搜索树(Binary Search Tree,BST)是一种二叉树。又叫二叉查找树,二叉排序树。
定义:设x为二叉查找树中的一个结点,x结点包含关键字key,结点x的key值计为key[x]。如果y是x的左子树中的一个结点,则key[y]<=key[x];如果y是x的右子树的一个结点,则key[y]>=key[x]。
通俗讲就是:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树。
- 没有键值相等的结点。
为什么又叫做二叉排序树呢?
只要一颗树是二叉搜索树,那么它的中序遍历一定是有序的。左 根(输出) 右。
比如上图这颗二叉搜索树,它的中序遍历为:11 20 29 32 41 50 65 72 91 99。
2、基本操作
二叉搜索树的基本操作演示 - 可参考链接:https://visualgo.net/en/bst
2.1 查找元素
通过递归查找是否存在key。O(logn)。
思路:二叉搜索树的左子树永远是比根节点小的,而它的右子树则都是比根节点大的值。当前节点比要找的大就往左走,当前元素比要找的小就往右走。
2.2 插入元素
因为是二叉搜索树就不能插入重复的元素了,且每次插入都是插入到叶子节点的位置。
原树中不存在key,插入key返回true,否则返回false。
插入n个元素的时间复杂度 O(nlogn)。
思路:如果是空树直接把元素插入root位置就好了。否则,比较插入的元素比当前位置元素小就往左走,比当前位置元素大就往右走,直到为空。最后再判断插入到 parent 的左子树还是右子树位置。
注意:每次插入元素的时候都是要和根结点比较。一直要找到它应该插入的位置。
2.3 删除元素
删除元素是一个比较难的点,要考虑到几种种情况:
- (1)叶子结点 O(1):直接删除,不影响原。
- (2)仅仅有左或右子树的结点 O(1):结点删除后,将它的左子树或右子树整个移动到删除结点的位置就可以,子承父业。
- (3)既有左又有右子树的结点O(logn):找到需要删除结点d 的直接前驱或者直接后继 s,用 s来替换结点d,然后再删除结点s。
3、代码实现
1)基于链表存储的二叉搜索树
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BinarySearchTree {
public Node root = null;
/**
* 结点
*/
public static class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
/**
* 插入结点:插入其实就是查找,每次插入时都要和根结点比较,一直要找到它应该插入的位置。
*
* @param root
* @param data
*/
public boolean insert(Node root, int data) {
if (this.root == null) {
return false;
}
if (root.data < data) { // 比根节点大 我们要放到右边
if (root.right == null) {
root.right = new Node(data);
} else {
insert(root.right, data);
}
} else { // 比根节点小 我们要放到右边
if (root.left == null) {
root.left = new Node(data);
} else {
insert(root.left, data);
}
}
return true;
}
/**
* 查找元素
*
* @param key
*/
public Node search(int key) {
if (this.root == null) {
return null;
}
Node cur = this.root;
while (cur != null) {
if (cur.data == key) {
return cur;
} else if (cur.data > key) {
cur = cur.left;
} else {
cur = cur.right;
}
}
return null;
}
/**
* 删除元素
*
* @param key
* @return
*/
public boolean remove(int key) {
if (this.root == null) {
return false;
}
Node parent = null;
Node cur = this.root;
while (cur != null) {
// 找到该节点
if (cur.data == key) {
removeKey(parent, cur);
return true;
} else if (cur.data < key) {
parent = cur;
cur = cur.right;
} else {
parent = cur;
cur = cur.left;
}
}
return false;
}
/**
* 从父结点中删除当前节点
*
* @param parent
* - cur的父结点
* @param cur
* - 要删除的当前结点
*/
private void removeKey(Node parent, Node cur) {
/**
* 第一种:当前结点的左子树为空 <br/>
* 如果删除的当前结点是根结点,则根结点指向根结点的右结点。 <br/>
* 如果删除的当前结点是父结点的左孩子,则父结点的左孩子指向当前结点右结点。<br/>
* 如果删除的当前结点是父结点的右孩子,则父结点的右孩子指向当前结点右结点。<br/>
*/
if (cur.left == null) {
if (cur == this.root) {
this.root = this.root.right;
} else if (cur == parent.left) {
parent.left = cur.right;
} else {
parent.right = cur.right;
}
} else if (cur.right == null) { // 第二种:当前结点的右子树为空
if (cur == this.root) {
this.root = this.root.left;
} else if (cur == parent.left) {
parent.left = cur.left;
} else {
parent.right = cur.left;
}
} else {
/**
* 第三种:当前结点的左右子树均不为空 <br></>
* 1.寻找当前结点的后继结点。注意:其后继结点的左子树一定为空。
* 2.将当前结点替换为其后继节点,并移动指针。
*/
// 1.寻找当前结点的后继结点
Node targetParent = cur;
Node target = cur.right;
while (target.left != null) {
targetParent = target;
target = target.left;
}
// 2.替换为后继结点的值,并移动指针
cur.data = target.data;
if (targetParent.left == target) {
targetParent.left = target.right;
} else {
targetParent.right = target.right;
}
}
}
}
2)测试
public static void main(String[] args) {
BinarySearchTree binarySearchTree = new BinarySearchTree();
BinarySearchTree.Node root = new BinarySearchTree.Node(10);
binarySearchTree.setRoot(root);
// 插入
binarySearchTree.insert(root, 9);
binarySearchTree.insert(root, 14);
binarySearchTree.insert(root, 11);
binarySearchTree.insert(root, 15);
binarySearchTree.insert(root, 12);
binarySearchTree.insert(root, 13);
binarySearchTree.insert(root, 8);
System.out.println(binarySearchTree);
// 查找
BinarySearchTree.Node search = binarySearchTree.search(80);
System.out.println(search == null ? "未找到" : search.data);
search = binarySearchTree.search(11);
System.out.println(search == null ? "未找到" : search.data);
// 删除
binarySearchTree.remove(10);
System.out.println(binarySearchTree);
}
4、性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
- 最好情况:二叉搜索树为完全二叉树,其平均比较次数为:O(logn)。
- 最坏情况:二叉搜索树退化为单支树,其平均比较次数为:O(n)。
结论:二叉树的结构就决定了其搜索的性能,不要让二叉树变成单支树,而是让树的左右子树高度相近,所以就有了 AVL树和红黑树。
– 求知若饥,虚心若愚。