二叉平衡搜索树AVL 学习解析 及代码实现研究

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉平衡搜索树AVL 学习解析 及代码实现研究相关的知识,希望对你有一定的参考价值。

前言

我们为了提高数据检索效率,从而引进树型结构;而常见的就是二叉树,一颗基本的二叉树他们都需要满足一个基本性质--即树中的任何节点的值大于它的左子节点,且小于它的右子节点。按照这个基本性质使得树的检索效率大大提高。如果一旦失去平衡则会出现检索效率大大降低的效果,为了维持二叉树的平衡,大牛们提出了各种实现的算法,如:AVLSBT伸展树TREAP ,红黑树等等;这篇文章将以java实现二叉平衡搜索树(avl)

原理及实现

二叉平衡搜索树(Self-Balancing Binary Search Tree)又被称为AVL树,是一种二叉排序树,其中每一个节点的左子树和右子树高度差最多等于1

 

左边的是AVL树,它任意节点的子节点的高度差最多相差1 ,右边的非平衡二叉树也叫最小不平衡树,距离插入节点最近,且平衡因子的绝对值大于1的结点为根的子树,我们称之为最小不平衡树。

这其中有个很重要的概念,也是实现平衡二叉树的重要元素、

平衡因子

二叉树上节点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor)

旋转

要实现平衡二叉树 就需要 向左旋转  向右旋转   添加数据过后进行左右平衡

左旋

 

 

 

右旋

 

 

先添加一组数据进行分析

主要实现的流程图

 

当数据添加到1时,平衡因子最大已经大于1了,开始 右旋

 

现在进行一次左旋就可以将树平衡起来,继续添加数据

 

继续添加数据

 

 

这就是整个进行平衡二叉树的的转换流程

  • 我们为保持平衡二叉树的过程,添加前六个数时,因为不复杂,平衡因子为2  ,即左边比右边深,则找到添加元素的父节点 ,进行右旋即可;平衡因子为-2  则,右边比左边深,则找到添加元素的父节点 ,进行左旋即可
  • 当我们添加七个数时,为平衡因子出现 -2  ,但是我们不能左旋,因为一旦左旋,则 会出现右边比父节点大,也就是说插入节点  为 左子节点,但平衡因子为-2,我们只能采用先右旋 父节点然后在父节点  进行左旋即可
  • 插入86最后一个数 ,也就是插入 节点的 右边  , 我们也只能 先右旋 父节点, 然后在父节点的父节点 进行左旋就可以了。具体实现可以看一下代码

java实现

创建树节点

public class Node<E extends Comparable<E>> {
        E elemet;
        int balance = 0;//平衡因子
        Node<E> left;
        Node<E> right;
        Node<E> parent;

        public Node(E elem, Node<E> pare) {
            this.elemet = elem;
            this.parent = pare;
        }

        public E getElemet() {
            return elemet;
        }

        public void setElemet(E elemet) {
            this.elemet = elemet;
        }

        public int getBalance() {
            return balance;
        }

        public void setBalance(int balance) {
            this.balance = balance;
        }

        public Node<E> getLeft() {
            return left;
        }

        public void setLeft(Node<E> left) {
            this.left = left;
        }

        public Node<E> getRight() {
            return right;
        }

        public void setRight(Node<E> right) {
            this.right = right;
        }

        public Node<E> getParent() {
            return parent;
        }

        public void setParent(Node<E> parent) {
            this.parent = parent;
        }


    }
  • 创建Node 树节点类 
  • 在树节点中 添加 泛型 数据  平衡因子  balance 默认为 0   在进行计算平衡时,需要改变
  • 并添加 树节点所需的  左子树节点   右子树节点   以及父节点

左旋操作

 public void left_rotate(Node<E> x){
        if(x!=null){
            Node<E> y=x.right;//先取到Y
            //1.把h作为X的右孩子 x.right=h
            x.right=y.left;
            //2.h不为空 ,则将h的父亲设置为 x
            if(y.left!=null){
                y.left.parent=x;
            }
            //3。把Y移到原来X的位置上  y的父亲修改为 x的父亲
            y.parent=x.parent;
            // 4.x父亲为空
            if(x.parent==null){
               //设置根节点为y节点
                root=y;
            }else{
                //x父亲不为空,则需要判断 x是父亲的左孩子还是右孩子  将父的左孩子或者右孩子设置为y
                if(x.parent.left==x){
                    x.parent.left=y;
                }else if(x.parent.right==x){
                    x.parent.right=y;
                }
            }
            //3.X作为Y的左孩子
            y.left=x;
            x.parent=y;
        }
    }

整个左旋操作还是很简单的,基本就是通过链表前后断链并重新链接实现的  需要结合代码看一下,在代码上备注了有的

  • 先取到Y 把h作为X的右孩子 x.right=h
  • h不为空 ,则将h的父亲设置为 x
  • 把Y移到原来X的位置上  y的父亲修改为 x的父亲
  • x父亲为空 设置根节点为y节点 
  • x父亲不为空,则需要判断 x是父亲的左孩子还是右孩子  将父的左孩子或者右孩子设置为y
  • X作为Y的左孩子

