自己动手写数据结构(12)——平衡二叉树(详解结点删除)

自己动手写数据结构总目录

具体内容:

该文章的源代码仓库为:https://github.com/MeteorCh/DataStructure/blob/master/Java/DataStructure/src/Searching/BinarySortingTree/AVLTree.java
在上一篇博客中实现了二叉排序树。我们发现,查找的效率取决于树的高度。比如对于{62,88,58,47,35,73,51,99,37,93}这样的数组,利用上一篇博客实现的插入,得到的二叉排序树为:
在这里插入图片描述
但当我们改变一下数组中元素的顺序,比如改成{35,37,47,51,58,62,73,88},那得到的二叉排序树就为:
在这里插入图片描述
不难看出,这时的二叉排序树退化成了一个线性表。这时已经完全没有了二叉排序树的优势。次吃的解决方案是就是尽量让树的高度降低,那就要求树的左右孩子的高度尽可能相近,即树要尽可能平衡。

一、相关定义

  • 1.平衡二叉树:一种二叉排序树, 其中每个节点的左右子树的高度差至多等于1。这里有两个地方要注意:第一,平衡二叉树首先是一颗二叉排序树;第二,每个节点的左右子树高度差要么是0,要么是1。
  • 2.平衡因子: 为了描述节点的左右子树差,我们将二叉树上节点的左子树高度减去右子树高度得到的值称为该节点的平衡因子。根据二叉树的定义,每个节点的平衡因子为-1,0,1三个中的一个。
  • 3.最小不平衡子树: 距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树, 称为最小不平衡子树。如下所示的二叉排序树,当插入35时,58的左子树高度为2,右子树高度为0,该树变不平衡,因此以节点58为根的子树为最小不平衡子树。
    在这里插入图片描述

二、平衡调整

从上面的图可以看出,要让不平衡的二叉排序树重新平衡,就需要对最小不平衡子树进行调整,使其平衡。有四种调整方式:LL型、RR型、LR型、RL型。在月雲之霄 大佬的博客中,对这四种调整讲的相当清楚了,我这里将月雲之霄 大佬的博客中的图整理成一张图(本文重点讲解删除的各种情况,插入的情况可以先去看看月雲之霄大佬的博客,讲的可以说非常好了),用来解释调整情况:
在这里插入图片描述

三、节点插入

通过上面的图示,我们知道LR和RL是LL和RR的组合情况。知道了这个,我们在插入节点的时候,从上往下,利用递归,插入后,判断插入过程路径上的节点的平衡因子,当平衡因子大于1或小于-1时,就去调整该接节点(根据递归的特性,在寻找插入点位置时,是从上到下的,那在判断平衡因子时,就是从下往上的,这样其实调整的肯定就是最小不平衡子树了)。在月雲之霄 大佬的博客中对插入讲的相当详细了,我这里就不赘述了,可以详细看一遍大佬的博客,肯定能理解。我这里用Java实现平衡二叉树,Java中没有C++中delete的操作,所以每个节点都要有一个成员变量存储其父节点信息。在左旋和右旋的时候,要调整对应的父节点信息,我们这里先来看看左旋和右旋的两种情况。

1.LL型旋转(即右旋)

这里仍然使用月雲之霄 大佬的博客中的图来说明:
在这里插入图片描述
实现代码为:

	protected TreeNode llRotate(TreeNode node){//LL型旋转
        TreeNode xNode=node.lChild;
        node.lChild=xNode.rChild;
        if (xNode.rChild!=null)
            xNode.rChild.parent=node;//父节点调整
        xNode.rChild=node;
        if (xNode.rChild!=null)
            xNode.rChild.parent=xNode;//父节点调整
        node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
        xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
        return xNode;
    }

可以看到上面对x和y的父节点都进行了一些调整操作。

2.RR型旋转(即左旋)

