数据结构与算法-二叉查找树平衡(DSW)

上一节探讨了二叉查找树的基本操做,二叉查找树的查找效率在理想状态下是O(lgn),使用该树进行查找老是比链表快得多。可是,该论点并不老是正确,由于查找效率和二叉树的形状息息相关。就像这样:算法


图1-1给出了3颗二叉查找树,它们存储着相同的数据,但很明显,图1-1(A)的树是最好的。在最坏的状况下,图A定位一个对象须要3次测试,图C须要6次。缘由在于,图C的数据不是平均分布的,该树实际上已经退化成一个链表,已经失去了二叉查找树的优越性。数组

这里须要引入一个新的概念,叫作平衡。若是树中任一节点的两个子树的高度差为0或者1,该二叉树就是高度平衡的或者简称平衡的。例如,图B中的节点20,其子树的高度差是1,这是能够接受的。 可是对于节点10,其子树的高度差是3,这意味着整棵树是不平衡的。另外,若是树是平衡的,而且该树全部叶节点都出如今一个或者两个层次上,那么该树是彻底平衡的。数据结构

那么问题来了,如何获得一颗平衡的二叉查找树?post

许多技术均可以适当的平衡二叉树。一些技术对数据从新排序从而建立一颗平衡的二叉树,另外一些技术在因为插入或者删除元素而致使树不平衡时,会从新平衡树。咱们首先来探讨如何建立一颗平衡二叉查找树,而后介绍如何从新平衡已有的二叉查找树。测试

想要建立一颗平衡二叉查找树,首先要观察这种树的特性,根据观察到的规律总结出来的数学逻辑,其实就是算法。就像下面:spa


发现了吗?若是将一颗完美的平衡的二叉查找树压平,将数据线性列出,你会发现它是有序的,而且根节点30处于数组A中间的位置。不止如此,根节点30的左子树的根节点20处于数组B的中间位置,根节点30的右子树的根节点47处于数组C的中间位置。以此类推,全部子树的根节点老是处于某个数组的中间位置。这很相似二分查找的逻辑,而且你是否发觉,数组A实际上是该二叉树中序遍历的结果。3d

这里有个定论:二叉查找树的中序遍历能够获得有序的数据流指针

证实也很容易,这里使用天然语言简单描述下:code

假如要对一颗二叉查找树进行中序遍历,首先将其分解成根节点,左子树,右子树。由于中序遍历的逻辑是首先遍历左子树,而后是根节点,最后是右子树。咱们能够将它们放到一个栈中,大概长得是这个样子:cdn


根据二叉查找树的定义,左子树全部节点小于根节点,右子树全部节点大于根节点。所以,栈此时的状态是有序的。最后,咱们将栈中的子树所有分解成新的根节点、左子树、右子树,而且按照上图所示的顺序放入栈中,直到栈中再也不存在子树,所有分解成了根节点。最后是这个样子的:


栈中保存的数据流就是中序遍历的结果。因为从分解开始是有序的,而且随后的每一次分解都是有序的,最终造成的数据流也是有序的。

这就证实了上面的定论: 二叉查找树的中序遍历能够获得有序的数据流

好吧,说了这么多,实际上是在总结咱们观察平衡二叉查找树找到的规律:

将平衡二叉查找树进行中序遍历,能够获得一个有序的数据流,而且根节点处于数据流的中间位置。以此类推,右子树的根节点处于右子树数据流的中间位置,左子树的根节点处于左子树数据流的中间位置。

根据以上规律,咱们能够获得一个建立平衡二叉查找树的算法,首先用天然语言进行描述:

假设咱们有一个有序的数组,数组中元素个数为n。咱们能够将数组中间元素指定为根,这个数组如今包含两个子数组:一个包含从数组的开始到刚刚选为根的元素之间的全部元素,另外一个包含刚刚选为根的元素到数组的末尾之间的全部元素。根的左子节点指定为第一个子数组的中间元素,根的右子节点指定为第二个子数组的中间元素。以此类推,数组中的每一个元素均可以放到二叉树中,最终造成的二叉树就是一颗平衡二叉查找树。

代码以下:

void balance(int data[], int first, int last) { 
        if (first <= last) {
                int middle = (first + last)/2;         
                insert(data[middle]);
                balance(data, first, middle - 1);
                balance(data, middle + 1, last);   
        }
}复制代码

代码中使用了递归,递归可使程序逻辑变得简单,可是会加大运行时栈的负担,慎用。咱们这里只是探讨算法,所以,使用递归实现是能够的。

