数据结构与算法(九):AVL树详细讲解


数据结构与算法(一):基础简介算法

数据结构与算法(二):基于数组的实现ArrayList源码完全分析数组

数据结构与算法(三):基于链表的实现LinkedList源码完全分析数据结构

数据结构与算法(四):基于哈希表实现HashMap核心源码完全分析ide

数据结构与算法(五):LinkedHashMap核心源码完全分析性能

数据结构与算法(六):树与二叉树学习

数据结构与算法(七):赫夫曼树this

数据结构与算法(八):二叉排序树spa

本文目录

1、二叉排序树性能问题

在上一篇中咱们提到过二叉排序树构造可能出现的性能问题,好比咱们将数据:2,4,6,8构造一颗二叉排序树,构造出来以下: 设计

这确定不是咱们所但愿构造出来的,由于这样一棵树查找的时候效率是及其低下的,说白了就至关于数组同样挨个遍历比较。3d

那咱们该怎么解决这个问题呢?这时候就须要咱们学习一下二叉平衡树的概念了,本系列设计的二叉平衡树主要包含AVL树以及红黑树,本篇主要讲解AVL树。

下面咱们了解一下AVL树。

2、AVL树定义以及相关概念

AVL树定义

一棵AVL树是其每一个结点的平衡因子绝对值最多相差1的二叉查找树。

平衡因子?这是什么鸟,别急,继续向下看。

平衡因子

平衡因子就是二叉排序树中每一个结点的左子树和右子树的高度差。

这里须要注意有的博客或者书籍会将平衡因子定义为右子树与左子树的高度差,本篇咱们定义为左子树与右子树的高度差,不要搞混。

好比下图中: 
根结点45的平衡因子为-1 (左子树高度2,右子树高度3) 
50结点的平衡因子为-2 (左子树高度0,右子树高度2) 
40结点的平衡因子为0 (左子树高度1,右子树高度1)

根据定义这颗二叉排序树中有结点的平衡因子超过1,因此不是一颗AVL树。

因此AVL树能够表述以下:一棵AVL树是其每一个结点的左右子树高度差绝对值最多相差1的二叉查找树。

最小不平衡二叉排序树

最小不平衡二叉树定义为:距离插入结点最近,且平衡因子绝对值大于2的结点为根结点的子树,称为最小不平衡二叉排序树。

好比下图:

在插入80结点以前是一颗标准的AVL树,在插入80结点以后就不是了,咱们查找一下最小不平衡二叉排序树,从距离80结点最近的结点开始,67结点平衡因子为-1,50结点平衡因子为-2,到这里就找到了,因此以50为根结点的子树就是最小不平衡二叉排序树。

明白了以上概念后咱们就须要再了解一下左旋与右旋的概念了,这里左旋右旋对于刚接触的同窗来讲有点难度,可是对于理解AVL树,红黑树是必须掌握的概念,十分重要,不要怕,跟着个人思路我就不信讲不明白。

3、左旋与右旋的概念

左旋与右旋就是为了解决不平衡问题而产生的,咱们构建一颗AVL树的过程会出现结点平衡因子绝对值大于1的状况,这时就能够经过左旋或者右旋操做来达到平衡的目的。

接下来咱们了解一下左旋右旋的具体操做。

左旋操做

上图就是一个标准的X结点的左旋流程。 
在第一步图示仅仅将X结点进行左旋,成为Y结点的一个子节点。

可是此时出现一个问题,就是Y结点有了三个子节点,这连最基础的二叉树都不是了,因此须要进行第二部操做。

在第二部操做的时候,咱们将B结点设置为X结点的右孩子,这里能够仔细想一下,B结点一开始为X结点的右子树Y的左孩子,那么其确定比X结点大,比Y结点小,因此这里设置为X结点的右孩子是没有问题的。

上图中Y结点有左子树B,若是没有左子树B,那么第二部也就不须要操做了,这里很容易理解,都没有还操做什么鬼。

到这里一个标准的左旋流程就完成了。