右旋操作

 public void right_rotate(Node<E> y) {
        if (y != null) {
            Node<E> yl = y.left;

            //step1
            y.left = yl.right;
            if (yl.right != null) {
                yl.right.parent = y;
            }

            // step2
            yl.parent = y.parent;
            if (y.parent == null) {
                root = yl;
            } else {
                if (y.parent.left == y) {
                    y.parent.left = yl;
                } else if (y.parent.right == y) {
                    y.parent.right = yl;
                }
            }
            // step3
            yl.right = y;
            y.parent = yl;
        }
    }

右旋操作 和左旋操作很像

  • 首先拿到 y的左节点 x  在拿到 x的右节点  i 用于右旋时,进行移动  
  •   i不为空,则将 i的父节点 设置为 y 
  • 拿到 y的 父节点  判断是否为根节点 ,不为根节点  则 判断是否y为父节点的左节点 还是有节点   并将父节点的左节点 或右节点 设置为 x 将 x的父节点设置为y的父节点
  • 最后将 x的右节点设置为y,  y的父节点设置为x  就可以了

新增一个节点 

    public boolean insertElement(E element) {
        Node<E> t = root;
        if (t == null) {
            root = new Node<E>(element, null);
            size = 1;
            root.balance = 0;
            return true;
        } else {
            //开始找到要插入的位置
            int cmp = 0;
            Node<E> parent;
            Comparable<? super E> e = (Comparable<? super E>) element;
            do {
                parent = t;
                cmp = e.compareTo(t.elemet);
                if (cmp < 0) {
                    t = t.left;
                } else if (cmp > 0) {
                    t = t.right;
                } else {
                    return false;
                }
            } while (t != null);
            //开始插入数据
            Node<E> child = new Node<E>(element, parent);
            if (cmp < 0) {
                parent.left = child;
            } else {
                parent.right = child;
            }
            //节点已经放到了树上
            //检查平衡,回溯查找
            while (parent != null) {
                cmp = e.compareTo(parent.elemet);
                if (cmp < 0) {
                    parent.balance++;
                } else {
                    parent.balance--;
                }
                if (parent.balance == 0) {//如果插入后还是平衡树,不用调整
                    break;
                }
                if (Math.abs(parent.balance) == 2) {
                    //出现了平衡的问题,需要修正
                    fixAfterInsertion(parent);
                    break;
                } else {
                    parent = parent.parent;
                }
            }
        }
        size++;
        return true;
    }
  • 插入数据 ,判断 根节点是否为空,是空则设置为根节点, 平衡因子为0, size 设置为1
  •  然后开始找插入的位置,用do while进行查找 到需要插入的位置  记录着根节点  进行判断是插入左边还是 右边
 do {
                parent = t;
                cmp = e.compareTo(t.elemet);
                if (cmp < 0) {
                    t = t.left;
                } else if (cmp > 0) {
                    t = t.right;
                } else {
                    return false;
                }
            } while (t != null);
  • 然后插入数据  
 if (cmp < 0) {
                parent.left = child;
            } else {
                parent.right = child;
            }
  • 接下来就是检查平衡并修正数据 ,这里涉及到左平衡和右平衡

    private void fixAfterInsertion(Node<E> parent) {
        if (parent.balance == 2) {
            leftBalance(parent);
        }
        if (parent.balance == -2){
            rightBalance(parent);
        }
    }

设置平衡因子

设定平衡因子(左子树高度-右子树高度):

LH=1(表示:左子树高,插入节点为左子节点)

RH=-1(表示:右子树高,插入节点为右子节点)

EH=0(表示:左右子树高度相同)

右平衡操作 

为什么需要右平衡操作,插入元素破坏平衡,平衡因子在设置为负数,既右子树过深;既然右子树过深,肯定要左旋

从上面的a[10]={3,2,1,4,5,6,75,100,91,86}一组数据 来推理的规律 分成两种情况(当插入新节点,平衡因子要改变的只有父节点往上的节点,叶子节点不变的)

  • 当节点插入t的 右子节点(tr)的右子树时,直接将t元素,也就是插入元素往上遍历,失去平衡的点,进行左旋即可;tr到顶端 ;然后将 t节点及右子节点(tr)平衡因子设置eh即可。