该算法存在严重的缺陷:在建立树以前,全部的数据都必须放在数组中。当必须使用树,可是准备保存到树中的数据仍然在输入的时候,该算法就不太合适了。咱们可使用折中的方法,若是数据在持续输入,咱们能够按照建立二叉查找树的方法,将数据保存到二叉树中。数据输入完毕以后,只须要对该树进行中序遍历,就能够获得有序的数据流,而后使用上述的算法,就能够获得一颗平衡的二叉查找树。

上诉讨论的算法效率有点低,由于在建立彻底平衡的树以前,须要使用一个额外的有序数组。为了不排序,这一算法须要破坏树并用中序遍历把元素放在数组中,而后重建该树,这样作效率并不高,除非树很小。然而,存在几乎不须要存储中间变量也不须要排序过程的算法。这就是DSW算法。该算法能够对已经存在的二叉查找树进行平衡,而且不须要中间变量。

老规矩,在坐享其成的获取DSW算法以前,咱们先本身分析一下如何将一颗二叉查找树进行平衡。先思考一个问题:


如何将上图中的二叉查找树从新构建成一颗平衡的二叉查找树,为了方便找到规律,这里给出平衡以后的二叉查找树:


是否是发现很类似?若是将平衡以后的二叉查找树从右上角到左下角压平,能够获得和原始二叉查找树类似的结构。那么,经过哪些操做能够将原始二叉树转变成平衡二叉树呢?咱们首先观察左上角的4个节点:五、十、20、15。能够屏蔽掉其余节点,把它们当作不存在。若是你了解二叉树节点的左旋操做,立刻就能明白,只要将20节点围绕其父节点10左旋转,立刻就能够获得平衡以后的二叉树。什么是左旋?左旋有什么做用?咱们首先来探讨左旋的做用,而后探讨左旋的原理。左旋能够提高根节点左子树的高度,下降根节点右子树的高度,而且左旋以后依然是二叉查找树。好比,五、十、20、15。10做为根节点,左子树高度为1,右子树高度为2。通过左旋以后,20成为新的根节点,左子树高度提高为2,右子树高度下降为0。能够发现,左旋以后的二叉树依然是二叉查找树。那么左旋究竟是什么呢?左旋其实与二叉树节点的合并删除算法很是类似,而且原理是一致的,在数据结构与算法-二叉查找树这篇文章中,详细讲解了合并删除的原理。这里简单描述下左旋的操做以及原理。依然以五、十、20、15节点为例,左旋是针对右子树的根节点来讲的,对称的右旋是针对左子树的根节点来讲的。在这里,20节点做为10节点的右子节点,能够围绕10节点进行左旋操做。首先,将根节点10以及左子树做为A组,将20节点所在的右子树做为B组,左旋就是将A组合并到B组上。将A组设置为20节点的左子树,将20节点原有的左子树设置为A组的右子树。就像这样:


将A组合并到B组,原则上来说,只要20节点到10节点的路径中不存在右指针便可,由于,一旦出现右指针,就意味着,B组中存在节点小于A组节点,可是咱们都知道,A组是二叉查找树的根节点以及左子树组成,全部节点都小于B组节点(右子树)。在左旋操做中,将A组设置为20节点的左子树,只有一个左指针,所以,该操做是合法的。20节点原有的左子树须要合并到A组上,原则上来说,只要10节点到15节点的路径中不存在左指针便可,原理和上诉相似。由于10的右子树为空,因此这里就将15节点直接设置为10节点的右子树。到此为止,左旋操做完毕,由于左旋操做本质是二叉查找树中合法的子树合并操做,因此最后的二叉树也是合法的二叉查找树,可是左旋提升了左子树的高度,下降了右子树的高度,左旋和右旋是对称的,有兴趣的能够自行了解。将目光放到平衡二叉查找树上,比较平衡以前的二叉树以及平衡以后的二叉树。能够发现,只要进行两步操做就能实现转变。第一步,分别对20节点、30节点、49节点进行左旋操做。第二步,继续对30节点进行左旋操做。搞定收工,下面总结通用算法。

若是忽略最后一层的叶子节点,剩余的二叉树是一个完美二叉树的线性排列。那么,该完美二叉树的元素个数是多少呢?咱们假设原二叉查找树元素个数为n,完美二叉树的高度为h,那么能够获得不等式2^h - 1 <= n,即h <= lg(n+1),只要不等式向下取整,就能够获取到完美二叉树的高度。那么,完美二叉树元素个数m为2^h - 1。一眼就能看出m是个奇数,而且在第一次左旋时,是从上到下第二个节点开始的,作多少次左旋呢?实际上是m/2次。带入到上述二叉查找树中,完美二叉树高度h为lg(12 + 1),向下取整为3,完美二叉树元素个数m为2^h - 1,即7,第一次左旋次数为m/2,即3。上述平衡二叉查找树过程当中,一共有两步,第一步作了3次左旋,第二步作了1次左旋。能够发现,根据不一样高度(h)的完美二叉树,须要作不一样的步数(p),它们的关系是p = h - 1。固然,终止条件也能够是另外一种。能够发现,假设完美二叉树元素个数为m,那么第一步左旋次数为m/2,记为m1,第二步左旋次数为m1/2,记为m2,以此类推,若是m(n)小于1,证实已经平衡完毕。

