平衡二叉树(AVL Tree)
在学习算法的过程中,二叉平衡树是一定会碰到的,这篇博文尽可能简明易懂的介绍下二叉树的相关概念,然后着重讲下什么事平衡二叉树。
(由于作图的时候忽略了箭头的问题,正常的树一般没有箭头,虽然不影响描述的过程,但是还是需要注意,所以还请读者忽略一下部分图的箭头)
一、二叉(查找)树
二叉查找树(Binary Search Tree)是二叉树的一种,其树节点(internal nodes of the tree)储存着键值并且满足以下特性并如图A所示:
- 假设u, v, r分别为树的三个结点(nodes),r为树的根节点,u为根的左子树,v为根结点的右子树;
- 键值大小关系:key(u) < key(r) < key(v),也就是位于根结点(亦或是父节点)的左子树的所有结点的值都是小于根或者父结点的,而位于右子树的结点都大于根或者父结点;
- 树的外部结点不储存任何的信息。
图A 二叉查找树
二、二叉查找树的操作
2.1 查找(Search)
如若要查找二叉树中的某个元素k,我们会从根节点朝着树结构往下寻找对应的结点,所寻找的结点方向取决于当前结点与所要寻找的结点的值的对比。基于图A,假设我们现在所要寻找的结点是7,那么从根结点开始,我们可以知道7 < 8,那么往下朝着结点值为6的子树走,然后我们发现6 < 7所以此时我们就寻找结点为6的右子树,这时我们发现7 = 7,也就是到达了我们所要寻找的结点了,下图B是寻找结点的过程:
图B 二叉树查找过程
算法伪代码:
BSTreeSearch(k, v): if T.isExternal(v): return v elif k < key(v): return BSTreeSearch(k, T.left(v)) elif k == key(v): return v else k > key(v): return BSTreeSearch(k, T.right(v))
2.2 插入(Insertion)
如果要执行插入操作,我们首先需要对树进行查找操作,找到相应的节点的叶子(leaf)结点,然后再执行插入操作。下面以插入5位例子进行执行,如果要插入5,执行2.1节中所说的二叉树的查找操作,我们可以发现5 < 8,5 < 6,5 > 4,这时我们可以发现最终5是大于4的,那么我们需要在4的右叶子结点插入5,并且延伸新插入的结点(5)的叶子结点。具体操作如下图C所示:
图C 二叉树插入操作
2.3 删除(Deletion)
相应的执行删除操作,我们也先需要查找到相对应的结点,然后将该结点从二叉树中移除(remove),实际代码实现中需要判断要删除的结点的键值是否存在于二叉树中,除此之外,如果该结点伴有叶子结点,那么需要将该结点和叶子结点一同移除。当然,这里亦有另外一种情况,就是被移除的结点的左右子树都是内部结点(internal nodes),这个时候的操作会稍微复杂一点,这里以删除4和6为例子来讲述这两种情况,具体的操作如图D(基于图C进行操作)所示:
图D 删除操作
删除结点4的操作相对简单,只需要移除该结点和相应的叶子节点即可,但是相反的删除6的时候,我们需要确保所有父结点的左子树都是小于该结点的,并且右子树都是大于该父结点的,所以当我们删除6的时候,我们需要将5移到相应的位置并在相应的叶子结点补上新的叶子。
2.4 算法表现(Performance)
假设一个二叉树的高度为H,最坏的操作情况是O(n),而最好执行情况则为O(log(n))。
三、二叉平衡树(AVL TREE)
3.1 平衡二叉树的定义
二叉平衡树指的是要么它本身是一个空树,要么它是一个左子树和右子树的深度之差的绝对值不大于1,并且保证左右子树都是平衡树,图E是一个平衡二叉树。从图中我们可以看出,一个结点的高度位1则表明为其叶子结点到父结点的高度,整颗树的高度取决于最深叶子结点到根结点的距离。
图E 平衡二叉树
3.2 平衡二叉树的操作
AVL树的查找操作和普通的二叉树的查找基本一致,但是插入和删除操作有所不同,因为插入和删除会减少树的结点并且改变树的结构,这个时候为了使树始终保持平衡状态我们需要对树进行重构使其始终保持平衡状态,一般这个操作叫做旋转操作(rotation),旋转分为左旋转和右旋转等,下面就具体来看看插入和删除操作及如何运用旋转使二叉平衡树在插入和删除某结点之后依然保持平衡。
3.2.1 旋转操作
在这个小节中主要介绍一下左旋转和右旋转,旋转操作不局限于这两个,但是基本原理都一样,最终目的就是为了让二叉平衡树在被操作之后再次达到平衡。
图F 左旋转
在上图所说的左旋转操作中,我们假设的是x < y < z,因为树不平衡了,我们执行左旋转,将x及其左子树进行左旋转,并且将原本y的左子树变为x的右子树,这里需要注意的两点,①就是我们需要寻找到三个点,这三个点的大小是有排序的,如这此段开头所说道的xyz的关系,将中间那个值作为新的中心结点,然后再进行旋转操作,②就是一定要确保所有的左右子树遵循二叉树的定义要求,既左子树一定要永远都是小于其父结点的,而右子树始终大于父结点的。
图G 右旋转
图G所述的三个节点的关系为z < x < y,因此根据左旋转所描述的我们可以知道x应该作为中心结点也就是父结点,然后这里需要进行两次旋转才能使二叉树最终处于平衡,首先是先对z进行左旋转,将z变为x的左子树,然后再对y进行右旋转,在这个过程中,x的左子树变为z的右子树,而右子树则成为了y的左子树。有了基本的这两个操作,接下来我们就根据实际例子来看看对平衡二叉树执行插入和删除的操作并且结合旋转来达到平衡状态。
3.2.2 插入操作
平衡二叉树的插入操作与普通二叉查找树的操作一样,新插入的节点都发生在叶子结点,唯一不同的就如上述所说,新插入的结点致使树的结构发生改变而导致不平衡,此时需要进行旋转以达到平衡。在图E的基础上插入一个新结点,结点的值为40,新得到的图如图H所示:
图H 插入新的节点Key(40)
这时我们会发现此时的二叉树已经不平衡,这时我们需要寻找到树里面导致树不平衡的三个点,进行相应的操作,具体有以下两步:① 先对结点39以结点42为父结点进行左旋转,此时节点40变成了39的右结点,而33,39,40一起成为了结点42的左子树。② 对结点53进行右旋转,将其变成节点42的右子树,结点55依然为结点53的右子树。由此便完成了整棵树的重构并让新的树保持平衡。重构之后的树如下图I所示:
图I 重构之后得到的树
3.2.3 删除操作
假设在图I的基础上删除结点22,那么此时我们从节点19开始便利找到第一个导致不平衡的结点为25并且具有最大高度值的结点,之后往右子树进行便利寻找到第二个具有最大高度值的结点,此结点为42(下图标注了红色边框的结点),
图J 删除之后进行重构流程图
3.2.4 二叉平衡树的算法表现
二叉平衡树的算法表现主要体现在以下几个方面:
- 如果单独重构一次所需要的运行时间为O(1)
- 如果查找二叉平衡树中某个结点的话为O(log(n))
- 插入操作为O(log(n)),若需要重构,为了保持平衡所需要的时间为O(log(n))
- 删除操作为O(log(n)),若需要重构,为了保持平衡所需要的时间为O(log(n))
以上便是有关二叉树和二叉平衡树相关的知识点,如果有哪里讲的不对的,还请读者指出,谢谢!