上一篇博客写了有序表查找,其中三种方法的基本思想都是二分查找,他们的查找的时间复杂度均为 O(logn),我们发现有序表的查找效率挺高的了,但是插入效率很低,插入的时间复杂度仍然是O(n),为了提高插入效率,有人提出了二叉排序树的数据结构。
一、定义
二叉排序树,又称二叉查找树。它或者是一颗空树,或者是一棵具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值;
- 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 它的左、右子树也分别是二叉排序树。
上面的定义是一种二叉树中常见的递归的定义方法。这种二叉排序树左子树节点一定比父节点小,右子树节点一定比双亲节点大。如下面就是一颗二叉排序树:
二、二叉排序树的操作
1.查找
二叉排序树的查找其实很好理解,给定一个键值key,从根节点开始,比较当前节点的键值与key的大小,如果大于key,则在该节点的右子树中查找;如果小于key,则在该节点的左子树中查找,不断递归,直到找到或者到达叶子节点(未找到)。比如上面的二叉排序树中,查找51,查找过程为:
二叉排序树查找的时间复杂度是O(logn)。
2.插入
有了查找,插入就非常容易了。对于一个要插入的key,先按照上面的查找算法,直到找到叶子结点。再去判断key与该叶子结点的大小关系,如果key大于叶子结点,则key结点作为叶子结点的右孩子,小于的话则作为叶子结点的左孩子。比如上面的二叉排序树插入50,我们先查找到51所在的节点,然后将50作为51结点的左孩子。插入过程如下:
3.删除
二叉排序树中结点的删除是最复杂的一种操作。需要分三种情况讨论:
(1)删除结点为叶节点
删除节点为叶结点,说明删除结点的左右孩子均为空。这是最简单的一种情况。如下图,我们删除二叉排序树中的37结点。我们只需要找到37的父节点35,将对应的孩子节点置空,然后释放被删除节点即可。
(2)删除结点左孩子或右孩子为空
这是稍微复杂一点的情况。当删除结点node的左孩子为空,右孩子不为空时。我们只需要将node的右孩子节点移动到删除节点的位置,然后断开删除节点的连接关系,再释放被删除结点即可。如上面二叉查找树中的35节点,删除时如下所示:
同理,当删除结点node的右孩子为空,左孩子不为空时。我们只需要将node的左孩子移动到删除节点的位置,然后断开删除节点的连接关系,再释放删除结点即可。我这里就不画图了。
(3)删除结点既有左孩子又有右孩子
这是最复杂的情况。这里我们用先覆盖再删除的方式。 (至于为什么要这样,在讨论部分讨论)。
将结点删除后,我们需要找一个树中原来的结点放置在删除位置,那这个结点应该找哪个呢?根据二叉排序树的性质,这个结点要大于左子树中的所有节点,小于右子树中的所有节点。那这个代替节点要么是删除节点左子树中最右边的节点(对应着删除节点左子树中的最大节点),要么是删除节点右子树中最左边的节点(对应着删除节点右子树中的最小节点) 。先覆盖的意思是,将找到的代替节点中的数据全部赋值给删除节点(删除节点的左右孩子、双亲信息不变,只覆盖所携带的数据)。 再删除的意思是再删除代替节点。代替节点的删除肯定是上面两种情况中的一种,调用上面的删除方法即可。 比如删除上面二叉排序树中的根结点62,我们先找到62左子树中最右边的节点51(或者62右子树中最左边的节点73),然后将51节点的键值数据赋值给62节点,再删除51节点。