左旋操做具体应用

在构建AVL树的过程当中咱们到底怎么使用左旋操做呢?这里咱们先举一个例子,以下图: 

在上图中咱们插入结点5的时候就出现不平衡了,3结点的平衡因子为-2,这时候咱们能够将结点3进行左旋,如右图,这样就从新达到平衡状态了。

左旋操做代码实现
 1    /**
 2     * 左旋操做
 3     * @param t
 4     */
 5    private void left_rotate(AVL<E>.Node<E> t) {
 6        if (t != null) {
 7            Node tr = t.right;
 8            //将t结点的右孩子的左结点设置为t结点的右孩子
 9            t.right = tr.left;
10            if (tr.left != null) {
11                //重置其父节点
12                tr.left.parent = t;
13            }
14            //t结点旋转下来,其右孩子至关于替换t结点的位置
15            //因此这里一样须要调整其右孩子的父节点为t结点的父节点
16            tr.parent = t.parent;
17            //整棵树只有根结点没有父节点,这里检测咱们旋转的是否为根结点
18            //若是是则须要重置root结点
19            if (t.parent == null) {
20                root = tr;
21            } else {
22                //若是t结点位于其父节点的左子树,则旋转上去的右结点则
23                //位于父节点的左子树,反之同样
24                if (t.parent.left == t) {
25                    t.parent.left = tr;
26                } else if (t.parent.right == t) {
27                    t.parent.right = tr;
28                }
29            }
30            //将t结点设置为其右子树的左结点
31            tr.left = t;
32            //重置t结点的父节点
33            t.parent = tr;
34        }
35    }

代码基本上都加上了备注,对比左旋流程仔细分析一下,这里须要注意一下,旋转完后结点的父节点都须要重置。

好了,对于左旋操做,相信你已经有必定了解了,若是还有不明白的地方能够本身仔细想一下,实在想不明白能够关注我公众号联系本人单独交流。

接下来咱们看看右旋是怎么回事。

右旋操做

上图就是对Y结点进行右旋操做的流程,有了左旋操做的基础这里应该很好理解了。

第一步一样仅仅将Y结点右旋,成为X的一个结点,一样这里会出现问题X有了三个结点。

第二步,若是一开始Y左子树存在右结点,上图中也就是B结点,则将其设置为Y的右孩子。

到这里一个标准的右旋流程就完成了。

右旋操做具体应用

咱们看一个右旋的例子,如图:

在咱们插入结点1的时候就会出现不平衡现象,结点5的平衡因子变为2,这里咱们将结点5进行右旋,变为右图就又变为一颗AVL树了。

右旋操做代码实现
 1    /**
 2     * 右旋操做
 3     * @param t
 4     */
 5    private void right_rotate(AVL<E>.Node<E> t) {
 6        if (t != null) {
 7            Node<E> tl = t.left;
 8            t.left =tl.right;
 9            if (tl.right != null) {
10                tl.right.parent = t;
11            }
12
13            tl.parent = t.parent;
14            if (t.parent == null) {
15                root = tl;
16            } else {
17                if (t.parent.left == t) {
18                    t.parent.left = tl;
19                } else if (t.parent.right == t) {
20                    t.parent.right = tl;
21                }
22            }
23            tl.right = t;
24            t.parent = tl;
25        }
26    }

对于右旋操做代码实现,没有加任何注释,但愿你本身沉下心来逐行分析一下,有了左旋代码基础,这里并不难。

好了,以上就是左旋与右旋的操做,这部分必定要搞明白,AVL树与红黑树的构建过程出现不平衡状况主要经过左旋与右旋来使其从新达到平衡状态。

4、分治思想,左平衡操做与右平衡操做

上面咱们了解了左旋与右旋的概念,也经过具体案例明白到底怎么经过左旋或者右旋来使二叉排序树从新达到AVL树的要求,可是这里要明白有些状况并非仅仅靠一次左旋或者右旋就能实现平衡的目的,这是就须要左旋右旋一块儿使用来使其达到平衡的目的。

