二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)
所谓二叉搜索树(Binary Search Tree),又叫二叉排序树, 简单而言就是左子树上所有节点的值均小于根节点的值, 而右子树上所有结点的值均大于根节点的值, 左小右大,并不是乱序,因此得名二叉排序树。
一个新事物不能凭空产生,那二叉搜索树又有什么用呢?
有了二叉搜索树,当你要查找一个值, 就不需要遍历整个序列或者说遍历整棵树了, 可以根据当前遍历到的结点的值来确定搜索方向, 这就好比你要去日本,假设你没有见过世界地图, 你不知道该往哪个方向走, 只能满地球找一遍才能保证一定能够到达日本; 而如果你见过世界地图,你知道日本在中国的东边, 你就不会往西走、往南走、往北走。 这种思维在搜索中被叫做“剪枝”, 把不必要的分枝剪掉可以提高搜索效率。 在二叉搜索树中查找值,每次都会把搜索范围缩小, 与二分搜索的思维类似。
创建二叉搜索树
现有序列:A = {61, 87, 59, 47, 35, 73, 51, 98, 37, 93}。根据此序列构造二叉搜索树过程如下:
- i = 0,A[0] = 61,节点61作为根节点;
- i = 1,A[1] = 87,87 > 61,且节点61右孩子为空,故81为61节点的右孩子;
- i = 2,A[2] = 59,59 < 61,且节点61左孩子为空,故59为61节点的左孩子;
- i = 3,A[3] = 47,47 < 59,且节点59左孩子为空,故47为59节点的左孩子;
- i = 4,A[4] = 35,35 < 47,且节点47左孩子为空,故35为47节点的左孩子;
- i = 5,A[5] = 73,73 < 87,且节点87左孩子为空,故73为87节点的左孩子;
- i = 6,A[6] = 51,47 < 51,且节点47右孩子为空,故51为47节点的右孩子;
- i = 7,A[7] = 98,98 < 87,且节点87右孩子为空,故98为87节点的右孩子;
- i = 8,A[8] = 93,93 < 98,且节点98左孩子为空,故93为98节点的左孩子;
创建完毕后如下图中的二叉搜索树:
构造复杂度
二叉搜索树的构造过程,也就是将节点不断插入到树中适当位置的过程。该操作过程,与查询节点元素的操作基本相同,不同之处在于:
- 查询节点过程是,比较元素值是否相等,相等则返回,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,返回节点不存在
- 插入节点的过程是,比较元素值是否相等,相等则返回,表示已存在,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,则将节点插入该空节点位置。
由此可知,单个节点的构造复杂度和查询复杂度相同,为 ~。
使用二叉搜索树可以提高查找效率,其平均时间复杂度为O(log2n)。
二叉搜索树的两种极端情况:
- 完全二叉树,所有节点尽量填满树的每一层,上一层填满后还有剩余节点的话,则由左向右尽量填满下一层
- 每一层只有一个节点的二叉树。如下图所示:
插入
插入流程:
- 先检测该元素是否在树中已经存在。如果已经存在,则不进行插入;
- 若元素不存在,则进行查找过程,并将元素插入在查找结束的位置。
图解过程
删除复杂度
二叉搜索树的节点删除包括两个过程,查找和删除。查询的过程和查询复杂度已知, 这里说明一下删除节点的过程。
节点的删除有以下三种情况:
- 待删除节点度为零;
- 待删除节点度为一;
- 待删除节点度为二。
第一种情况如下图 s_1 所示,待删除节点值为 “6”, 该节点无子树,删除后并不影响二叉搜索树的结构特性, 可以直接删除。即二叉搜索树中待删除节点度为零时, 该节点为叶子节点,可以直接删除;
第二种情况如下图 s_2 所示,待删除节点值为 “7”,该节点有一个左子树,删除节点后,为了维持二叉搜索树结构特性,需要将左子树“上移”到删除的节点位置上。即二叉搜索树中待删除的节点度为一时,可以将待删除节点的左子树或右子树“上移”到删除节点位置上,以此来满足二叉搜索树的结构特性。
第三种情况如下图 s_3 所示,待删除节点值为 “9”,该节点既有左子树,也有右子树,删除节点后,为了维持二叉搜索树的结构特性,需要从其左子树中选出一个最大值的节点,“上移”到删除的节点位置上。即二叉搜索树中待删除节点的度为二时,可以将待删除节点的左子树中的最大值节点“移动”到删除节点位置上,以此来满足二叉搜索树的结构特性。
其实在真实的实现代码中,该情况下的实际节点删除操作是:
1.查找出左子树中的最大值节点
2.替换待删除节点 的值为 的值
3.删除 节点
因为 作为左子树的最大值节点,所以节点的度一定是 0 或 1,所以删除节点的情况就转移为以上两种情况。
之前提到二叉搜索树中节点的删除操作,包括查询和删除两个过程,这里称删除节点后,维持二叉搜索树结构特性的操作为“稳定结构”操作,观察以上三种情况可知:
- 前两种情况下,删除节点后,“稳定结构”操作的复杂度都是常数级别,即整个的节点删除操作复杂度为 ~;
- 第三种情况下,设删除的节点为 ,“稳定结构”操作需要查找 节点左子树中的最大值,也就是左子树中最“右”的叶子结点,即“稳定结构”操作其实也是一种内部的查询操作,所以整个的节点删除操作其实就是两个层次的查询操作,复杂度同为 ~;
性能分析
由以上查询复杂度、构造复杂度和删除复杂度的分析可知,三种操作的时间复杂度皆为 ~。下面分析线性结构的三种操作复杂度,以二分法为例:
- 查询复杂度,时间复杂度为 ,优于二叉搜索树;
- 元素的插入操作包括两个步骤,查询和插入。查询的复杂度已知,插入后调整元素位置的复杂度为 ,即单个元素的构造复杂度为:
- 删除操作也包括两个步骤,查询和删除,查询的复杂度已知,删除后调整元素位置的复杂度为 ,即单个元素的删除复杂度为: 由此可知,二叉搜索树相对于线性结构,在构造复杂度和删除复杂度方面占优;在查询复杂度方面,二叉搜索树可能存在类似于斜树,每层上只有一个节点的情况,该情况下查询复杂度不占优势。
总结
二叉搜索树的节点查询、构造和删除性能,与树的高度相关,如果二叉搜索树能够更“平衡”一些,避免了树结构向线性结构的倾斜,则能够显著降低时间复杂度。二叉搜索树的存储方面,相对于线性结构只需要保存元素值,树中节点需要额外的空间保存节点之间的父子关系,所以在存储消耗上要高于线性结构。
package datastructure.tree;
import lombok.Setter;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 二叉排序树:二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),也称二叉搜索树。
*
* @author liuyi27
*
* <p>
* 二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
* </p>
* <p>
* (1)若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
* </p>
* <p>
* (2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
* </p>
* <p>
* (3)左、右子树也分别为二叉排序树;
* </p>
*
*/
@Setter
public class BinarySortTree {
private int data;
private BinarySortTree lchild, rchild;
/**
* 递归查找二叉排序树T中是否存在key 指针f指向T的双亲,其初始调用值为NULL 若查找成功,* 则指针p指向该数据元素结点,并返回TRUE
* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE
*/
// 如果树是空的,则查找结束,无匹配。
// 如果被查找的值和根结点的值相等,查找成功。否则就在子树中继续查找。如果被查找的值小于根结点的值就选择左子树,大于根结点的值就选择右子树。
public static boolean search(BinarySortTree tree, int key) {
if (tree == null) {
return false;
}
if (tree.data == key) {
return true;
}
System.out.println(tree.data);
if (key < tree.data) {
System.out.println("<");
/* 在左子树中继续查找 */
return search(tree.lchild, key);
} else {
System.out.println(">");
/* 在右子树中继续查找 */
return search(tree.rchild, key);
}
}
/**
* 若查找的key已经有在树中,则p指向该数据结点。 若查找的key没有在树中,则p指向查找路径上最后一个结点。
*
* @param tree
* @param key
* @return
*/
public static BinarySortTree add(BinarySortTree tree, int key) {
if (tree == null) {
tree = new BinarySortTree();
tree.data = key;
return tree;
}
if (key < tree.data) {
tree.lchild = add(tree.lchild, key);
} else {
tree.rchild = add(tree.rchild, key);
}
return tree;
}
public static BinarySortTree create(BinarySortTree tree, int[] array) {
for (int i = 0; i < array.length; i++) {
tree = add(tree, array[i]);
}
return tree;
}
public static boolean remove(BinarySortTree tree, int key) {
if (tree == null) {
return false;
}
if (tree.data == key) {
return delete(tree);
} else if (tree.data > key) {
return remove(tree.lchild, key);
} else {
return remove(tree.rchild, key);
}
}
/**
* 1)删除结点为叶子结点; 2)删除的结点只有左子树; 3)删除的结点只有右子树 4)删除的结点既有左子树又有右子树。
*
* @param tree
* @return
*/
private static boolean delete(BinarySortTree tree) {
if (tree.rchild == null) {
// 右子树空则只需重接它的左子树(待删结点是叶子也走此分支)
tree = tree.lchild;
} else if (tree.lchild == null) {
// 只需重接它的右子树
tree = tree.rchild;
} else {
// 左右子树均不空
// 转左
BinarySortTree tempP = null;
BinarySortTree temp = tree.lchild;
// 然后向右到尽头(找待删结点的前驱)
while (temp.rchild != null) {
tempP = temp;
temp = temp.rchild;
}
// TODO 感觉非常有问题
if (tempP != null) {
tempP.rchild = null;
}else {
temp.lchild = null;
}
// 将被删结点前驱的值取代被删结点的值(指向被删结点的直接前驱)
tree.data = temp.data;
}
return true;
}
public static void layerOrder(BinarySortTree node, Queue<BinarySortTree> queue) {
if (node != null && queue.isEmpty()) {
// 将当前节点放入队列首指针所指位置
queue.add(node);
System.out.print(queue.poll().data + " ");
} else {
System.out.print(node.data + " ");
}
if (node.lchild != null) {
queue.add(node.lchild);
}
if (node.rchild != null) {
queue.add(node.rchild);
}
BinarySortTree nextNode = queue.poll();
if (nextNode != null) {
layerOrder(nextNode, queue);
}
}
public static void main(String[] args) {
int[] array = { 10, 4, 6, 34, 32, 5, 2, 1, 11, 23 };
BinarySortTree tree = null;
tree = create(tree, array);
Queue<BinarySortTree> queue = new LinkedBlockingDeque<BinarySortTree>();
layerOrder(tree, queue);
System.out.println("查找1");
// System.out.println(search(tree, 10));
// System.out.println(search(tree, 4));
// System.out.println(search(tree, 6));
// System.out.println(search(tree, 34));
// System.out.println(search(tree, 32));
// System.out.println(search(tree, 5));
// System.out.println(search(tree, 2));
// System.out.println(search(tree, 11));
// System.out.println(search(tree, 23));
System.out.println("查找1");
boolean result = remove(tree, 4);
System.out.println("");
System.out.println(result);
layerOrder(tree, queue);
System.out.println("");
System.out.println("查找2");
// System.out.println(search(tree, 10));
// System.out.println(search(tree, 4));
// System.out.println(search(tree, 34));
// System.out.println(search(tree, 32));
// System.out.println(search(tree, 2));
// System.out.println(search(tree, 11));
// System.out.println(search(tree, 23));
System.out.println(search(tree, 5));
System.out.println(search(tree, 6));
}
}
import java.util.Random;
/**
* 二叉搜索树
* @author 爱学习的程序员
* @version V1.0
*/
public class BST{
// 根结点
public static Node root = null;
/**
* 二叉搜索树的结点类
*/
public static class Node{
// 父结点
Node p;
// 左孩子
Node left;
// 右孩子
Node right;
// 关键字
int key;
public Node(Node p, Node left, Node right, int key){
this.p = p;
this.left = left;
this.right = right;
this.key = key;
}
}
/**
* 插入结点
* @param z 待插入结点
* @return 根结点
*/
public static void insert(Node z){
// 树为空,直接作为根结点
if(root == null)
root = z;
else{
Node y = null;
Node x = root;
// 寻求树中结点z的合适位置
while(x != null){
y = x;
if(z.key < x.key)
x = x.left;
else
x = x.right;
}
z.p = y;
if(z.key < y.key)
y.left = z;
else
y.right = z;
}
}
/**
* 中序遍历二叉搜索树
* @param x 树中结点
* @return 无
*/
public static void inorderTreeWalk(Node x){
if(x!=null){
inorderTreeWalk(x.left);
System.out.print(x.key+"\t");
inorderTreeWalk(x.right);
}
}
/**
* 二叉搜索树中查找一个具有指定关键字的结点
* @param x 树中结点
* @param k 关键字
* @return 无
*/
public static Node search(Node x, int k){
while(x != null && x.key != k){
if(k < x.key)
x = x.left;
else
x = x.right;
}
return x;
}
/**
* 二叉搜索树中关键字最小的结点
* @param x 树中结点
* @return 关键字最小的结点
*/
public static Node minimum(Node x){
while(x.left != null)
x = x.left;
return x;
}
/**
* 二叉搜索树中关键字最大的结点
* @param x 树中结点
* @return 关键字最大的结点
*/
public static Node maxmum(Node x){
while(x.right != null)
x = x.right;
return x;
}
/**
* 结点的后继(中序遍历)
* @param x 树中结点
* @return 结点的后继
*/
public static Node successor(Node x){
// 如果x的右子树不为空,则x的后继为x的右子树中具有最小关键字的结点
if(x.right != null)
return minimum(x.right);
// 如果x的右子树为空,则x的后继为x的最底层祖先,而且它的左孩子也是x的一个祖先(左孩子是x即可)
else{
Node y = x.p;
while(y !=null && x == y.right){
x = y;
y = y.p;
}
return y;
}
}
/**
* 结点的先驱(代码与结点的后继对称)
* @param x 树中结点
* @return 结点的先驱
*/
public static Node predecessor(Node x){
if(x.left != null)
return maxmum(x.left);
else{
Node y = x.p;
while(y !=null && x == y.left){
x = y;
y = y.p;
}
return y;
}
}
/**
* 二叉搜索树内移动子树(用另一棵子树替换一棵子树,并成为其父结点的孩子结点)
* @param u 被替换子树的根结点
* @param v 替换子树的根结点
* @return 无
*/
public static void transplant(Node u, Node v){
if(u.p == null)
root = v;
else if(u == u.p.left)
u.p.left = v;
else
u.p.right = v;
if(v != null)
v.p = u.p;
}
/**
* 删除指定结点
* @param z 待删除结点
* @return 无
*/
public static void delete(Node z){
// 如果z最多有一个孩子结点,则直接调用transplant
if(z.left == null)
transplant(z, z.right);
else if(z.right == null)
transplant(z, z.left);
// 如果z两个孩子结点都存在,则寻找其后继
else{
// z的后继
Node y = minimum(z.right);
if(y.p != z){
transplant(z, z.right);
y.right = z.right;
y.right.p = y;
}
transplant(z, y);
y.left = z.left;
y.left.p = y;
}
}
public static void main(String[] args){
Random rand = new Random();
// 结点数组
Node[] node = new Node[10];
int i = 0;
System.out.println("生成二叉树结点并插入树中:");
for(i = 0; i < node.length ;i++){
node[i] = new Node(null, null, null, rand.nextInt(100) + 1);
System.out.print(node[i].key+"\t");
insert(node[i]);
}
// 中序遍历
System.out.println("\n"+"中序遍历二叉搜索树:");
inorderTreeWalk(root);
// 查找指定结点
Node x = search(root, node[5].key);
System.out.println("\n"+"查找结果:");
System.out.println("自身关键字:"+x.key+"\t"+"父结点的关键字:"+x.p.key);
// 具有最小关键字的结点
x = minimum(root);
System.out.println("树中最小关键字:"+x.key);
// 具有最大关键字的结点
x = maxmum(root);
System.out.println("树中最大关键字:"+x.key);
// x的后继
x = predecessor(node[5]);
System.out.println("前驱的关键字:"+x.key);
// x的前驱
x = successor(node[5]);
System.out.println("后继的关键字:"+x.key);
// 删除结点,并中序输出观看结果
delete(node[5]);
System.out.println("删除结点:");
inorderTreeWalk(root);
}
}