当节点插入t的右子节点(tr)的左子树时,需要分为三种情况

  • 右子节点(tr)的左子树时根节点(trl)平衡因子为lh,则 左子树深, 右子节点(tr)右旋,然后t节点进行左旋, trl到顶端 ;t的平衡因子和trl平衡因子设置为 eh,tr平衡因子设置为rh
  • 右子节点(tr)的左子树时根节点(trl)平衡因子为rh, 则右子树深,右子节点(tr)右旋,然后t节点进行左旋,trl到顶端 ; tr的平衡因子和trl平衡因子设置为 eh,t平衡因子设置为lh
  • 右子节点(tr)的左子树时根节点(trl)平衡因子为eh,因为在不断往上走的情况下,可能会出现这种情况;右子节点(tr)右旋,然后t节点进行左旋,trl到顶端 ;所有的平衡因子设置为eh

 

 public void rightBalance(Node<E> t) {
        Node<E> tr = t.right;
        switch (tr.balance) {
            case RH://新的结点插入到t的右孩子的右子树中
                left_rotate(t);
                t.balance = EH;
                tr.balance = EH;
                break;
            case LH://新的结点插入到t的右孩子的左子树中
                Node<E> trl = tr.left;
                switch (trl.balance) {
                    case RH:
                        t.balance = LH;
                        tr.balance = EH;
                        trl.balance = EH;
                        break;
                    case LH:
                        t.balance = EH;
                        tr.balance = RH;
                        trl.balance = EH;
                        break;
                    case EH:
                        tr.balance = EH;
                        trl.balance = EH;
                        t.balance = EH;
                        break;

                }
                right_rotate(t.right);
                left_rotate(t);
                break;

        }
    }
  • 如果新的结点插入到t的右孩子的右子树中,则直接进行左旋操作即可

   

如果新的结点插入到t的右孩子的左子树中,则需要进行分情况讨论

  • 情况a:当t的右孩子的左子树根节点的balance = LEFT_HIGH

  • 情况b:当t的右孩子的左子树根节点的balance = RIGHT_HIGH

 

 

  • 情况C:当t的右孩子的左子树根节点的balance = EQUAL_HIGH

 

左平衡操作

为什么需要左平衡操作,插入元素破坏平衡,平衡因子在设置为正数数,t结点的不平衡是因为左子树过深;既然左子树过深,肯定要右旋保证平衡

上面有右平衡已经分析了左右平衡旋转,左平衡是很像的,因此我简单简述一下过程

  • 当节点插入t的 左子节点(tl)的左子树时,直接将t元素,也就是插入元素往上遍历,失去平衡的点,进行右旋即可;tl到顶端 ;然后将 t节点及右子节点(tl)平衡因子设置eh即可。

当节点插入t的左子节点(tl)的右子树时,需要分为三种情况

  • 左子节点(tl)的右子树时根节点(tlr)平衡因子为lh,则 左子树深, 右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ;tl的平衡因子和tlr 平衡因子设置为 eh,t平衡因子设置为rh
  • 左子节点(tr)的右子树时根节点(tlr)平衡因子为rh, 则右子树深,右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ; t的平衡因子和tlr平衡因子设置为 eh,tl平衡因子设置为lh
  • 左子节点(tr)的右子树时根节点(tlr)平衡因子为eh,因为在不断往上走的情况下,可能会出现这种情况;右子节点(tl)左旋,然后t节点进行右旋, tlr到顶端 ;所有的平衡因子设置为eh
 public void leftBalance(Node<E> t) {
        Node<E> tl = t.left;
        switch (tl.balance) {
            case LH:
                right_rotate(t);
                tl.balance = EH;
                t.balance = EH;
                break;
            case RH:
                Node<E> tlr = tl.right;
                switch (tlr.balance) {
                    case LH:
                        t.balance = RH;
                        tl.balance = EH;
                        tlr.balance = EH;
                        break;
                    case RH:
                        t.balance = EH;
                        tl.balance = LH;
                        tlr.balance = EH;
                        break;
                    case EH:
                        t.balance = EH;
                        tl.balance = EH;
                        tlr.balance = EH;
                        break;

                    default:
                        break;
                }
                left_rotate(t.left);
                right_rotate(t);
                break;


        }
    }

左平衡操作,即结点t的不平衡是因为左子树过深

  • 如果新的结点插入到t的左孩子的左子树中,则直接进行右旋操作即可

 

如果新的结点插入到t的左孩子的右子树中,则需要进行分情况讨论

  •     情况a:当t的左孩子的右子树根节点的balance = RIGHT_HIGH

  • 情况b:当t的左孩子的右子树根节点的balance = LEFT_HIGH

  • 情况c:当t的左孩子的右子树根节点的balance = EQUAL_HIGH

 

删除节点操作

我想大家都能经过上面的理解,结合二叉排序树,然后在来理解删除节点还是比较简单把;

  • 在删除某个节点时,需要保证数据排序性,因此需要将树进行先按照二叉排序树的删除节点 ;还需将平衡因子重新设置
  • 在走一遍上面的左平衡  或者是右平衡

二叉排序树学习构建方式及如何遍历二叉排序树

 

总结

整个二叉平衡搜索树,最关键 的点在于如何保证每一个节点的左子树和右子树高度差最多等于1;而要保证这一特性,则需要我们做左右平衡,如何判断左右平衡则需要平衡因子的判断,所以 如何左平衡 右平衡 ,显得格外重要,在左右平衡中分为几种属性。

 

以上是关于二叉平衡搜索树AVL 学习解析 及代码实现研究的主要内容,如果未能解决你的问题,请参考以下文章

C++AVL树的实现--详细解析旋转细节

C++AVL树的实现--详细解析旋转细节

AVLTree(二叉平衡树)底层实现

AVLTree(二叉平衡树)底层实现

AVLTree(二叉平衡树)底层实现

C++泛型编程实现平衡二叉搜索树AVL