小橙书阅读指南(九)——红黑平衡树(2)

从标准二叉树的极端情况我们推导出2-3树这样的数据结构具备自平衡的特性,但是要实现这个特性在算法上相当复杂。考虑在大部分情况下,对于检索的指数级时间消费O(lgN)要求并不严格。因此,我们会看到如何将一颗标准的2-3树转变成红黑树的过程。

一、局部变换

考虑如果在2-节点上挂新的键并不会破坏2-3树的平衡结构。可是在3-节点上挂新的键,可能的变化却多达6种。这个临时的4-节点可能是根节点,可能是一个2-节点的左子节点或者右子节点,也可能是3-节点的左子节点、中子节点或者右子节点。2-3树插入算法的根本在于这些变换都是局部的:除了相关的节点和链接之外不必修改该或者检查树的其他部分。因此,每次变换中,变更的链接数量都不会超过一个很小的常熟。

每一个变换都会将4-节点中的一个键送入它的父节点中,并重构相应的链接,但不必涉及树的其他部分。这也是2-3树的重要特性:一次聚聚变换不会影响树的有序和平衡性:

二、红黑树与2-3树的相互转换

尽管2-3树一种高效的结构并提供了自平衡特性,但是它的算法过于复杂以至于在一般的情况下我们并不愿意使用这样的结构作为首选方案。但是红黑树则不然,它的实现算法非常简单,代码量也不大。但理解整个红黑树的构造过程却需要一番仔细的探究。

红黑二叉查找树背后的基本思想是用标准的二叉查找树和一些额外信息来表示2-3树。我们将树中的链接分为两种类型:红连接将2个2-节点连接起来构成一个3-节点,黑链接则是2-3树中的普通链接。

对于红黑树我们给出的定义如下:

1.红连接均为左连接;

2.没有任何一个节点同时和两条红连接相连;

3.该树是完美黑色平衡的,即任意空节点到根节点的路径上的黑色链接数量相等。

我们只需要将一颗红黑树中的红连接画平,那么所有的空节点到根节点的距离都是相同的。如果我们将由红连接项链的节点合并,得到的就是一颗2-3树。

方便起见,因为每一个节点都只会由一条指向自己的链接,我们将链接的颜色泊岸村在表示节点的Node对象中,布尔变量color为true表示红色,false为黑色。节点定义代码如下:

class Node {
        private Key key;
        private Value val;
        private Node left;
        private Node right;
        private boolean color;
        private int size;

        public Node(Key key, Value val, boolean color, int size) {
            this.key = key;
            this.val = val;
            this.color = color;
            this.size = size;
        }
    }

三、旋转和颜色变化

对红黑树来说旋转是它的基础变化,在实现代码前我们先思考一下为什么需要旋转。

向2-3树的2-节点插入新的键是非常简单的,无论这个键本身是E还是S,在插入新的键以后都一个样子。但对于红黑树来说就不是这样了,回想一下之前对红黑树的定义:红连接均为左连接。如果要插入的新值小于节点我们可以直接插入,如果新值大于节点,我们就会创建一条红色的右链接。因此我们就需要通过旋转将右链接变成左连接。当然更复杂的情况下,我们还会通过旋转把左连接变成右链接。也许到这里有人会问:当插入的新键大于节点的时候,为什么不可以直接插入一条黑色的右链接呢?在思考一下2-3树的生长特性以及红黑树对平衡的定义:

1.2-3树是区别于二叉树,它是向上生长的。

2.红黑树中任一空节点到根节点的距离相同。

综合起来就是,我们在红黑树中插入的新键都必须从红链接开始,再通过变换来调整树的高度以恢复平衡。

接下来我们再考虑一个问题,如果我们向红黑树插入了一条向左的红链接(显然此时并未破坏树的平衡性,因此我们不必调整树的结构)然后再向同一个节点插入了一条红色的右链接怎么办?左链接已经被占据了,我们无法再通过旋转调整平衡。

回想一下2-3树在遇到这样的情况是是如何变化的:我们先构造一个临时的4-节点,再将它转换成3个2-节点并把父节点向上转移。那么对于红黑树来说似乎就更简单了一些。我们只需要调整两条子链接为黑链接并把根链接变成红连接即可。

你或许又会问了:如果父节点已经是红色的怎么办呢?事实上你完全不用担心这样的情况发生,因为红黑树的另一条特性已经保证了:没有任何一个节点同时和两条红连接相连。换言之,如果父节点(E)为红色,当我们在插入(A)的时候已经不符合红黑树的定义,我们需要通过旋转来恢复平衡。

总的来说,无论之前节点的情况如何,我们通过0次,1次或2次旋转以及颜色的变化得到期望的结果。

原文地址:https://www.cnblogs.com/learnhow/p/9613845.html

时间: 2024-11-01 23:23:05

小橙书阅读指南(九)——红黑平衡树(2)的相关文章

小橙书阅读指南(五)——归并排序的两种实现

