引言
公众号原文链接:教你玩转二叉查找树的结点插入操作 压轴出场-二叉查找树的结点删除操作 希望点进去的小伙伴关注一下我的公众号哟,文末有二维码,谢谢!
完整项目我已经上传到我的码云git仓库上了,如果就需要的话请访问我的码云git仓库获取,附上地址:https://gitee.com/bobo_tea/datastructure。或者点击公众号底部菜单->资源汇总->仓库汇总。或者联系我。
1、二叉查找树的结点插入
现有只有一个结点的二叉查找树,该结点的值为50,现在我要往这颗二叉查找树依次插入9个结点:42,57,13,72,47,65,88,93,26。
这个就是典型的二叉查找树的插入操作了,最后生成的树一共有10个结点,插入过程中不能违背二叉查找树的定义,即每插入一个结点后它仍然是一颗二叉查找树。
下面展示一下用Java代码实现二叉查找树的插入操作核心代码。
ADTInsert,插入操作核心实现类
import java.util.ArrayList;
import java.util.List;
/**
* 二叉查找树结点插入操作类
*/
public class ADTInsert {
/**
* 存放插入的结点
* 这种插入算法最终会将要插入的值会生成一个叶子结点,也就是说不会破坏原来树的结构,虽然简单,但会导致树越来越不平衡
*/
private List<ADTNode> insertNodes = new ArrayList();
/**
* 插入操作核心方法
* @param adtNode 从该结点开始搜索
* @param key 要插入的值
*/
private void insert(ADTNode adtNode,int key){
// 如果节点指针是null,就没法插入,直接返回
if(adtNode == null){ return; }
if(key < adtNode.getVal()){
// 作为左孩子,找一个叶子结点插入
if(adtNode.getLeft() == null){
adtNode.setLeft(new ADTNode(key));
insertNodes.add(adtNode.getLeft());
return;
}
insert(adtNode.getLeft(),key);
}
if(key > adtNode.getVal()){
// 作为右孩子,找一个叶子结点插入
if(adtNode.getRight() == null){
adtNode.setRight(new ADTNode(key));
insertNodes.add(adtNode.getRight());
return;
}
insert(adtNode.getRight(),key);
}
}
/**
* 插入操作入口
* @param root 从该结点开始搜索
* @param keys 要插入的值数组
*/
public void insertEntrance(ADTNode root,int[] keys){
insertNodes.add(root);
for (int key:keys){
this.insert(root,key);
}
}
public List<ADTNode> getInsertNodes() {
return insertNodes;
}
}
ADTMain,二叉查找树结点插入示例类
import com.bobo.group.common.CommonUtil;
import com.bobo.group.tree.draw.DrawTree;
/**
* 二叉查找树main方法
*/
public class ADTMain {
public static void main(String[] args) {
//根结点
ADTNode root = new ADTNode(50);
ADTInsert adtInsert = new ADTInsert();
//依次插入9个结点
adtInsert.insertEntrance(root,new int[]{42,57,13,72,47,65,88,93,26});
//遍历打印各结点
adtInsert.getInsertNodes().forEach(item->{
System.out.println(item == null?"null":item.toString());
});
//生成二叉树图片
DrawTree drawTree = new DrawTree();
drawTree.drawEntrance(root, CommonUtil.getResourceRoot() +"tree/adt/insert_10.png",10);
}
}
运行main方法,输出结果如下。
ADTNode{val=50, left=42, right=57}
ADTNode{val=42, left=13, right=47}
ADTNode{val=57, left=null, right=72}
ADTNode{val=13, left=null, right=26}
ADTNode{val=72, left=65, right=88}
ADTNode{val=47, left=null, right=null}
ADTNode{val=65, left=null, right=null}
ADTNode{val=88, left=null, right=93}
ADTNode{val=93, left=null, right=null}
ADTNode{val=26, left=null, right=null}
生成的图片如下。
如果中序遍历这棵树的话,其结果是一个从小到大排好序的序列。
但是有没有觉得这棵树的形状有点奇怪,看起来有点斜(主要是右边)、有点不平衡。
其实二叉查找树的极端恶劣的情况就是一颗斜树,如下图所示。
这样的树也符合二叉查找树的定义,但无异于一个单链表,对查找速度并没有任何提升,因此,要避免二叉查找树出现过多的斜树。
而怎样才算一颗好的二叉查找树呢?当然是树的高度越小,查找速度越快。
上面代码中的插入算法,并不是一个好的插入算法,因为最后树的形状完全取决于结点的插入顺序,并且插入过程中不会改变树原来的结构,也就是说新插入的结点始终会被当成叶子结点插入。
比如现在二叉查找树只有一个值是50的结点,我要依次插入50,70,80,90四个结点,那最后生成的树就是一颗斜树,如下图所示。
而如果我的插入算法足够好,那生成的树应该是下面这个样子,当然,插入的结点越多,效果差距越大。
本文暂不介绍好的插入算法,后面我会讲平衡二叉树来解决这个问题。
2、二叉查找树的结点删除
前面介绍了二叉查找树的查找和插入,还算比较简单,因为它们均不会破坏树的结构。而二叉查找树的删除,可就不是那么简单了。
-
如果删除的是叶子结点,则直接删除该结点,不会对树的结构造成影响
-
如果删除的结点只有一个孩子,则会对树的结构造成影响,但也还好办,将被删除的结点替换为该节点的孩子即可
-
如果删除的结点左右孩子都有,那情况就有点复杂了,稍有不慎就会破坏树的结构使之不满足二叉查找树的定义。
对于第三种情况,我们下面来探讨一下解决办法:
-
因为二叉查找树的中序遍历的结果是从小到大的,如果将其中某个结点删除,则可以用该结点的前驱或后继来替代该结点,使之仍然能保持二叉查找树的结构,用前驱代替还是用后继代替都可以。
下面用图来描述一下。
算法大概就是这样,下面主要看实现过程吧,相比于查找和插入两种操作而言,删除操作的代码量比较大,也比较难于理解。
ADTDelete,二叉查找树结点删除核心代码
/**
* 二叉查找树结点删除操作类
*/
public class ADTDelete {
/**
* 是否使用前驱结点代替被删除结点
*/
private static final boolean USE_BEFORE = true;
/**
* 删除操作入口
*/
public void deleteEntrance(ADTNode adtNode,int key){
//空树,不删除
if(adtNode == null){
return;
}
//删除根结点
if(adtNode.getVal() == key){
System.out.println("天啊,要删除根结点,暂不支持!");
return;
}
searchForDelete(adtNode,key);
}
/**
* 删除结点核心方法
* @param parentNode 父结点
* @param keyNode 要删除的结点
* @param isLeft 是否是左孩子
*/
private void delete(ADTNode parentNode,ADTNode keyNode,boolean isLeft){
//叶子结点,删除轻而易举
if(keyNode.getLeft() == null && keyNode.getRight()==null){
if(isLeft){
parentNode.setLeft(null);
}else{
parentNode.setRight(null);
}
return;
}
//只有一个孩子结点,删除也容易
if(keyNode.getLeft() == null && keyNode.getRight()!=null){
if(isLeft){
parentNode.setLeft(keyNode.getRight());
}else{
parentNode.setRight(keyNode.getRight());
}
return;
}
if(keyNode.getLeft() != null && keyNode.getRight()==null){
if(isLeft){
parentNode.setLeft(keyNode.getLeft());
}else{
parentNode.setRight(keyNode.getLeft());
}
return;
}
//左右孩子都有,比较复杂
if(keyNode.getLeft() != null && keyNode.getRight() != null){
//先找到替代者,前驱和后继都可以是替代者
//前驱要么是叶子结点,要么只有左子树;后继要么是叶子结点,要么只有右子树
if(USE_BEFORE){
if(keyNode.getLeft().getRight() == null){
//备份右子树
ADTNode rightNode = keyNode.getRight();
//替换
keyNode = keyNode.getLeft();
//续接
keyNode.setRight(rightNode);
}else{
//先找到替代者的父亲
ADTNode maxBeforNodeParent = searchMaxForDelete( keyNode.getLeft());
//备份左子树、右子树
ADTNode leftNode = keyNode.getLeft();
ADTNode rightNode = keyNode.getRight();
ADTNode replacement = maxBeforNodeParent.getRight();
ADTNode replacementLeft = maxBeforNodeParent.getRight().getLeft();
//将替代者从原来的地方删除,并接上它的左孩子
maxBeforNodeParent.setRight(replacementLeft);
//替换
if(isLeft){
parentNode.setLeft(replacement);
}else{
parentNode.setRight(replacement);
}
//第四步:将被删除结点的左子树与右子树与替代者结点连接上
replacement.setLeft(leftNode);
replacement.setRight(rightNode);
}
}else{
System.out.println("暂不支持替换后继");
}
}
}
/**
* 查找前驱
* @param adtNode 开从该结点开始搜索
* @return 前驱结点的父亲
*/
private ADTNode searchMaxForDelete(ADTNode adtNode){
if(adtNode.getRight().getRight() != null){
return searchMaxForDelete( adtNode.getRight());
}
return adtNode;
}
/**
* 查找后继
* @param adtNode 从该结点开始搜索
* @return 后继结点的父亲
*/
private ADTNode searchMinForDelete(ADTNode adtNode){
if(adtNode.getLeft().getLeft() != null){
return searchMaxForDelete( adtNode.getLeft());
}
return adtNode;
}
/**
* 在二叉查找树上递归搜索要删除的结点,找到了就调用delete方法删除
* @param adtNode 从该结点开始搜索
* @param key 要删除的结点的值
*/
private void searchForDelete(ADTNode adtNode,int key){
if(key < adtNode.getVal()){
if(adtNode.getLeft() == null){
System.out.println("死胡同了-找不到要删除的结点-"+key);
return;
}
if(key == adtNode.getLeft().getVal()){
//找到了,执行删除
delete(adtNode, adtNode.getLeft(),true);
}else{
//否则,向下走一个结点
searchForDelete( adtNode.getLeft(),key);
}
}
if(key > adtNode.getVal()){
if(adtNode.getRight() == null){
System.out.println("死胡同了-找不到要删除的结点-"+key);
return;
}
if(key == adtNode.getRight().getVal()){
//找到了,执行删除
delete(adtNode,adtNode.getRight(),false);
}else{
//否则,向下走一个结点
searchForDelete(adtNode.getRight(),key);
}
}
}
}
ADTMain,二叉查找树删除示例代码
import com.bobo.group.common.CommonUtil;
import com.bobo.group.tree.draw.DrawTree;
/**
* 二叉查找树main方法
*/
public class ADTMain {
public static void main(String[] args) {
ADTDelete adtDelete = new ADTDelete();
ADTNode root = ADTBuilder.buildBySample_2();
adtDelete.deleteEntrance(root,47);
DrawTree drawTree = new DrawTree();
drawTree.drawEntrance(root,CommonUtil.getResourceRoot()+"tree/adt/删除47.png",false);
}
}
运行main方法,生成的图片如下,这正是我们想要的结果。
我的二维码
觉得写得不错的小伙伴,扫码关注一下我的公众号吧,谢谢呀!