目录
二叉查找树的定义
二叉查找树(Binary Search Tree,BST),又叫做二叉排序树、二叉搜索树,是一种对查找和排序都有用的特殊二叉树,红黑树,AVL树都是特殊的二叉查找树(自平衡二叉搜索树)
二叉查找树或是空树,或是满足如下三个性质的二叉树:
- 若其左子树非空,则左子树上所有节点的值都小于根节点的值
- 若其右子树非空,则右子树上所有节点的值都大于根节点的值
- 其左右子树都是一棵二叉查找树
如下图所示,其中序遍历为{5,18,29,25,32,25,69}
二叉查找树的特性:二叉排序树通过中序遍历可以得到递增序列
二叉查找树的查询
因为二叉查找树的中序遍历有序性,即得到的递增的序列,由于有序,因此其查找与二分查找类似,每次都可以缩小查找范围,查询效率较高。
算法步骤:
- 若二叉查找树为空,则查找失败,返回空指针
- 若二叉查找树非空,则将待查找关键字key与根节点的关键字T − > d a t a T->dataT−>data进行比较:
- 如果x = T − > d a t a ,则查找成功,返回查询到的当前节点T
- 如果x < T − > d a t a ,则递归查找左子树
- 如果x > T − > d a t a ,则递归查找右子树
如下图所示,查找关键字32:
1)将32与二叉查找树的树根25比较,发现32 > 25 32>2532>25,于是到右子树中查询,如下图所示:
(2)将32与右子树的树根69比较,发现32 < 69 32<6932<69,于是到左子树中查询,如下图所示:
(3)将32与左子树的树根32比较,发现32 = 32 32=3232=32,相等,查询成功,返回该节点的指针,如下图所示:
Java代码实现
public class BST {
static class Node {
private int key;
private Node left;
private Node right;
public Node(int key) {
this.key = key;
}
}
private Node root;//BST的根节点
}
/**
* 查找是否存在节点
*
* 思路:根据二叉排序树的特点:
* ①如果要查找的值小于当前节点的值,那么,就往当前节点的左子树走
* ②如果要查找的值大于当前节点的值,那么,就往当前节点的右子树走
*
* @param key 带查找的key
* @return boolean是否存在
*/
public boolean find(int key) {
Node cur = root;
while (cur != null) {
if (key < root.key) {
cur = cur.left;
} else if (key > root.key) {
cur = cur.right;
} else {
return true;
}
}
return false;
}
算法分析:
- 时间复杂度:最好情况是O(logn),最坏情况是O ( n )
- 空间复杂度:O ( 1 )
二叉查找树的插入
因为二叉查找树的中序遍历存在有序性,所以首先要查找待插入元素的插入位置,当查找不成功时再将待插入元素作为新的叶子节点成为最后一个查找节点的左孩子或者右孩子。
算法步骤:
- 若二叉查找树为空,则创建一个新的节点S SS,将待插入关键字放入新节点的数据域,然后将S SS节点作为根节点,S SS节点的左右子树都设置为空。
- 若二叉查找树非空,则将带插入元素e和根节点的关键字T − > d a t a T->dataT−>data比较:
- 如果e < T − > d a t a ,则将 e 插入到左子树中
- 如果e > T − > d a t a ,则将 e 插入到右子树中
如图,向其中插入元素30:
(1)将30与根节点25比较,发现25 < 30 25<3025<30,因此到右子树中查询,如下图:
(2)将30与右子树的树根69比较,发现30 < 69 30<6930<69,则到69的左子树中查询,如下图:
(3)将30与左子树的树根32比较,发现30 < 32 30<3230<32,在32的左子树中查找,如下图:
(4)将30作为新的叶子节点插入到32的左子树中,如下图:
Java代码
二叉搜索树的插入思路:
思路和查找一样的,只是我们这次要进行的是插入操作,那么我们还需要一个parent节点,来时刻记录当前节点的双亲节点即:
①如果要插入的值等于当前节点的值,那么,无法插入(不可出现重复的key)
②如果要插入的值小于当前节点的值,那么,就往当前节点的左子树走
③如果要插入的值大于当前节点的值,那么,就往当前节点的右子树走
最终,如果走到空了,就说明不存在重复的key,只要往双亲节点的后面插就好了,就是合适的位置,具体往左边还是右边插入,需要比较待插入节点的key和parent的key
public void insert(int key) {
if (root == null) { //如果是空树,那么,直接插入
root = new Node(key);
return;
}
Node cur = root;
Node parent = null; //parent 为cur的父节点
while (true) {
if (cur == null) { //在遍历过程中,找到了合适是位置,就指针插入(没有重复节点)
if (parent.key < key) {
parent.right = new Node(key);
} else {
parent.left = new Node(key);
}
return;
}
if (key < cur.key) {
parent = cur;
cur = cur.left;
} else if (key > cur.key) {
parent = cur;
cur = cur.right;
} else {
throw new RuntimeException("插入失败,已经存在key");
}
}
}
算法分析
在二叉查找树中进行插入操作时需要先查找插入位置,插入本身只需要常数时间,但是查找插入位置的时间复杂度为O ( l o g n )
二叉查找树的创建
二叉查找树的创建可以从空树开始,按照输入关键字的顺序依次进行插入操作,最终得到一棵二叉查找树。
算法步骤
- 初始化二叉查找树为空树,T = N U L L
- 输入一个关键字e ,将e 插入到二叉查找树T中
- 重复步骤2,直到关键字输入完毕。
算法分析
有 n 个即将插入的元素,因此二叉查找树的创建需要 n 次插入,每次插入在最好情况和平均情况下都需要O(logn)时间,每次插入在最坏情况需要O(n)时间,因此在最好情况和平均情况下的时间复杂度为O(nlogn),最坏情况下的时间复杂度为O(n^2)
创建二叉查找树时,输入序列的次序不同, 创建的二叉查找树也是不同的。
二叉查找树的删除
算法步骤
- 在二叉查找树中查找待删除关键字的位置,p指向待删除节点,f指向p的父节点。如果查找失败,则返回
- 如果查找成功,则分为三种情况进行删除操作:
- 如果被删除节点的左子树为空,则令其右子树子承父业代替其位置即可
- 如果被删除节点的右子树为空,则令其左子树子承父业代替其位置即可
- 如果被删除节点的左右子树都不为空,则令其直接前驱或者直接后继代替它,再删除其直接前驱或者直接后继即可
(1)左子树为空。在二叉查找树中删除32,首先查找到32所在的位置,判断其左子树为空,则令其右子树子承父业代替其位置,删除过程如下图:
(2)右子树为空。在二叉查找树中删除69,首先查找到69所在的位置,判断其右子树为空,则令其左子树子承父业代替其位置,删除过程如下图:
(3)左右子树都不为空。在二叉查找树中删除25,首先查找到25所在的位置,判断其左右子树都不为空,则令其直接前驱(左子树最右节点是20)代替它,在删除其直接前驱20,删除20时i,其左子树代替其位置。删除过程如下图所示:
Java代码
public void remove(int key) {
if (root == null) {
throw new RuntimeException("为空树,删除错误!");
}
Node cur = root;
Node parent = null;
//查找是否key节点的位置
while (cur != null) {
if (key < cur.key) {
parent = cur;
cur = cur.left;
} else if (key > cur.key) {
parent = cur;
cur = cur.right;
} else {
break;
}
}
if (cur == null) {
throw new RuntimeException("找不到key,输入key不合法");
}
//cur 为待删除的节点
//parent 为待删除的节点的父节点
/*
* 情况1:如果待删除的节点没有左孩子
* 其中
* ①待删除的节点有右孩子
* ②待删除的节点没有右孩子
* 两种情况可以合并
*/
if (cur.left == null) {
if (cur == root) { //①如果要删除的是根节点
root = cur.right;
} else if (cur == parent.left) { //②如果要删除的是其父节点的左孩子
parent.left = cur.right;
} else { //③如果要删除的节点为其父节点的右孩子
parent.right = cur.right;
}
}
/*
* 情况2:如果待删除的节点没有右孩子
*
* 其中:待删除的节点必定存在左孩子
*/
else if (cur.right == null) { //①如果要删除的是根节点
if (cur == root) {
root = cur.left;
} else if (cur == parent.left) { //②如果要删除的是其父节点的左孩子
parent.left = cur.left;
} else { //③如果要删除的节点为其父节点的右孩子
parent.right = cur.left;
}
}
/*
* 情况3:如果待删除的节点既有左孩子又有右孩子
*
* 思路:
* 因为是排序二叉树,要找到整颗二叉树第一个大于该节点的节点,只需要,先向右走一步,然后一路往最左走就可以找到了
* 因此:
* ①先向右走一步
* ②不断向左走
* ③找到第一个大于待删除的节点的节点,将该节点的值,替换到待删除的节点
* ④删除找到的这个节点
* ⑤完成删除
*
*/
else {
Node nextParent = cur; //定义父节点,初始化就是待删除的节点
Node next = cur.right; //定义next为当前走到的节点,最终目的是找到第一个大于待删除的节点
while (next.left != null) {
nextParent = next;
next = next.left;
}
cur.key = next.key; //找到之后,完成值的替换
if (nextParent == cur) { //此时的父节点就是待删除的节点,那么说明找到的节点为父节点的右孩子(因为此时next只走了一步)
nextParent.right = next.right;
} else { //此时父节点不是待删除的节点,即next确实往左走了,且走到了头.
nextParent.left = next.right;
}
}
}
算法分析
二叉查找树的删除主要是查找的过程,需要 O(logn) 时间。在删除过程中,如果需要查找被删除节点前驱,则也需要O(logn)时间。所以,在二叉查找树中进行删除操作的时间复杂度为O(logn)