算法描述:将两个较小的有序数组合并成为一个较大的有序数组是比较容易的事情.我们只需要按照相同的顺序依次比较最左侧的元素,然后交替的放进新数组即可.这就是自顶向下的归并排序的实现思路.与之前的算法不同的是,归并排序需要使用额外的存储空间,用空间换时间的做法也是在排序算法中经常需要做的选择. 算法图示: 算法解释:把一个较大的数组不断划分为较小的两个数组,直到无法再切分之后再做逆向合并,并再合并的过程中调整顺序.归并算法的难点是如何尽可能的减少额外存储空间的使用. Java代码示例: package

小橙书阅读指南(十)——二叉查找树

算法描述:二叉查找树时一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表(SymbolTable)实现.具体来说,就是使用每个节点含有两个链接的二叉树来高效地实现符号表.一颗二叉查找树时一颗二叉树,其中每个节点都含有一个Comparable的键且每个节点的键都大于其左子树中的任意节点的键而小于右子树的任意节点的键. 一.查找 一般来说,在符号表中查找一个键只可能出现命中和未命中两种情况.一般通过递归算法在二叉树中查找,如果树时空的则查找未命中:如果被查找的键和根节点相等,查找命中,

小橙书阅读指南(二)——选择排序

算法描述:一种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置.再次,再剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置.如此往复,知道将整个数组排序.这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小者. 算法图示: Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.io.IOException; import java.uti

小橙书阅读指南(三)——插入排序

算法描述:通常人们在整理扑克的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置.在算法的实现中,为了给要插入的元素腾出1个空间,我们需要将其余所有元素在插入之前都向右移动1位.这种算法叫插入算法. 算法图示: 算法解释:在基础版本中通常的做法是,当新元素需要被插入有序数组的时候,从右向左依次交换.直到新元素到达它合适的位置. Java代码示例: import common.ArraysGenerator; import common.Sortable; import java.i

小橙书阅读指南(六)——快速排序和三向切分快速排序

算法描述:快速排序是一种分治的排序算法.它将数组分为两个子数组,并将两部分独立的排列.快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将子数组归并以将整个数组排序:而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了. 算法图示: 算法解释:选择标的元素(5)并且便利数组,将素有小于5的元素都安排在它的左侧,而大于5的元素都安排在它的右侧.之后再通过递归的方法分别处理左边的子数组和右边的子数组. 快速排序的算法难点在于尽量不要使用额外的存储空间(即保证原地

小橙书阅读指南(七)——优先队列和索引优先队列

算法描述:许多应用程序都需要按照顺序处理任务,但是不一定要求他们全部有序,或是不一定要一次就将他们排序.很多情况下我们只需要处理当前最紧急或拥有最高优先级的任务就可以了.面对这样的需求,优先队列算法是一个不错的选择. 算法图示: 算法解释:上图所展示的是最大优先队列(大顶堆)的算法逻辑,在这个标准的二叉树中,任意节点的元素都大于其叶子节点的元素.利用数组表示该二叉树即Array[2]和Array[3]是Array[1]的叶子节点,Array[4]和Array[5]是Array[2]的叶子节点,A

小橙书阅读指南(十一)——散列表

算法描述:散列表是一种在时间和空间上做出权衡的查找算法.使用查找算法分为两步.第一步是通过散列函数将被查找的键转化未数组的一个索引.理想情况下,不同的键都能转为不同的索引值.当然,这只是理想情况,所以我们需要面对两个或多个键都被散列到相同索引值的情况.因此,散列查找的第二部就是处理碰撞冲突的过程. 一个比较令人满意的散列函数能够均匀并独立地将所有键散布于0到M-1之间. 一.基于拉链法的散列表 算法图示: 拉链散列表算法的本质是将哈希值相同的键保存在一个普通链表中,当我们需要调整数组长度的时候,

小橙书阅读指南(十二)——无向图、深度优先搜索和路径查找算法

在计算机应用中,我们把一系列相连接的节点组成的数据结构,叫做图.今天我们将要介绍它的一种形式--无向图,以及针对这种结构的深度优先搜索和路径查找算法. 一.无向图数据结构 接口: /** * 图论接口 */ public interface Graph { /** * 顶点数 * * @return */ int vertexNum(); /** * 边数 * * @return */ int edgeNum(); /** * 向图中添加一条v-w的边 * * @param v * @param

Swift语言指南(九)--基本运算符

运算符(operator)是用来检查,改变或合并值的一种特殊符号或短语.例如,加号运算符让两个数字相加(如:let i = 1 + 2),还有些更复杂的运算符,如逻辑与运算符(&&)(如:if enteredDoorCode && passedRetinaScan)和自增运算符(++i)(将 i 的值加 1 的便捷写法). Swift 支持标准C语言的大多数运算符,并且改进了一些特性以规避常见的代码错误.赋值运算符(=)是没有返回值的,这样是为了避免在使用等于运算符(==)