二叉排序树简介
二叉排序树实际上是在二叉树的基础上添加了三条规则得来的。
一.定义
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值。
(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值。
(3)左、右子树也分别为二叉排序树。
这是一个一般的二叉排序树
二.基本操作
这里用Java实现
首先来定义树的结点类
class Node{
int key;//结点值
Node parent;//父节点
Node Leftchild;//左子节点
Node Rightchild;右子节点
boolean isdelete;//是否删除的标志位
Node(int value){
this.value=value;
}
public void display(){
System.out.print(this.value + "\t");
}
@Override
public String toString() {
return String.valueOf(value);
}
}
下面就是二叉排序树的基本操作了:
- 遍历(这里主要指中序遍历,因为中序遍历出的结果恰好是一个升序序列)
- 查找
- 最小值和最大值
- 插入
- 删除
遍历(递归方式)
中序遍历:首先获得左子节点,然后是父节点,最后是右子节点。
//中序遍历
public void infixOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.data+" ");
infixOrder(current.rightChild);
}
}
先序遍历:先输出父节点,再输出左子节点,最后是右子节点
//前序遍历
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
preOrder(current.leftChild);
preOrder(current.rightChild);
}
}
后序遍历:先输出左子节点,再输出右子节点,最后是父节点
public void postOrder(Node current){
if(current != null){
postOrder(current.leftChild);
postOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
查找
这里使用的是迭代的方式来进行查找,为什么使用迭代呢?
递归在调用的时候会进行一次入栈和一次出栈,每调用一次就会进行一次出入栈,这产生了额外的运算量
使用迭代的一个好处就是去除了频繁的出入栈操作,节约了运算资源
//查找
public Node findKey(int value){
Node current = root;
while(current != null){
if(current.data > key){//当前值比查找值大,搜索左子树
current = current.leftChild;
}else if(current.data < key){//当前值比查找值小,搜索右子树
current = current.rightChild;
}else{
return current;
}
}
return null;//遍历完整个树没找到,返回null
}
查找方式的解释:
先判断所要查找的值和当前节点的value的大小关系。如果所要查找的值大于value,说明要找的值在结点的右子树里,如果比value小就说明在左子树里,否则就是当前结点。只要不是当前节点,就进行一次迭代,大于就迭代到右子节点,小于就迭代到左子节点。
时间复杂度
可以看出在最坏的情况下,想要找到一个值就会不停的向深进行,所以最坏的时间复杂度为O(h),这里h是该二叉排序树的最大高度
最小值和最大值
实现查找最大值和最小值其实很简单。
为了找到最小值,只需要从根节点开始一直沿着Leftchild指针向下搜索,总能找到一个最小值。
同理,最大值就只需一直沿着Rightchild指针向下搜索即可
这里给出最大值和最小值的代码:
//查找以节点x为根的子树的最小关键字并返回其节点指针
public Node tree_Minimum(Node x)}
while (x.Leftchild !=null)
{
x = x.Leftchild; // 从树根x沿着Leftchild指针一直往下找,直到遇到一个NIL
}
return x;
}
// 查找以节点x为根的子树的最大关键字并返回其节点指针
public Node tree_Maximum(Node x)}
while (x.Leftchild != null)
{
x = x.Rightchild; // 从树根x沿着Rightchild指针一直往下找,直到遇到一个NIL
}
return x;
}
插入
插入的方式和查找有很大的相似度,插入前会new 一个新节点并为其value赋值。然后为了找到插入的位置,和查找的原理一样,先从根节点开始进行遍历,逐步向下,直到找到一个合适的位置。
注意:所有插入的结点都是叶子节点,也就是他们都位于树的最底层
public boolean insert(int data) {
Node newNode = new Node(data);
if(root == null){//当前树为空树,没有任何节点
root = newNode;
return true;
}else{
Node current = root;
Node parentNode = null;
while(current != null){
parentNode = current;
if(current.data > data){//当前值比插入值大,搜索左子节点
current = current.leftChild;
if(current == null){//左子节点为空,直接将新值插入到该节点
parentNode.leftChild = newNode;
return true;
}
}else{
current = current.rightChild;
if(current == null){//右子节点为空,直接将新值插入到该节点
parentNode.rightChild = newNode;
return true;
}
}
}
}
return false;
}
删除
删除可以算是树中操作实现最难的一部分,因为它不仅有多种情况,还要考虑到删除后是否还能符合二叉排序树的定义。
在不考率结点空间是否要回收的情况下有一种简单的办法就是为结点添加一个标记位,为想要删掉的结点做个标记,在遍历时判断标志位是否为删除结点。
当然上面的办法只是一种投机取巧,事实上在数据量较多的时候并不提倡这样子做。
假设要删除的结点为z,下面就来分析删除的各种情况:
- 第一种:z没有子节点。这种情况是最简单的,只需要将它删掉之后把其父节点的相应指针清空即可
- 第二种:z只有一个子树。如果z只有左子树,就用其左子树替换z,如果只有右子树,就用其右子树替换z
右子树替换:
左子树替换:
- 第三种:z有两个子节点。那么找z的后继,并让后继占据z的位置。z的原来右子树部分成为y的新的右子树,z的原来左子树部分成为y新的左子树。这里要注意,z的后继y一定在z的右子树中,并且没有左孩子。利用z的后继y替换z,又细分为以下两种情况:
- 如果后继是z的右子节点,那么直接用后继替换z,并保留后继的右子树
- 如果后继不是z的右子节点,那么先用后继的右子节点替换后继结点,然后再用后继替换z。
可以发现,删除的时候会有一个移动子树的操作(无论它是否为空),所以先定义一个移动子树的方法
- 如果后继是z的右子节点,那么直接用后继替换z,并保留后继的右子树
// 用一棵以v为根的子树来替换一棵以u为根的子树,节点u的双亲变成节点v的双亲,并且v成为u的双亲的相应孩子
public void transplant(Node T, Node u, Node v)
{
if (u.parent == null){ // 节点u为根节点时
T = v; // 节点v直接替换u作为根节点
}else if (u == u.parent.Leftchild){ // 节点u是其父节点的左孩子
u.parent.Leftchild = v; // 父节点的左孩子指向节点v
}else{ // 节点u是其父节点的右孩子
u.parent.Rigtchild = v;// 父节点的右孩子指向节点v
}
if (v != null){
v.parent = u.parent;
} // 更新节点v的父节点指针
}
再给出删除的代码:
public Node tree_Delete(Node T, Node z)
{
Node y = null;
if (z-。Leftchild == null){ // 若z的左孩子为NIL,直接用z的右孩子替换z
transplant(T, z, z.Rightchild);
}else if (z.Rightchild == null){ // 若z的右孩子为NIL,直接用z的左孩子替换z
transplant(T, z, z.Leftchild);
}else{ // 若z有两个孩子
// 先让y为z的后继,也即z的右子树中的最小关键字
// y肯定没有左孩子,否则最小关键字就是那个左孩子而不是y了
y = tree_Minimum(z.Rightchild);
if (y->parent != z) // 若y不是z的右孩子
{
transplant(T, y, y.Rightchild); // 先用y的右孩子替换y
y.Rightchild = z.Rightchild; // y的右子树指针指向原来z的右子树指针
y.Rightchild.parent = y; // 原来z的右子树(现为y的右子树)的父节点指针更新为y
}
transplant(T, z, y); // 然后再用y替换z
y.Leftchild = z.Leftchild; // y的左子树指针指向原来z的左子树指针
y.Leftchild.parent = y; // 原来z的左子树(现为y的左子树)的父节点指针更新为y
}
return z;
}
二叉排序树虽好,但是还是没有有效地避免在一些情况下树的高度过高的情况,树过高的话会导致查找性能严重下降。后面会引出2,3查找树以及B-tree,B+树等等,他们都是利用不同的规则来限制树高。