那么到底怎么区分是使用左旋或者右旋或者左旋右旋一块儿使用才能使树从新达到平衡呢?

这里咱们就须要仔细分状况来处理了,咱们在构建AVL树插入某一个元素候若是出现不平衡现象确定是左子树或者右子树出现了不平衡现象,这里有点绕,不过也很好理解,某一结点平衡因子绝对值超过1了,确定是左子树太高或者右子树太高产生的,这里,咱们采用分治的思想来解决,分治思想是算法思想的一种,就是把一个复杂的问题分红两个或更多的相同或类似的子问题,直到最后子问题能够简单的直接求解,原问题的解即子问题的解的合并。

这里咱们怎么使用分治的思想呢?首先出现不平衡只有两种可能,某一结左子树或者右子树太高致使的,咱们能够先考虑左子树太高该怎么处理,而后考虑右子树太高怎么处理,固然这里只是粗略的分为两大解决问题的方向,往下还会继续分析不一样状况,接下来咱们将会仔细分析。

左平衡操做

左平衡操做,即结点t的不平衡是由于左子树过深形成的,这时咱们须要对t左子树分状况进行解决。

左平衡操做状况分类

一、若是新的结点插入后t的左孩子的平衡因子为1,也就是插入到t左孩子的左侧,则直接对结点t进行右旋操做便可

二、若是新的结点插入后t的左孩子的平衡因子为-1,也就是插入到t左孩子的右侧,则须要进行分状况讨论

  • 状况a:当t的左孩子的右子树根节点的平衡因子为-1,这时须要进行两步操做,先以tl进行左旋,在以t进行右旋。

通过上述过程,最终又达到了平衡状态。

  • 状况b:当p的左孩子的右子树根节点的平衡因子为1,这时须要进行两步操做,先以tl进行左旋,在以t进行右旋。

  • 状况c:当p的左孩子的右子树根节点的平衡因子为0,这时须要进行两步操做,先以tl进行左旋,在以t进行右旋。

到这里细心的同窗确定有一个疑问,状况a,b,c不都是先以tl左旋,再以t右旋吗?为何还要拆分出来?

首先观察a,b,c三种状况,旋转以前是叶子结点的,在两次旋转以后依然是叶子结点,也就是说其平衡因子旋转先后无变化,均是0。

可是再观察一下t,tl,tlr这三个节点旋转先后的平衡因子,不一样状况下先后是不同的,因此这里须要区分一下,具体旋转后t,tl,tlr的平衡因子以下:

状况a: 
t.balance = 0; 
tlr.balance = 0; 
tl.balance = 1;

状况b: 
t.balance = -1; 
tl.balance =0; 
tlr.balance = 0;

状况c: 
t.balance = 0; 
tl.balance = 0; 
tlr.balance = 0;

以上就是左平衡操做的全部状况,接下来看下左平衡具体代码:

 1    /**
 2     * 左平衡操做
 3     * @param t
 4     */
 5    private void leftBalance(AVL<E>.Node<E> t) {
 6        Node<E> tl = t.left;
 7        switch (tl.balance) {
 8            case LH: 
 9                right_rotate(t);
10                tl.balance = EH;
11                t.balance = EH;
12                break;
13            case RH:
14                Node<E> tlr = tl.right;
15                switch (tlr.balance) {
16                    case RH:
17                        t.balance = EH;
18                        tlr.balance = EH;
19                        tl.balance = LH;
20                        break;
21                    case LH:
22                        t.balance = RH;
23                        tl.balance =EH;
24                        tlr.balance = EH;
25                        break;
26                    case EH:
27                        t.balance = EH;
28                        tl.balance = EH;
29                        tlr.balance =EH;
30                        break;
31                    //统一旋转
32                    default:
33                        break;
34                }
35                //统一先以tl左旋,在以t右旋
36                left_rotate(t.left);
37                right_rotate(t);
38                break;
39            default:
40                break;
41        }
42    }