到此为止,咱们已经总结出了平衡二叉查找树的关键逻辑。问题从平衡二叉查找树转变成了如何获取相似下图的二叉树?


能够发现,二叉查找树总的元素个数n为12,从右上到左下,最外层的节点数m是7。发现了吗?其实7就是该二叉查找树包含的完美二叉树元素个数。经过lg(n + 1)向下取整能够获得完美二叉树的高度h,经过2^h - 1能够获得完美二叉树元素个数m。将12带入公式,能够获得h为3,m为7。内层有5个节点五、1五、2三、2八、40,若是分别对它们进行右旋操做,能够获得如下图形:


是否是很熟悉?这不就是二叉查找树最差的链表形式嘛!真是造化弄人,咱们在平衡二叉查找树的过程当中,居然还须要借助二叉树的链表形式。从链表形式的第二个节点开始,每次隔一个节点进行n - m次左旋,即5次左旋,就能获得咱们想要的二叉树形式。如今问题又转变成了,如何从一个普通的二叉查找树获取到最差的链表形式?答案是从根节点开始,沿着右子树,不停的右旋(提升右子树高度,下降左子树高度),直至全部左指针为空。就像下面动图:


到目前为止,咱们已经探讨出从普通二叉查找树到平衡二叉查找树的过程。其实这就是DSW算法。总结以下:

  • 建立主链

建立主链就是从普通二叉查找树转变到只有右指针的链式结构,伪代码以下:

createMainChain(root) {
        tmp = root;     
        while(tmp != 0) {
                if tmp有左子节点
                        围绕tmp右旋左子节点;             //这样左子节点将成为tmp的父节点
                        tmp设置为刚刚成为父节点的子节点;
                else  将tmp设置为它的右子节点; 
        }
}复制代码
能够发现,建立主链的过程就是不停的右旋,直至二叉树中不存在左指针。
  • 主链转换成平衡树

主链转换成平衡树其实有两步,第一步就是从主链转变成相似下面的图形:


二叉树一共有n(12)个节点,外层节点数n(7)是该二叉树包含的完美二叉树个数,计算方式在上面已经探讨过,就是lg(n + 1)向下取整获取完美二叉树高度h(3),而后由2^h - 1获取到完美二叉树个数m(7),那么内层叶子节点个数就是n - m,即5个。咱们只要从主链的第二个节点开始,每隔一个节点进行一次左旋,一共进行n - m次(5次)便可。

第二步就是将以上图形转变成平衡二叉查找树,算法逻辑已经探讨过了。其实就是将外层节点从第二个开始,每隔一个节点进行一次左旋,一共进行m/2次,记为m1。这时,最外层节点个数成为m1,继续从第二个开始,每隔一个节点进行一次左旋,一共进行m1/2次,记为m2。以此类推,终止条件是m(n)小于1,或者是循环h - 1次。

算法中的逻辑,也就是为何这么作?所有已经在上面探讨清楚,能够反复阅读加深理解。

第二步的逻辑总结一下就是不停的左旋(提升左子树高度,下降右子树高度),直至二叉查找树平衡。

伪代码以下:

createBalanceTree() {     
        n = 节点数;     
        m = 2^h - 1;    
        从主链的顶部第二个节点开始,每隔一个节点进行左旋,一共进行n - m次; 
        while(m > 1) {
                m = m/2;
                从主链的顶部第二个节点开始,每隔一个节点进行左旋,一共进行m次;       
        }
}复制代码

DSW算法已经探讨完毕。DSW算法是对已有的二叉查找树进行全局平衡的算法,二叉树中的每一个节点都有可能改变位置。它的算法复杂度是O(n),这一时间随着n线性增加,并且只须要很小且固定的存储空间。总之,DSW是一个很是优秀的对二叉树进行全局平衡的算法。

到目前为止,咱们探讨了从数组中如何建立一颗平衡的二叉查找树以及如何对已有的普通二叉查找树进行全局平衡。可是,还有一个问题没有解决,二叉查找树之因此不平衡,一般是因为插入或者删除操做形成的。这种不平衡一般是局部不平衡,这种时候不须要使用DSW算法对全局进行平衡。从新平衡只须要在局部进行便可,这就是大名鼎鼎的AVL树,这是咱们下节须要探讨的内容。

数据结构与算法-二叉查找树平衡(AVL)

相关文章
相关标签/搜索