使用月雲之霄 大佬的博客中的图来说明:
在这里插入图片描述
实现代码为:

	protected TreeNode rrRotate(TreeNode node){//RR型旋转
        TreeNode xNode=node.rChild;
        node.rChild=xNode.lChild;
        if (xNode.lChild!=null)
            xNode.lChild.parent=node;//父节点调整
        xNode.lChild=node;
        if (xNode.lChild!=null)
            xNode.lChild.parent=xNode;//父节点调整
        node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
        xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
        return xNode;
    }

上面也对x和y的父节点都进行了一些调整操作。
LR型插入和RL型插入是LL和RR型的组合,都会调用上面两个函数,我这里就不写了。有这两个函数,我们再来看平衡二叉树中的节点删除问题。

四、节点删除

我感觉节点删除是平衡二叉树中最难的部分了,删除的以后要判断调整的类型。当然,平衡调整类型仍然是LL、RR、LR、RL这四种,但是,与插入不同的是,在结点删除的时候,要考虑调整结点是父节点的左孩子还是右孩子,所以每种类型又有两种情况,总共是8种情况。 关于为什么在插入的时候不需要考虑父节点,而在删除的时候要考虑父节点,我们在讨论部分讨论。

1.LL型删除

1.1 LLL型删除

这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LLL型调整,具体情况如下所示,要删除20节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的左孩子(LLL中第一个L的含义)。
在这里插入图片描述
假设y的父节点为parent,LLL型删除对应的代码为

	parent.lChild=llRotate(y);
	parent.lChild.parent=parent;

1.2 RLL型删除

这种情况是最小不平衡子树的根节点为其父节点的右孩子的情况,我这里称之为RLL型调整,具体情况如下所示,要删除80节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的右孩子(RLL中R的含义)。
在这里插入图片描述
假设y的父节点为parent,RLL型删除对应的代码为

	parent.rChild=llRotate(y);
	parent.rChild.parent=parent;

2.RR型删除

2.1 LRR型删除

这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LRR型调整,具体情况如下所示,要删除0节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的左孩子(LRR中L的含义)。
在这里插入图片描述
假设y的父节点为parent,LRR型删除对应的代码为

	parent.lChild=rrRotate(y);
	parent.lChild.parent=parent;

2.2 RRR型删除

这种情况是最小不平衡子树的根节点为其父节点的右孩子的情况,我这里称之为RRR型调整,具体情况如下所示,要删除80节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整部分中的y为其父节点的右孩子(RRR中第一个R的含义)。
在这里插入图片描述
假设y的父节点为parent,RRR型删除对应的代码为

	parent.rChild=rrRotate(y);
	parent.rChild.parent=parent;

3.LR型删除

3.1 LLR型删除

这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LLR型调整,具体情况如下所示,要删除20节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行RR型调整,再对y节点进行LL型调整。调整部分中的y为其父节点的左孩子(LLR中第一个L的含义)。
在这里插入图片描述
假设y的父节点为parent,LLR型删除对应的代码为:

	y.lChild =  rrRotate(y.lChild);
    y.lChild.parent=y;
    parent.lChild=llRotate(y);
    parent.lChild.parent=parent;

3.2 RLR型删除

这种情况是最小不平衡子树的根节点为父节点的右孩子的情况,我这里称之为RLR型调整,具体情况如下所示,要删除90节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行RR型调整,再对y节点进行LL型调整。调整部分中的y为其父节点的右孩子(RLR中第一个R的含义)。
在这里插入图片描述
假设y的父节点为parent,RLR型删除对应的代码为:

	y.lChild =  rrRotate(y.lChild);
    y.lChild.parent=y;
    parent.rChild=llRotate(y);
    parent.rChild.parent=parent;

4. RL型删除

4.1 LRL型删除

这种情况是最小不平衡子树的根节点为父节点的左孩子的情况,我这里称之为LRL型调整,具体情况如下所示,要删除0节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行LL型调整,再对y节点进行RR型调整。调整部分中的y为其父节点的左孩子(LRL中第一个L的含义)。
在这里插入图片描述
假设y的父节点为parent,LRL型删除对应的代码为:

	y.rChild =llRotate(y.rChild);
    y.rChild.parent=y;
    parent.lChild=rrRotate(y);
    parent.lChild.parent=parent;

