模拟实现二叉搜索树
概念❓
二叉搜索树又叫二叉排序树,它或者是一颗空树,或者是具有以下性质的一棵树:
- 一个根若有左子树,则左子树上所有的节点值都比这个根的节点值来的小
- 一个根若有右子树,则右子树上所有节点的值都比这个根的节点值来的大
- 二叉搜索树的任意一棵子树也要满足前述两条要求,也即任何一颗子树也必须是一棵二叉搜索树
基本功能🏎
- 增
- 删(较复杂)
- 查
- 改
增📦
要清楚,在二叉搜索树上新增节点,新节点都是长在没加节点时树的叶子上的,只不过问题在于新节点长在哪个叶子上,所以首当其冲就是找到新叶子的位置,找位置就是利用搜索树的前两条性质,其次加新节点,类似于单链表的尾插,我们得知道新叶子的前驱是谁.
代码:
class Node{
public int val;
public Node left;
public Node right;
public Node(int val){
this.val=val;
}
}
public class BinarySearchTree(){
public Node root;
//增加新节点
public void insert(int data){
if(root==null){
Node node=new Node(data);
this.root=node;
return;
}
Node node=new Node(data);
Node parent=null;
Node cur=root;
while(cur!=null){
parent=cur;
if(data>cur.val){
cur=cur.right;
}else if(data==cur.val){
return;
}else{
cur=cur.left;
}
}
if(parent.val>data){
parent.left=node;
}else{
parent.right=node;
}
}
}
查🤕
较简单,不再赘述.
代码
public boolean contains(int data){
Node cur=root;
while(cur!=null){
if(cur.val==data){
return true;
}else if(cur.val>data){
cur=cur.left;
}else{
cur=cur.right;
}
}
return false;
}
删✌️
较麻烦,需采取枚举的手段对所有情况进行罗列,针对不同的情况进行讨论,讨论时,我们可以先假设找到了这个指定节点值的待删除节点,因为删除节点就类似于单链表所提到过的,只是通过修改前驱的指向来达到删除的效果,但是待删除节点如果在树的中间位置,所波及到的节点指向问题可能涉及到3个节点,较为复杂,所以可以考虑删节点之前把树做一些准备工作,这里采取的方法是:用待删除节点的右子树的最左下角的节点值把待删除节点的值进行覆盖,为什么这样做?因为搜索树的性质决定了待删除节点右子树的最左下角的那个节点值被放到待删除节点的位置,整棵搜索树的本质才不会变,之所以要交换二者,是因为交换完成之后,我们只需要把待删除节点右子树的最左下角的那个节点进行删除即可,那此过程就只会涉及到最左下角的节点及其前驱的链接情况的讨论而已,事情大大被简化
代码
public void remove(int data){
//删除节点之前,需要找到该节点及其前驱
Node cur=root;
Node prev=null;
while(cur!=null){
if(cur.val==data){
removeNode(cur,parent);
break;
}else if(cur.val>data){
parent=cur;
cur=cur.left;
}else{
parent=cur;
cur=cur.right;
}
}
}
//上述完成了待删除节点及其前驱的查找,接下来就是讨论的情况
//待删除节点的情况可分为以下几种:cur.left==null/cur.right==null/cur.left!=null&&cur.right!=null
private void removeNode(Node cur,Node parent){
//cur.left==null
if(cur.left==null){
//若cur是根的话,是没有前驱的,所以需单独考虑以下
if(cur==root){
root=root.right;
}else if(cur==parent.left){
parent.left=cur.right;
}else{
parent.right=cur.right;
}
}else if(cur.right==null){
if(cur==root){
root=root.left;
}else if(cur==parent.left){
parent.left=cur.left;
}else{
parent.right=cur.left;
}
}else{
//cur.left!=null&&cur.right!=null,上述两个if语句已经考虑左右都为空时的情况2次,所以针对cur左右孩子的情况,这是最后一种
//这里就是找待删节点右子树的最左下角节点值,覆盖待删除节点的节点值,并删除那个左下角节点的讨论
Node father=cur;
Node child=cur.right;
while(child.left!=null){
father=child;
child=child.left;
}
//除了while循环,child就是指向了那个最左下角的节点了
cur.val=child.val;//覆盖
//对左下角节点的改变链接状况式的删除
//当father和cur一样时:child就是father的右孩子
//当father和child追赶至father离开了cur,那此时father的左孩子才是child
//上述两个情况的讨论尤为关键
if(father.left==child){
father.left=child.right;
}else{
father.right=child.right;
}
}
}
改🖌
有两个思路:一个是拿着就的节点值去找搜索树中有没有这个节点,找到这个节点后,把这个节点的值改成新的节点值即可;第二个思路是:实现了删除操作之后,先把老节点值对应的节点删除之后,再利用增操作,新增新的节点值即可.本文利用后者.代码直接调用上述的增和删操作即可,不再赘述.
搜索树的性能分析🌵
- 插入和删除都必须先查找,查找效率代表了二叉搜索树的各个操作的性能
- 对于有n个节点的二叉搜索树,若每个元素被查找的概率相等,则二叉搜索树的平均查找长度是节点在二叉搜索树的深度的函数,即几点越深,查找的次数就会越多
- 对于同一个关键码的集合,如果关键码的插入次序不同,可能会得到不同的二叉搜索树
- 最优查找情况:二叉搜索树为完全二叉树:其平均比较次数为:logn
- 最差查找情况,二叉搜索树为单支树,其平均比较次数为:n/2