糖尿病康复,内容丰富有趣,生活中的好帮手!
糖尿病康复 > Java 7之集合类型 - 二叉排序树 平衡树 红黑树---转

Java 7之集合类型 - 二叉排序树 平衡树 红黑树---转

时间:2019-08-18 10:11:31

相关推荐

Java 7之集合类型 - 二叉排序树 平衡树 红黑树---转

/mazhimazh/article/details/19961017

为了理解 TreeMap 的底层实现,必须先介绍排序二叉树和平衡二叉树,然后继续介绍红黑树。平衡二叉树和红黑树又是一种特殊的二叉排序树。二叉排序树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索。

1、排序二叉树

排序二叉树特性如下:

若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值它的左、右子树也分别为排序二叉树

1.1 排序二叉树之插入操作

已知一个关键字值为key的结点s,若将其插入到二叉排序树中,只要保证插入后仍符合二叉排序树的定义即可。插入可以用下面的方法进行:

(1)若二叉排序树是空树,则key成为二叉排序树的根;

(2)若二叉排序树非空,则将key与二叉排序树的根进行比较。如果key的值等于根结点的值,则停止插入;如果key的值小于根结点的值,则将key插入左子树,如果key的值大于根结点的值,则将key插入右子树。

(3)重复步骤2,直到找到合适的插入位置。

1.2 排序二叉树之删除操作

当程序从排序二叉树中删除一个节点之后,为了让它依然保持为排序二叉树,程序必须对该排序二叉树进行维护。维护可分为如下几种情况:

(1)被删除的节点是叶子节点,则只需将它从其父节点中删除即可。

(2)如果待删除节点左子树存在右子树不存在,或者左子树不存在右子树存在。直接将其子树中存在的一边候补上来即可。

图2显示了被删除节点只有左子树的示意图:

图3显示了被删除节点只有右子树的示意图:

(3)若被删除节点p的左、右子树均非空,有两种做法: 将pL设为p的父节点q的左或右子节点(取决于p是其父节点q的左、右子节点),将pR设为p节点的中序前趋节点s的右子节点(s是pL最右下的节点,也就是pL子树中最大的节点)。以p节点的中序前趋或后继替代p所指节点,然后再从原排序二叉树中删去中序前趋或后继节点即可。(也就是用大于p的最小节点或小于p的最大节点代替p节点即可)。

图4显示了被删除节点左右子节点不为空的情形,采用到是第一种方式维护:

图5显示了被删除节点左右子节点不为空的情形,采用到是第二种方式维护:

TreeMap删除节点采用图5所示右边的情形进行维护——也就是用被删除节点的右子树中最小节点与被删节点交换的方式进行维护。

在使用第二种方式进行维护时,如果使用前驱节点代替被删除的节点,则前驱节点可能还存在左子树(因为前驱节点是根节点左子树中最右边的节点),而如果是后继节点的话,这个后继节点可能还存在右子树。他们的处理方法相同,直接将子树移上去即可。

一定要理解,无论是前驱还是后继节点,不可能同时具有左子树或右子树,这就为删除替代节点后的操作带来了方便。

1.3 排序二叉树之查找操作

从二叉排序树中进行查找时,根据树的性质,节点的左子树必定小于根节点,右子树必定大于根结点。如果查找的节点值小于根节点,则进入左子树,大于进入右子树,重复这个比较步骤直到找到这个节点或者这个节点不存在。

1.4 总结

排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很差。

2、平衡二叉树(AVL)

ALV树中任何两个结点的高度差值最大不超过1。树在查找、插入和删除时平均和最坏的情况下都是O(logn)。在一颗二叉搜索树查找一个值的平均时间复杂度为log(n),但是若查找树的所有的节点向一边倾斜,这时候的查找就退化为线性查找,复杂度为n。为了获得更高的查找效率,就有了AVL树的概念,对于一颗非平衡的AVL树,可以通过旋转变换为AVL树。

一棵空树是平衡二叉树;若 T 是一棵非空二叉树,其左、右子树为 TL 和 TR ,令 hl 和 hr 分别为左、右子树的深度。当且仅当:

①TL 、 TR 都是平衡二叉树;

② | hl - hr |≤ 1;

时,则 T 是平衡二叉树。 由于平衡二叉树也是排序二叉树,所以使用二叉树的插入、删除和查找操作即可。只是在操作完成后,为了下次能够保持一个好的检索效率,也为了防止这个链表退化为普通链表,则需要对树进行旋转。旋转可以再次达到平衡。主要包括:左旋转、右旋转、左右旋转、右左旋转。 2.1 LL 插入一个新节点到根节点的左子树的左子树,导致根节点的平衡因子由1变为2.需要 右 旋转来解决。

2.2 LR 插入一个新节点到根节点的左子树的右子树,导致根节点的平衡因子由1变为2.需要先左旋后右旋转来解决。