4.2 RRL型删除

这种情况是最小不平衡子树的根节点为父节点的右孩子的情况,我这里称之为RRL型调整,具体情况如下所示,要删除50节点,左图中红圈标记出来的部分会引起不平衡,我们要对其进行调整。调整时先对x节点进行LL型调整,再对y节点进行RR型调整。调整部分中的y为其父节点的右孩子(RRL中第一个L的含义)。
在这里插入图片描述
假设y的父节点为parent,RRL型删除对应的代码为:

	y.rChild =llRotate(y.rChild);
    y.rChild.parent=y;
    parent.rChild=rrRotate(y);
    parent.rChild.parent=parent;

五、代码实现

用Java实现平衡二叉树,代码如下:

package Searching.BinarySortingTree;

/**
 * 平衡二叉树的实现
 * 使用示例:
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         int findKey=30;
 *         System.out.println("键"+findKey+"在平衡二叉树中"+(tree.find(findKey)==null?"不存在":"存在"));
 * 删除的各类情况示例:
 *      1.右子树中LL型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.deleteNode(70);
 *         tree.deleteNode(90);
 *         tree.deleteNode(80);
 *      2.左子树中LL型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.insertData(-1);
 *         tree.deleteNode(20);
 *      3.右子树中RR型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.deleteNode(70);
 *         tree.deleteNode(90);
 *         tree.deleteNode(80);
 *      4.左子树中RR型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.insertData(25);
 *         tree.deleteNode(0);
 *      5.右子树中LR型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.deleteNode(40);
 *         tree.deleteNode(50);
 *         tree.deleteNode(90);
 *      6.左子树中LR型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.insertData(5);
 *         tree.deleteNode(20);
 *      7.右子树中RL型删除
 *         int[] data={20,10,0,30,40,50,60,80};
 *         AVLTree tree=new AVLTree(data);
 *         tree.deleteNode(40);
 *         tree.insertData(75);
 *         tree.deleteNode(50);
 *      8.左子树中RL型删除
 *         int[] data={20,10,0,30,40,50,60,90,80,70};
 *         AVLTree tree=new AVLTree(data);
 *         tree.insertData(15);
 *         tree.deleteNode(0);
 */
public class AVLTree {
    public static class TreeNode{
        protected int data;//数据
        protected TreeNode lChild,rChild,parent;//左孩子、右孩子、父节点
        protected int height;//树的高度

        public TreeNode(int data){
            this.data=data;
            lChild=rChild=parent=null;
            height=1;
        }

        public int getData() {
            return data;
        }
    }

    TreeNode root;

    public AVLTree(int[] data){
        for (int item:data)
            insertData(item);
    }

    public TreeNode find(int key){//利用非递归的方式实现查找
        TreeNode curNode=root;
        while (curNode!=null){
            if (key==curNode.data){
                return curNode;
            }else if (key<curNode.data){
                curNode=curNode.lChild;
            }else
                curNode=curNode.rChild;
        }
        return null;
    }

    public int getHeight(TreeNode node){
        return node==null?0:node.height;
    }

    public int getBalanceFactor(TreeNode node){
        if (node==null)
            return 0;
        return getHeight(node.lChild)-getHeight(node.rChild);
    }

    protected TreeNode llRotate(TreeNode node){//LL型旋转
        TreeNode xNode=node.lChild;
        node.lChild=xNode.rChild;
        if (xNode.rChild!=null)
            xNode.rChild.parent=node;//父节点调整
        xNode.rChild=node;
        if (xNode.rChild!=null)
            xNode.rChild.parent=xNode;//父节点调整
        node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
        xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
        return xNode;
    }