好了,左平衡操做全部状况讲解以及具体代码实现,主要就是分治思想,加以细分而后逐个状况逐个解决的套路。

右平衡操做

右平衡操做,即结点t的不平衡是由于右子树过深形成的,这时咱们须要对t右子树分状况进行解决。

右平衡操做状况分类

一、若是新的结点插入后t的右孩子的平衡因子为1,也就是插入到t左孩子的右侧,则直接对结点t进行左旋操做便可

二、若是新的结点插入后t的右孩子的平衡因子为-1,也就是插入到t右孩子的左侧,则须要进行分状况讨论

  • 状况a:当t的右孩子的左子树根节点的平衡因子为1,这时须要进行两步操做,先以tr进行右旋,在以t进行左旋。

  • 状况b:当p的右孩子的左子树根节点的平衡因子为-1,这时须要进行两步操做,先以tr进行右旋,在以t进行左旋。

  • 状况c:当p的右孩子的左子树根节点的平衡因子为0,这时须要进行两步操做,先以tr进行右旋,在以t进行左旋。

一样,a,b,c三种状况旋转先后叶子结点依然是叶子结点,变化的
只是t,tr,trl结点的平衡因子,而且三种状况trl最后平衡因子均为0.

右平衡代码实现:

1    /**
 2     * 右平衡操做
 3     * @param t
 4     */
 5    private void rightBalance(AVL<E>.Node<E> t) {
 6        Node<E> tr = t.right;
 7        switch (tr.balance) {
 8            case RH:
 9                left_rotate(t);
10                t.balance = EH;
11                tr.balance = EH;
12                break;
13            case LH:
14                Node<E> trl = tr.left;
15                switch (trl.balance) {
16                    case LH:
17                        t.balance = EH;
18                        tr.balance = RH;
19                        break;
20                    case RH:
21                        t.balance = LH;
22                        tr.balance = EH;
23                        break;
24                    case EH:
25                        t.balance = EH;
26                        tr.balance = EH;
27                        break;
28
29                }
30                trl.balance = EH;
31                right_rotate(t.right);
32                left_rotate(t);
33                break;
34            default:
35                break;
36        }
37    }

到此,左平衡与右平衡操做也就讲解完了,主要思想是采用的分治思想,大问题化为小问题,而后逐个解决,到这里,若是能所有理解,那么AVL树的最核心部分就彻底理解了,对于红黑树来讲上面也是很核心的部分。

5、AVL树的建立过程

这部分咱们主要了解下怎么建立AVL树,也就是添加元素方法的总体逻辑。

先看下每一个结点类所包含的信息:

 1public class Node<E extends Comparable<E>>{
 2        E element; // data
 3        int balance = 0; // 每一个结点的平衡因子
 4        Node<E> left;
 5        Node<E> right;
 6        Node<E> parent;
 7        public Node(E element, Node<E> parent) {
 8            this.element = element;
 9            this.parent = parent;
10        }
11
12        @Override
13        public String toString() {
14            // TODO Auto-generated method stub
15            return element + "BF: " + balance;
16        }
17
18        public E getElement() {
19            return element;
20        }
21
22        public void setElement(E element) {
23            this.element = element;
24        }
25
26        public int getBalance() {
27            return balance;
28        }
29
30        public void setBalance(int balance) {
31            this.balance = balance;
32        }
33
34        public Node<E> getLeft() {
35            return left;
36        }
37
38        public void setLeft(Node<E> left) {
39            this.left = left;
40        }
41
42        public Node<E> getRight() {
43            return right;
44        }
45
46        public void setRight(Node<E> right) {
47            this.right = right;
48        }
49
50        public Node<E> getParent() {
51            return parent;
52        }
53
54        public void setParent(Node<E> parent) {
55            this.parent = parent;
56        }
57    }

最主要的是每一个结点类添加了一个balance属性,也就是记录本身的平衡因子,在插入元素的时候须要动态的调整。

