引入
上一节讲的是二叉树,那二叉搜索树是为什么出现的呢?它解决了二叉树的什么问题?
可以简单理解为:二叉树和散列表是并列的,查询查找删除都很快,任何散列表都可以用二叉树实现,二叉树也可以使用散列表实现。
但散列表是无序的,怎么让散列表有序呢,通过双向链表串联数组上挂的单链表,详情可以看散列表这一章节。
二叉树也是无序的,怎么让二叉树也有序呢? 这就引出了本章的内容 二叉搜索树。
二叉搜索树(Binary Search Tree)
二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。它是怎么做到这些的呢?
这些都依赖于二叉查找树的特殊结构。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉搜索树.png
查找操作
来看看如何在二叉搜索树中查找一个节点。我们先获取根节点,如果等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中查找。
查找.png插入操作
新插入的数据,都是在 叶子节点上,所以我们只需要从跟节点开始,依次比较要插入的数据和节点的大小关系。
如果新插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插入到右子节点的位置;如果不为空,就再遍历右子树,查找插入位置。同理,如果要插入的数据比节点值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,递归遍历左子树,查找插入位置。
插入55.png
删除操作
删除相比查找和插入较复杂一些,分3种情况。
第一种情况:如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为null。比如图中删除节点55
第二种情况:如果要删除的节点只有一个子节点(只有左子节点或只有右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中删除节点13
第三种情况:如果要删除的节点有两个子节点,就比较复杂了。我们需要找到这个节点右子树中的最小节点,把它替换到删除的节点上。然后删除这个最小节点。因为最小节点肯定没有左节点(有左节点就不是最小节点了),所以,应用上两条来删除这个最小节点就可以了。比如图中删除节点18
删除操作.png
实际上,删除操作还有个非常简单、取巧的方法,就是单纯的将要删除的节点标记为"已删除",也就是"逻辑删除"。这样做只是比较浪费内存,但大大减少了代码实现的难度。这点内存在现阶段,其实可以忽略不计。
上代码
public class BinarySearchTree extends BinaryTree {
/**
* 定义根节点
*/
private static TreeNode root;
/**
* 查找
*/
private static TreeNode find(int data) {
TreeNode p = root;
while (p != null) {
if (p.key == data) {
return p;
} else if (p.key < data) {
p = p.leftChild;
} else {
p = p.rightChild;
}
}
return null;
}
/**
* 插入
*/
private static void insert(int data) {
if (root == null) {
root = new TreeNode(data, "");
return;
}
TreeNode p = root;
while (p != null) {
if (data < p.key) {
if (p.leftChild == null) {
p.leftChild = new TreeNode(data, "");
return;
} else {
p = p.leftChild;
}
} else {
if (p.rightChild == null) {
p.rightChild = new TreeNode(data, "");
return;
} else {
p = p.rightChild;
}
}
}
}
/**
* 删除
*/
private static void delete(int data) {
TreeNode p = root;
// pp 记录的是p的父节点
TreeNode pp = null;
// 先找到它,再分情况
while (p != null && p.key != data) {
if (p.key < data) {
pp = p;
p = p.leftChild;
} else {
pp = p;
p = p.rightChild;
}
}
// 到这里就是找到了, 或者轮训完了
if (p == null) {
// 没找到
return;
}
// 1.要删除的节点有左右两个子节点
if (p.leftChild != null && p.rightChild != null) {
// 现在要做的就是找到右子树里面最小的那个节点 跟现在这个节点换一下
TreeNode minP = p.rightChild;
TreeNode minPP = p;
while (minP.leftChild != null) {
minPP = minP;
minP = minPP.leftChild;
}
// 现在minP就是 右子树里面最小的那个节点 了
// 1.1 先把这个节点的值,赋值给删除节点
p.data = minP.data;
// 1.2 删除这个 最小节点
minPP.leftChild = null;
return;
}
// 2.要删除的节点没有子节点 或 只有一个叶子节点
// 那直接挂过去就行了
TreeNode child;
if (p.leftChild != null) {
child = p.leftChild;
} else if (p.rightChild != null) {
child = p.rightChild;
} else {
child = null;
}
if (pp == null) {
// 删除的是根节点
root = child;
} else if (pp.leftChild == p) {
pp.leftChild = child;
} else {
pp.rightChild = child;
}
}
public static void main(String[] args) {
insert(1);
insert(2);
insert(3);
insert(4);
insert(5);
insert(6);
insert(11);
insert(12);
insert(13);
insert(14);
insert(15);
insert(16);
System.out.println("=============原始树=================");
inOrder(root);
System.out.println("=====================================");
System.out.println("=============添加99=================");
insert(99);
inOrder(root);
System.out.println("=====================================");
System.out.println("=============删除12=================");
delete(12);
inOrder(root);
System.out.println("=====================================");
System.out.println("=============查询11=================");
TreeNode node = find(11);
visited(node);
System.out.println("=====================================");
}
}
支持重复数据的二叉搜索树
和散列表一样的问题。当一样的数据,被放到同一个格里时,要怎么处理呢?
1、放入右子树,也就是说,把这个新插入的数据当做大于这个节点的值来处理
2、也是链表法。在这个节点上存链表。
二叉搜索树的时间复杂度
1、最糟糕的情况,全在左子树,或者全在右子树,这就意味着,二叉树退化成了链表,查找的时间复杂度为O(n)
2、最理想的情况,二叉搜索树是一个完全二叉树(或满二叉树),一层一层向下,就看树有多高了,也就是O(树高)。那有n个节点的完全二叉树树高多少呢,就不推导了,答案是logn。
所以最理想情况下,查找的时间复杂度为O(logn)
二叉搜索树相比散列表的优势
第一,散列表无序,二叉搜索树有序
第二,散列表扩容时,性能不稳定。虽然二叉搜索树也不太稳定,但完全二叉搜索树稳定啊。这个完全二叉搜索树,还有一个大名鼎鼎的名字,平衡二叉树,下章讲。
第三,笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
第四,散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。
网友评论