    protected TreeNode rrRotate(TreeNode node){//RR型旋转
        TreeNode xNode=node.rChild;
        node.rChild=xNode.lChild;
        if (xNode.lChild!=null)
            xNode.lChild.parent=node;//父节点调整
        xNode.lChild=node;
        if (xNode.lChild!=null)
            xNode.lChild.parent=xNode;//父节点调整
        node.height=Math.max(getHeight(node.lChild),getHeight(node.rChild))+1;
        xNode.height=Math.max(getHeight(xNode.lChild),getHeight(xNode.lChild))+1;
        return xNode;
    }

    public void insertData(int data){
        root=insertData(root,data);
        root.parent=null;
    }

    /**
     * 插入数据
     * @param data
     */
    protected TreeNode insertData(TreeNode node,int data) {
        if (node==null) {
            return new TreeNode(data);
        }
        else {
            if (data<node.data) {
                node.lChild=insertData(node.lChild,data);
                node.lChild.parent=node;
            }
            else if (data>node.data) {
                node.rChild=insertData(node.rChild,data);
                node.rChild.parent=node;
            }
            //更新树中节点的高度
            node.height=1+Math.max(getHeight(node.lChild),getHeight(node.rChild));
            int balance=getBalanceFactor(node);
            if (balance>1&&data<node.lChild.data)//LL型
                return llRotate(node);
            if (balance<-1&&data>node.rChild.data)//RR型
                return rrRotate(node);
            if (balance>1&&data>node.lChild.data){//LR型
                node.lChild=rrRotate(node.lChild);
                node.lChild.parent=node;
                return llRotate(node);
            }
            if (balance<-1&&data<node.rChild.data){//RL型
                node.rChild=llRotate(node.rChild);
                node.rChild.parent=node;
                return rrRotate(node);
            }
        }
        return node;
    }

    public void deleteNode(int data){//提供给外部操作的删除节点接口
        deleteNode(root,data);
    }

    public void deleteNode(TreeNode node,int key){
        if (node==null)
            System.out.println("数据表中不存在"+key);
        else {
            if (key==node.data)
                deleteNode(node);
            else if (key<node.data)
                deleteNode(node.lChild,key);
            else
                deleteNode(node.rChild,key);
            node.height=1+Math.max(getHeight(node.lChild),getHeight(node.rChild));
            int balance=getBalanceFactor(node);
            TreeNode tmp=node.parent;//保存变量,因为node.parent在llRotate时会变化
            boolean flag=checkChild(tmp,node);//flag为false代表在左子树上,true代表在右子树上
            if (balance>1&&getBalanceFactor(node.lChild)>=0) {//LL型
                if (flag){//右子树
                    System.out.println("RLL型删除");
                    tmp.rChild=llRotate(node);
                    tmp.rChild.parent=tmp;
                }else {//左子树
                    System.out.println("LLL型删除");
                    tmp.lChild=llRotate(node);
                    tmp.lChild.parent=tmp;
                }
            }
            if (balance > 1 && getBalanceFactor(node.lChild) < 0){ //LR型
                node.lChild =  rrRotate(node.lChild);
                node.lChild.parent=node;
                if (flag){
                    System.out.println("RLR型删除");
                    tmp.rChild=llRotate(node);
                    tmp.rChild.parent=tmp;
                }else {
                    System.out.println("LLR型删除");
                    tmp.lChild=llRotate(node);
                    tmp.lChild.parent=tmp;
                }
            }

            if (balance < -1 && getBalanceFactor(node.rChild) <= 0) {//RR型
                if (flag){//右子树
                    System.out.println("RRR型删除");
                    tmp.rChild=rrRotate(node);
                    tmp.rChild.parent=tmp;
                }else {//左子树
                    System.out.println("LRR型删除");
                    tmp.lChild=rrRotate(node);
                    tmp.lChild.parent=tmp;
                }
            }
            if (balance < -1 && getBalanceFactor(node.rChild) > 0){  //Rl型
                node.rChild = llRotate(node.rChild);
                node.rChild.parent=node;
                if (flag){
                    System.out.println("RRL型删除");
                    tmp.rChild=rrRotate(node);
                    tmp.rChild.parent=tmp;
                }else {
                    System.out.println("LRL型删除");
                    tmp.lChild=rrRotate(node);
                    tmp.lChild.parent=tmp;
                }
            }
        }
    }