2.3 RR 插入一个新节点到根节点的右子树的 右子树,导致根节点的平衡因子由1变为2.需要左旋转来解决。 2.4 RL 插入一个新节点到根节点的 右子树的左子树,导致根节点的平衡因子由1变为2.需要先 右旋后 左 旋转来解决。

3、Java中的红黑树

红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型地,JDK提供的集合类TreeMap本身就是一个红黑树的实现。排序二叉树的深度直接影响了检索的性能,当插入节点本身就是由小到大排列时,排序二叉树将变成一个链表,这种排序二叉树的检索性能最低:N个节点的二叉树深度就是N-1。但是红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。

红黑树在原有的排序二叉树增加了如下几个要求:

性质1:每个节点要么是红色,要么是黑色。性质2:根节点永远是黑色的。性质3:所有的叶节点都是空节点(即null),并且是黑色的。性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

图6.Java红黑树的示意

备注:本文中所有关于红黑树中的示意图采用白色代表红色。黑色节点还是采用了黑色表示。

下面来看一下几个约定:

(1)性质3中指定红黑树的每个叶子节点都是空节点,而且叶子节点都是黑色 Java实现的红黑树将使用null来代表,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。非叶子节点,也就是非null节点称为红黑树中的儿子节点。

(2)红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度”。

对于给定的黑色高度为N的红黑树,从根到叶子节点的最短路径长度为N-1,最长路径长度为2*(N-1)。

假如有一棵黑色高度为3的红黑树:从根节点到叶节点的最短路径长度是2,该路径上全是黑色节点(黑节点-黑节点-黑节点)。最长路径也只可能为4,在每个黑色节点之间插入一个红色节点(黑节点-红节点-黑节点-红节点-黑节点),性质4保证绝不可能插入更多的红色节点。由此可见,红黑树中最长路径就是一条红黑交替的路径。

3.1 读取操作

由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。

为了在每次插入删除节点时,满足红黑树的如上一些性质,需要进行节点颜色的变换和进行旋转(向平衡二叉树一样,只是比平衡二叉树更加严格要求),下面就来看一下Java中实现的红黑树。

3.2 插入节点后修复

每次插入节点后必须进行简单修复,使该排序二叉树满足红黑树的要求。插入操作按如下步骤进行:

(1)以排序二叉树的方法插入新节点,并将它设为红色。

(2)进行颜色调换和树旋转。

在插入操作中,红黑树的性质1和性质3两个永远不会发生改变,因此无需考虑红黑树的这两个特性。

而颜色调用和树旋转就比较复杂了,下面将分情况进行介绍。在介绍中,我们把新插入的节点定义为N节点,N节点的父节点定义为P节点,P节点的兄弟节点定义为U节点,P节点父节点定义为G节点(参见图7)

下面分成不同情形来分析插入操作。

情形1:新节点N是树的根节点,没有父节点

在这种情形下,直接将它设置为黑色以满足性质2。

情形2:新节点的父节点P是黑色

在这种情况下,新插入的节点是红色的,因此依然满足性质4。而且因为新节点N有两个黑色叶子节点;但是由于新节点N是红色,通过它的每个子节点的路径依然保持相同的黑色节点数,因此依然满足性质5。

情形3:如果父节点P和父节点的兄弟节点U都是红色

在这种情况下,程序应该将P节点、U节点都设置为黑色,并将P节点的父节点设为红色(用来保持性质5)。现在新节点N有了一个黑色的父节点P。由于从P节点、U节点到根节点的任何路径都必须通过G节点,在这些路径上的黑节点数目没有改变(原来有叶子和G节点两个黑色节点,现在有叶子和P两个黑色节点)。

经过上面处理后,红色的G节点的父节点也有可能是红色的,这就违反了性质4,因此还需要对G节点递归地进行整个过程(把G当成是新插入的节点进行处理即可)。

图7显示了这种处理过程:

虽然图7 绘制的是新节点N作为父节点P左子节点的情形,其实新节点N作为父节点P右子节点的情况与图7完全相同。

情形4:父节点P是红色、而其兄弟节点U是黑色或缺少;且新节点N是父节点P的右子节点,而父节点P又是其父节点G的左子节点。

如上的情况下,需要对新节点和其父节点进行左旋转操作,接着按情形5处理以前的父节点P(也就是把P当成新插入的节点即可)。这导致某些路径通过它们以前不通过的新节点N或父节点P的其中之一,但是这两个节点都是红色的,因此不会影响性质5。

图8.插入节点后的树旋转

备注:图8中P节点是G节点的左子节点,如果P节点是其父节点G节点的右子节点,那么上面的处理情况应该左、右对调一下。

情形5:父节点P是红色、而其兄弟节点U是黑色或缺少;且新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。

