一、定义
二叉排序树也叫二叉查找树,或者是空树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有的值均小于它的根结点;
- 若它的右子树不空,则右子树上所有的值均大于它的根结点;
- 它的左右子树也分别为二叉排序树。
从上面定义可以看出二叉排序树一个很重要的性质:中序遍历可以得到一个结点值严格递增的有序序列。严格递增代表树中没有值相等的结点。(相等也容易实现,只需要在插入的时候修改下查找条件。)
二、存储
采用二叉链表,相应的结点结构
// 结点里还可以添加其它数据信息,为方便只保留关键字。
class TreeNode {
int key;
TreeNode lchild;
TreeNode rchild;
public TreeNode(int key) {
this.key = key;
}
}
三、查找
// 查找成功返回该结点,否则返回null
public static TreeNode search(TreeNode T, int key) {
while (T != null) {
if (key == T.key) {
break;
} else if (key < T.key) {
T = T.lchild;
} else {
T = T.rchild;
}
}
return T;
}
二叉排序树的查找类似于折半查找,通过不断缩小查找范围查找。但与折半查找不同的是,折半查找每次一定对半缩小范围,而排序树每次缩小的范围取决于排序树的形态,最好情况是完全二叉树形态对应的时间复杂度和折半查找相同为 O ( l o g 2 n ) Ο(log_2n) O(log2n),最坏的情况是斜二叉树平均查找长度为 n + 1 2 \frac {n+1}{2} 2n+1,退化到和顺序查找相同,对应的时间复杂度 O ( n ) Ο(n) O(n)。
综合 n n n个结点的排序树的各种形态,平均而言,二叉排序树的查找时间复杂度和折半查找相同为 O ( l o g 2 n ) Ο(log_2n) O(log2n)。
四、插入
插入的结点一定是个新的叶子结点,所以插入过程很方便,只需要修改原来叶子结点的左指针或右指针。
// 插入关键字已存在时直接退出,否则插入
public static void insert(TreeNode T, int key) {
TreeNode pre = null;
while (T != null) {
if (key < T.key) {
pre = T;
T = T.lchild;
} else if (key > T.key) {
pre = T;
T = T.rchild;
} else {
return;
}
}
if (key < pre.key) {
pre.lchild = new TreeNode(key);
} else {
pre.rchild = new TreeNode(key);
}
}
插入的过程即查找的过程,所以插入的时间复杂度也为 O ( l o g 2 n ) Ο(log_2n) O(log2n)。
五、创建
二叉排序树的创建过程就是反复插入的过程,也是对无序序列有序的过程。
这里应该注意无序序列中的元素顺序对创建的排序树形态影响很大,比如序列 [ 2, 1, 3 ] 或序列 [ 2, 3, 1 ] 创建后的形态是完全二叉树,而序列 [ 1, 2, 3 ] 和序列 [ 3, 2, 1 ] 创建后分别是右斜二叉树和左斜二叉树。
public static TreeNode creatBinTree(int[] arr) {
TreeNode T = null;
if (arr.length > 0) {
T = new TreeNode(arr[0]);
for (int i = 1; i < arr.length; i++)
insert(T, arr[i]);
}
return T;
}
n 个结点 n 次插入,所以创建排序树的时间复杂度 O ( n l o g 2 n ) Ο(nlog_2n) O(nlog2n)。
六、删除
删除过程同样是查找的过程,通过查找返回对应结点信息并修改相关指针,其中:
- 找到的结点的左右孩子都为空,那么直接删除该结点即可。
- 找到的结点的左右孩子有一个为空,那么将不空的那个孩子代替要删除的结点即可。
- 找到的结点的左右孩子都不为空,那么找到这个结点的右子树中的最小结点(此结点的左孩子一定为空),将最小结点的值赋给要删除的结点,然后删除最小结点。或者将左子树中最大结点(结点右孩子一定为空)的值赋给要删除的结点并删除最大结点。
上面三点始终围绕一点:删除前后其它结点中序遍历的相对顺序不发生改变。
注意在java中删除某个结点是通过断开它的引用来实现,一般需要另设个结点保存其父结点。初写这块时直接想通过node=null来实现删除结点的目的,其实仔细想想这只是将引用名指向空,被引用的结点与父结点间的联系丝毫没受到影响,归根结底还是对java中的引用理解不深刻。
删除部分代码
public static void delete(TreeNode T, int key) {
TreeNode pre = null; // 保存删除结点的父结点
while (T != null) { // 循坏获取删除结点
if (key == T.key) {
break;
} else if (key < T.key) {
pre = T;
T = T.lchild;
} else {
pre = T;
T = T.rchild;
}
}
if (T == null) // 空树直接返回
return;
if (T.lchild == null && T.rchild == null) {
if (pre == null) // 删除结点左右孩子为空且是根结点直接返回
return;
if (T.key < pre.key) // 否则根据与父结点的大小断开父结点一端的引用
pre.lchild = null;
else
pre.rchild = null;
} else if (T.lchild == null) {
if (pre == null) // 删除结点左孩子为空且删除结点是根结点直接返回
return;
if (T.key < pre.key) // 否则根据与父结点的大小将删除结点的右子树嫁接到父结点的一端
pre.lchild = T.rchild;
else
pre.rchild = T.rchild;
} else if (T.rchild == null) {
if (pre == null) // 删除结点右孩子为空且删除结点是根结点直接返回
return;
if (T.key < pre.key) // 否则根据与父结点的大小将删除结点的左子树嫁接到父结点的一端
pre.lchild = T.lchild;
else
pre.rchild = T.lchild;
} else { // 删除结点左右孩子均存在
TreeNode node = null;
TreeNode minNode = T.rchild;
while (minNode.lchild != null) { // 获取右子树最小结点
node = minNode;
minNode = minNode.lchild;
}
T.key = minNode.key; // 最小结点的值赋给根结点
if (node == null) { // 右子树只有一个结点断开根结点的引用
T.rchild = null;
} else { // 否则断开父结点的引用
node.lchild = null;
}
}
}
上面对删除结点是根结点的情况没有进行处理,主要是因为根结点没有被任何父结点引用,在这点上java中的引用传递没有C中的地址传递方便,如果想实现可以给树增加一个头结点。具体就不写了~
删除过程同样是查找的过程,时间复杂度仍为 O ( l o g 2 n ) Ο(log_2n) O(log2n)。