    protected boolean checkChild(TreeNode parent,TreeNode child){
        //这里需要判断是接到左子树上还是右子树上
        if (parent!=null){
            if (parent.lChild==child)
                return false;
            else
                return true;
        }
        return false;
    }

    protected void deleteNode(TreeNode node){//节点的移除操作,分为节点有左子树或右子树为空,左右子树均不为空两种情况
        if (node.rChild==null||node.lChild==null){//右子树或右子树为空
            TreeNode replace=node.rChild==null?node.lChild:node.rChild;
            if (node.parent!=null){
                if (node==node.parent.lChild)
                    node.parent.lChild=replace;
                else if (node==node.parent.rChild)
                    node.parent.rChild=replace;
                if (replace!=null)
                    replace.parent=node.parent;
            }else { //如果node为根节点
                root=replace;
                replace.parent=null;
            }
            //清空node释放内存
            node.parent=node.lChild=node.rChild=null;
        }else {
            //找到node的左子树中最右边的叶节点
            TreeNode curNode=node.lChild;
            while (curNode.rChild!=null){
                curNode=curNode.rChild;
            }
            //将curNode的值直接赋值给删除节点
            node.data=curNode.data;
            deleteNode(curNode);
        }
    }
}

在最上面的注释里,我将各种删除情况的示例都备注了,想试的小伙伴可以试试。其实我们发现,上面的删除部分,deleteNode(TreeNode node) 这个函数的实现和上一篇博客中的代码是一模一样的,不理解的可以先看看上一篇博客

六、讨论

1.为什么插入的时候不需要考虑左右子树问题?

这个思考一下,就可以知道,插入时,是确定了在左子树还是右子树上插入后,再进行插入调整操作。而删除时,是先删除,然后再判断二叉树是否平衡,然后调整。

  • 16
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
1. 什么是二叉树二叉树是一种树形结构,其中每个节点最多有两个子节点。一个节点的左子节点比该节点小,右子节点比该节点大。二叉树通常用于搜索和排序。 2. 二叉树的遍历方法有哪些? 二叉树的遍历方法包括前序遍历、中序遍历和后序遍历。前序遍历是从根节点开始遍历,先访问根节点,再访问左子树,最后访问右子树。中序遍历是从根节点开始遍历,先访问左子树,再访问根节点,最后访问右子树。后序遍历是从根节点开始遍历,先访问左子树,再访问右子树,最后访问根节点。 3. 二叉树的查找方法有哪些? 二叉树的查找方法包括递归查找和非递归查找。递归查找是从根节点开始查找,如果当前节点的值等于要查找的值,则返回当前节点。如果要查找的值比当前节点小,则继续在左子树中查找;如果要查找的值比当前节点大,则继续在右子树中查找。非递归查找可以使用栈或队列实现,从根节点开始,每次将当前节点的左右子节点入栈/队列,直到找到要查找的值或者栈/队列为空。 4. 二叉树的插入与删除操作如何实现? 二叉树的插入操作是将要插入的节点与当前节点的值进行比较,如果小于当前节点的值,则继续在左子树中插入;如果大于当前节点的值,则继续在右子树中插入。当找到一个空节点时,就将要插入的节点作为该空节点的子节点。删除操作需要分为三种情况:删除叶子节点、删除只有一个子节点的节点和删除有两个子节点的节点。删除叶子节点很简单,只需要将其父节点的对应子节点置为空即可。删除只有一个子节点的节点,需要将其子节点替换为该节点的位置。删除有两个子节点的节点,则可以找到该节点的后继节点(即右子树中最小的节点),将其替换为该节点,然后删除后继节点。 5. 什么是平衡二叉树平衡二叉树是一种特殊的二叉树,它保证左右子树的高度差不超过1。这种平衡可以确保二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。常见的平衡二叉树包括红黑树和AVL树。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MeteorChenBo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值