在这种情形下,需要对节点G的一次右旋转,在旋转产生的树中,以前的父节点P现在是新节点N和节点G的父节点。由于以前的节点G是黑色,否则父节点P就不可能是红色,我们切换以前的父节点P和节点G的颜色,使之满足性质4,性质5也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过节点G,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。

图9.插入节点后的颜色调整、树旋转

备注:图9中P节点是G节点的左子节点,如果P节点是其父节点G节点的右子节点,那么上面的处理情况应该左、右对调一下。

3.3 删除节点后修复

如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题,也就是典型的二叉排序树的删除操作。 对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除 的节点中。我们接着删除替换节点。因为只是复制了一个值而不违反任何属性,这就把问题简化为如何删除最多有一个儿子的节点的问题。

删除只有一个儿子的节点(如果它两个儿子都为空,即均为叶子,我们任意将其中一个看作它的儿子)是删除节点的关键:

删除一个红色节点,它的父亲和儿子一定是黑色的。所以可以简单的用它的黑色儿子替换它,并不会破坏性质3和4。通过被删除节点的所有路径只是少了一个红色节点,这样可以继续保证性质5。被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会 破坏性质4,但是如果我们重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持性质4要删除的节点和它的儿子二者都是黑色的时候,需要进行具体的讨论。

把删除的节点替换为树的后继节点(当然也可以是前驱节点)这个后继节点假设为 N,它的兄弟节点为S。P节点为N节点的父亲,SL为S的左子树,SR为S的右子树。如下图。

情形1:删除节点是树的根节点

从所有路径去除了一个黑色节点,而新根是黑色的,所以属性都保持着。举个例子如下:

注意: 在情况2、5和6下,我们假定 N 是它父亲的左儿子。如果它是右儿子,则在这些情况下的左和右应当对调。

情形2. 删除节点的兄弟节点S是红色

在这种情况下在N的父节点P上做左旋转操作,接着对调 N 的父节点和S节点的颜色。尽管所有的路径仍然有相同数目的黑色节点,现在 N 有了一个黑色的兄弟和一个红色的父亲,所以我们可以接下去按 4、5或6情况来处理。(它的新兄弟是黑色因为它是红色S的一个儿子)

情形3:N 的父亲、S 和 S 的儿子都是黑色的

在这种情况下,我们简单的重绘 S 为红色。结果是通过S的所有路径, 它们就是以前不通过 N 的那些路径,都少了一个黑色节点。因为删除 N 的初始的父亲使通过 N 的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过 P 的所有路径现在比不通过 P 的路径少了一个黑色节点,所以仍然违反属性4。要修正这个问题,我们要从情况 1 开始,在 P 上做重新平衡处理。

情形4.S 和 S 的儿子都是黑色,但是 N 的父亲是红色

在这种情况下,我们简单的交换 N 的兄弟和父亲的颜色。这不影响不通过 N 的路径的黑色节点的数目,但是它在通过 N 的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点。

情形5. S 是黑色,S 的左儿子是红色,S 的右儿子是黑色,而 N 是它父亲的左儿子

在这种情况下我们在 S 上做右旋转,这样 S 的左儿子成为 S 的父亲和 N 的新兄弟。我们接着交换 S 和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在 N 有了一个右儿子是红色的黑色兄弟,所以我们进入了情况 6。N 和它的父亲都不受这个变换的影响。

情形6. S 是黑色,S 的右儿子是红色,而 N 是它父亲的左儿子

在这种情况下我们在 N 的父亲上做左旋转,这样 S 成为 N 的父亲和 S 的右儿子的父亲。我们接着交换 N 的父亲和 S 的颜色,并使 S 的右儿子为黑色。子树在它的根上的仍是同样的颜色,所以属性 3 没有被违反。但是,N 现在增加了一个黑色祖先: 要么 N 的父亲变成黑色,要么它是黑色而 S 被增加为一个黑色祖父。所以,通过 N 的路径都增加了一个黑色节点。

此时,如果一个路径不通过 N,则有两种可能性:

它通过 N 的新兄弟。那么它以前和现在都必定通过 S 和 N 的父亲,而它们只是交换了颜色。所以路径保持了同样数目的黑色节点。

它通过 N 的新叔父,S 的右儿子。那么它以前通过 S、S 的父亲和 S 的右儿子,但是现在只通过 S,它被假定为它以前的父亲的颜色,和 S 的右儿子,它被从红色改变为黑色。合成效果是这个路径通过了同样数目的黑色节点。

在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了属性 4。在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色。

同样的,函数调用都使用了尾部递归,所以算法是就地的。此外,在旋转之后不再做递归调用,所以进行了恒定数目(最多 3 次)的旋转。

3.4 红黑树的优势

红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。

参考文献:

/daoluanxiaozi/p/3340382.html

如果觉得《Java 7之集合类型 - 二叉排序树 平衡树 红黑树---转》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。