咱们看下插入元素方法的Java实现:

 1    /**
 2     * 添加元素方法
 3     * @param
 4     */
 5    public boolean addElement(E element) {
 6        Node<E> t = root;
 7        //t检查root是否为空,若是为空则表示AVL树尚未建立,
 8        //则须要建立根结点便可
 9        if (t == null) {
10            root = new Node<E>(element, null);
11            size = 1;
12            root.balance = 0;
13            return true;
14        } else {
15            int cmp = 0;
16            Node<E> parent;
17            Comparable<? super E> e = (Comparable<? super E>)element;
18            //查找父类的过程,逻辑和讲解二叉排序树时查找父类是同样的
19            do {
20                parent = t;
21                cmp = e.compareTo(t.element);
22                if (cmp < 0) {
23                    t= t.left;
24                } else if (cmp > 0) {
25                    t= t.right;
26                } else {
27                    return false;
28                }
29            } while (t != null);
30            //建立结点,并挂载到父节点上
31            Node<E> child = new Node<E>(element, parent);
32            if (cmp < 0) {
33                parent.left = child;
34            } else {
35                parent.right = child;
36            }
37            //节点已经插入,
38            // 插入元素后 检查平衡性,回溯查找
39            while (parent != null) {
40                cmp = e.compareTo(parent.element);
41                //元素在左边插入
42                if (cmp < 0) {
43                    parent.balance++;
44                } else{ //元素在右边插入
45                    parent.balance --;
46                }
47                //插入以后父节点balance正好彻底平衡,则不会出现平衡问题
48                if (parent.balance == 0) {
49                    break;
50                }
51                //查找最小不平衡二叉树
52                if (Math.abs(parent.balance) == 2) {
53                    //出现平衡问题
54                    fix(parent);
55                    break;
56                } else {
57                    parent = parent.parent;
58                }
59            }
60            size++;
61            return true;
62        }
63    }

其大致流程主要分为两大部分,前半部分和二叉排序树插入元素的逻辑同样,主要是查找父节点,将其挂载到父节点上,然后半部分就是AVL树特有的了,也就是查找最小不平衡二叉树而后对其修复,修复也就是经过左旋右旋操做使其达到平衡状态,咱们看下fix方法主要逻辑:

 1    /**
 2     * 发现最小不平衡树,对其进行修复
 3     * @param parent
 4     */
 5    private void fix(AVL<E>.Node<E> parent) {
 6        if (parent.balance == 2) {
 7            leftBalance(parent);
 8        }
 9        if (parent.balance == -2) {
10            rightBalance(parent);
11        }
12    }

很简单,就是判断左边与右边哪边不平衡,进而进行左平衡或者右平衡操做,至于左平衡右平衡上面已经详细讲解过,不在过多说明。

好了,以上就是构建一颗AVL树的过程讲解,若是有不懂得地方能够静下心来本身好好分析一下。

6、AVL树总结

本篇主要讲解了AVL的概念以及经过最基础的左旋,右旋使其保持树中每个结点的平衡因子值保证在「-1,0,1」中,这样构建出来的树具备很好的查找特性。

AVL树相对于红黑树来讲是一颗严格的平衡二叉树,平衡条件很是严格(树高差只有1),只要插入或删除不知足上面的条件就要经过旋转来保持平衡。因为旋转是很是耗费时间的。AVL树适合用于插入删除次数比较少,但查找多的状况。

在平衡二叉树中应用比较多的是红黑树,红黑树对高度差要求没有AVL那么严格,用以保持平衡的左旋右旋操做次数比较少,用于搜索时,插入删除次数多的状况下一般用红黑树来取代AVL树。TreeMap的实现以及JDK1.8之后的HashMap中都有红黑树的具体应用。

下一篇我可能先写图的概念以及图一些经典算法,放心红黑树我确定会写的,关于AVL树与红黑树的差别在写完红黑树在作详细比较,以上简单提一下。

好了,本篇就到此为止了,但愿对你有用。

相关文章
相关标签/搜索