红黑树Java实现
1、序言
我们使用符号表这个词来描述一张抽象的表格,我们会将信息(值)存储在其中,然后按照指定的键来搜索并获取这些信息。键和值的具体意义取决于不同的应用。
符号表中可能会保存很多键和很多信息,因此实现一张高效的符号表也是一项很有挑战性的任务。
实现一张符号表,我们首先要定义其背后的数据结构,并指明创建并操作这种数据结构以实现增删查改等操作的算法(任何数据结构,他的应用无非是基于对其的增删查改四步)
红黑树就是一种实现符号表功能的高级数据结构,其能高效的插入和查找。在理解红黑树之前,我们理应先按顺序理解顺序查找,二分查找法,二叉查找树,2-3查找树。理解之后我们就能很轻易的理解红黑树
2、 顺序查找与二分查找
首先我们要明确
1、顺序查找基于无序数组(链表)
2、二分查找基于有序数组(链表)
我们先假定我们的符号表有 N 个键值对
顺序查找
顾名思义,我们会按照顺序从数组的开头不停往下寻找,直到找到我们需要的讯息。在查找命中的情况(即找的到情况),我们最坏也会经过 N 次比较,而对于所有未命中的查找,我们显然也要经过 N 此比较。 除此之外,如果我们要向一个空表中插入N个不同的键,我们需要~N^2/2 次比较(在表中每插入一个键,我们都会遍历已存在的整张表,确保没有重复的键)
到这我们已经可以看到,随着数据量的增大,我们查找的成本也明显上升
由此我们先找到了二分查找的办法
二分查找
二分查找就是将查找的键和子数组的中间键作比较,如果被查找的键小于中间键,就在左子数组继续查找;如果大于中间键,就在右子数组中查找,否则中间键就是要找的元素。
那么二分查找之所以基于有序数组的原因我们也可以轻易理解,我们每一次缩小查找范围都将抛弃其一半的元素,若二分不基于有序,我们相当于直接放弃验证其中是否有我们需要查找的元素
/**
* 在这里我们直接用C实现,相信大家都能理解,二分查找,找到该值在数组中的下标,否则为-1
*/
static int binarySerach(int[] array, int key) {
int left = 0;
int right = array.length - 1;
// 这里必须是 <=
while (left <= right) {
int mid = (left + right) / 2;
if (array[mid] == key) {
return mid;
}
else if (array[mid] < key) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1;
}
同样的,在 N 个键的有序数组中,我们任何一次查找,最多只需要(lgN+1)次比较,无论是否成功
可以看到,我们查找的时间相较于顺序查找下降了很多
对于一个数据量较大的静态表来说,我们在初始化的时候就对他进行排序是完全值得的
当然,二分查找也不完全符合我们的需求,仔细想想,我一开始说过,一张符号表除了要实现查找操作,还要实现插入操作(删、改其实与增,查无明显区别)
那么核心的问题转化为,我们能不能实现查找和插入都在对数级别的算法和数据结构,答案是令人兴奋的可以
3、 二分查找树
首先,我们需要定义一些概念
我们所使用的数据结构由 结点 组成,结点包含的链接可以为空(null)或者指向其他结点。在二叉树中,每个结点只能有一个父结点,只有一个例外(就是根节点,他没有爹爹),而且每个结点都有 左右 两个链接,分别指向自己的 左子结点 和 右子结点。尽管左右链接指向的是左右结点,但我们应该认识到,他其实指向了另一颗二叉树,即二叉子树
因此我们可以将二叉树定义为一个空链接,或是一个有左右两个链接的结点,每个链接都指向一颗(独立的)子二叉树,在二叉查找树中,每个结点还包含了一个键和一个值,键之间也有顺序之分以支持高校查找
接下来我给你们看一点图片,你们应该很快就能弄懂二分查找树上的数据排布
如果我们将一颗二叉查找树的所有键投影到一条直线上,保证一个结点的左子树中的键出现在它的右边,右子树中的键出现在它的右边,那么我们一定可以得到一条有序的键列。
查找原理
如果树是空的,则查找未命中。如果被查找的键和根结点的键相等,查找命中。否则我们就在适当的子树中继续查找。如果被查找的键较小就选择左子树,较大就选择右子树。
在二叉查找树中,随着我们不断向下查找,当前结点所表示的子树的大小也在减小(理想情况下是减半)
插入
查找代码几乎和二分查找的一样简单,这种简洁性是二叉查找树的重要特性之一。而二叉查找树的另一个更重要的特性就是插入的实现难度和查找差不多。
当查找一个不存在于树中的结点并结束于一条空链接时,我们需要做的就是将链接指向一个含有被查找的键的新结点。如果被查找的键小于根结点的键,我们会继续在左子树中插入该键,否则在右子树中插入该键。
分析
使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于键被插入的先后顺序。
在最好的情况下,一颗含有N个结点的树是完全平衡的,每条空链接和根结点的距离都为~lgN。在最坏的情况下,搜索路径上可能有N个结点。但在一般情况下树的形状和最好情况更接近。
【在由N个随机键构造的二叉查找树中,查找命中平均所需的比较次数为~2lnN。N越大这个公式越准确】
【在由N个随机键构造的二叉查找树中,插入和操作未命中平均所需的比较次数为~2lnN。N越大这个公式越准确】
接下来给出来的二分查找树的实现代码,get实现查找,put实现插入
package xxxx;
import java.util.Queue;
public class BST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {
private Node root;
private class Node {
private Key key;
private Value val;
private Node left, right;
private int N;
public Node(Key key, Value val, int n) {
this.key = key;
this.val = val;
N = n;
}
}
@Override
public void put(Key key, Value val) {
root = put(root, key, val);
}
private Node put(Node x, Key key, Value val) {
if (x == null) {
return new Node(key, val, 1);
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = put(x.left, key, val);
} else if (cmp > 0) {
x.right = put(x.right, key, val);
} else {
x.val = val;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
@Override
public Value get(Key key) {
return get(root, key);
}
private Value get(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return get(x.left, key);
} else if (cmp > 0) {
return get(x.right, key);
} else {
return x.val;
}
}
@Override
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) {
return 0;
} else {
return x.N;
}
}
@Override
public Key min() {
return min(root).key;
}
private Node min(Node x) {
if (x.left == null) {
return x;
} else {
return min(x.left);
}
}
@Override
public Key max() {
return max(root).key;
}
private Node max(Node x) {
if (x.right == null) {
return x;
} else {
return max(x.right);
}
}
@Override
public Key floor(Key key) {
Node x = floor(root, key);
if (x == null) {
return null;
}
return x.key;
}
private Node floor(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp == 0) {
return x;
} else if (cmp < 0) {
return floor(x.left, key);
}
Node t = floor(x.right, key);
if (t != null) {
return t;
} else {
return x;
}
}
@Override
public Key ceiling(Key key) {
return null;
}
@Override
public int rank(Key key) {
return rank(key, root);
}
private int rank(Key key, Node x) {
if (x == null) {
return 0;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return rank(key, x.left);
} else if (cmp > 0) {
return 1 + size(x.left) + rank(key, x.right);
} else {
return size(x.left);
}
}
@Override
public Key select(int k) {
return select(root, k).key;
}
private Node select(Node x, int k) {
if (x == null) {
return null;
}
int t = size(x.left);
if (t > k) {
return select(x.left, k);
} else if (t < k) {
return select(x.right, k - t - 1);
} else {
return x;
}
}
@Override
public Iterable<Key> key(Key lo, Key hi) {
return null;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
if (x == null) {
return;
}
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
if (cmplo < 0) {
keys(x.left, queue, lo, hi);
}
if (cmplo <= 0 && cmphi >= 0) {
queue.add(x.key);
}
if (cmphi > 0) {
keys(x.right, queue, lo, hi);
}
}
@Override
public void deleteMin() {
root = deleteMin(root);
}
private Node deleteMin(Node x) {
if (x.left == null) {
return x.right;
}
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
public void delete(Key key) {
}
private Node delete(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = delete(x.left, key);
} else if (cmp > 0) {
x.right = delete(x.right, key);
} else {
if (x.right == null) {
return x.left;
}
if (x.left == null) {
return x.right;
}
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
}
以下图片可以看出,虽然二叉查找树的查找成本比二分高了约39%,但插入的成本却成功降到了对数级别
4、 2-3查找树
之前的数据结构虽然能用于很多情况,但他们在最坏的情况下,成本仍然是线性级别,所以平衡查找树应运而生,平衡查找树将做到保证不论我们怎么构造他,他的运行时间都将是对数级别。
理想情况我们希望保证二分查找树的平衡性。在一棵含有 N 个点的树中,我们希望树高是**~lgN**,这样我们就能保证所有查找都能在lgN次内比较结束。不幸的是,在动态插入中保证树的完美平衡代价太高了,
所以接下来我们会放松一点完美平衡的要求,但仍要确保新的数据结构能在对数时间内完成对应操作。这就是2-3查找树
定义 .一颗2-3查找树或为一颗空树,或由以下结点构成
2-结点,含有一个键和两条链接,左链接指向的2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点
3-结点,含有两个键和三条链接,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点
一颗完美平衡的2-3查找树中的所有空链接到根结点的距离都是相同的。
查找
将二叉查找树的查找算法一般化我们就能得到2-3树的查找方法
要判断一个键是否在树中,我们先将它和根结点中的键比较。如果它和其中的任何一个相等,查找命中。否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。
接下来用图片理解
插入
要在2-3树中插入一个新结点,我们可以和二叉查找树一样先进行一次未命中的查找,然后把新结点挂在树的底部。但这样的话树无法保持完美平衡性。我们使用2-3树的主要原因就在于它能够在插入之后继续保持平衡。
1、向2-结点中插入新键
如果未命中的查找结束于一个2-结点,事情就好办了。我们只要把这个2-结点替换为一个3-结点,将要插入的键保存在其中即可。但如果未命中的查找结束于3-结点,就麻烦多了
2、向一颗只含有一个3-结点的树中插入新键
先考虑最简单的例子:只有一个3-结点的树,向其插入一个新键。
这棵树唯一的结点中已经没有可插入的空间了。我们又不能把新键插在其空结点上(破坏了完美平衡)。为了将新键插入,我们先临时将新键存入该结点中,使之成为一个4-结点。创建一个4-结点很方便,因为很容易将它转换为一颗由3个2-结点组成的2-3树(如图所示),这棵树既是一颗含有3个结点的二叉查找树,同时也是一颗完美平衡的2-3树,其中所有空链接到根结点的距离都相等。
【插入前树的高度为0,插入后树的高度为1,这个例子虽然简单,却值得学习,他说明了2-3树是如何生长的】
3、向一个父结点为2-结点的3-结点中插入新键
我们将情况进一步复杂化
假设未命中的查找结束于一个3-结点,而它的父结点是一个2-结点。在这种情况下我们需要在维持树的完美平衡的前提下为新键腾出空间。
我们先像刚才一样构造一个临时的4-结点并将其分解,但此时我们不会为中键创建一个新结点,而是将其移动至原来的父结点中。(如图所示)
这次转换也并不影响(完美平衡的)2-3树的主要性质。树仍然是有序的,因为中键被移动到父结点中去了,树仍然是完美平衡的,插入后所有的空链接到根结点的距离仍然相同。
到此,请你确认完全理解了这次转换,因为这是2-3树动态变化的核心过程
4、向一个父结点为3-结点的3-结点中插入新键
我们的情况进一步复杂
假设未命中的查找结束于一个父结点是一个3-结点的3-结点
我们再次和刚才一样构造一个临时的4-结点并分解它,然后将它的中键插入它的父结点中。但父结点也是一个3-结点,因此我们再用这个中键构造一个新的临时4-结点,然后在这个结点上进行相同的变换,即分解这个父结点并将它的中键插入到它的父结点中去。
我们就这样一直向上不断分解临时的4-结点并将中键插入更高的父结点,直至遇到一个2-结点并将它替换为一个不需要继续分解的3-结点,或者是到达3-结点的根。
5、分解根节点
我们把情况再复杂一点
如果插入结点到根节点的路径上全是3-结点,那我们怎么办呢
可以看到,根节点最终变成了一个临时4-结点,此时我们可以按照向一颗只有一个3-结点的树中插入新键处理问题,我们将临时的4-结点分解为3个2-结点,让树高+1
请注意,这次最后的变换仍然保持了树的完美平衡性,因为它变换的是根节点
6、局部变换
将一个4-结点分解为一颗2-3树可能有6种情况
【2-3插入算法的根本在于这些变换都是局部的】
即除了相关结点和链接之外不必修改或检查树的其他部分。每次变换中,变更的链接数量不会超过一个很小的常数。不光是在树的底部,树中的任何地方只要符合相应的模式,变换都可以进行。每个变换都会将4-结点中的一个键送入它的父结点中,并重构响应的链接而不涉及树的其他部分
请务必理解上面一段话,他是整个2-3树结构算法的根本
7、全局性质
局部变换不会影响树的全局有序性和平衡性
任何变换都不会影响树的完美平衡性,因为除了根结点的4-分解情况之外,树的高度都不会增加,根结点的4-分解会使树的整体高度加1。
总结:
先找插入结点,若结点有空(即2-结点),则直接插入。如结点没空(即3-结点),则插入使其临时容纳这个元素,然后分裂此结点,把中间元素移到其父结点中。对父结点亦如此处理。(中键一直往上移,直到找到空位,在此过程中没有空位就先搞个临时的,再分裂。)
★2-3树插入算法的根本在于这些变换都是局部的:除了相关的结点和链接之外不必修改或者检查树的其他部分。每次变换中,变更的链接数量不会超过一个很小的常数。所有局部变换都不会影响整棵树的有序性和平衡性。
至此,我已经将2-3树结点插入介绍完毕,并且说明了2-3树对其全局数据的维护不会受到插入数据的影响。请确定你已经全部理解
构造
相信你已经看出来了,2-3树的生长和2叉查找树不同,它是由下向上生长的
还记得在二叉查找树中,我们按升序插入7个键会得到高度为6的一颗树的最差情况吗(忘了可以翻回去看)。但在2-3树中,哪怕插入10个键,树的高度也只为2
因此我们可以确定,哪怕是最坏的情况,2-3树也有较好的性能
在一颗大小为N的2-3树中,查找和插入操作访问的结点必然不超过lgN个
设想一下,含有10亿结点的一颗2-3树,他的高度只会有19到30之间,是不是很惊人。我们最多只用访问30个结点就能在10亿个键中完成查找插入操作
【但是,这离代码实现还有一段距离,尽管我们可以用不同的数据类型表示2-结点或3-结点并写出变化的代码,但这样要处理的情况太多了,我们要维护两种不同类型的结点,将被查找的键和结点中的每个键比较,将连接和其他信息从一个结点复制到另一个结点,将结点从一种数据类型转成另一种数据类型等等。
实现起来不仅要大量代码,而且他们产生的额外开销可能会使算法比标准的二叉树更慢】
平衡一棵树的初衷是为了消除最坏情况,但我们希望这种保障所需的代码越少越好
5、 红黑树(红黑二叉查找树)
上文我们所述的2-3树不难理解,事实上实现它也不难,我们介绍一种叫红黑树的简单数据结构来表达并实现它。
我会用一句话告诉你什么是红黑树并且让你恍然大悟
红黑树就是用红链接表示3-结点的2-3树
红黑树的本质:
红黑树是对2-3查找树的改进,它能用一种统一的方式完成所有变换。
★红黑树背后的思想是用一个标准的二叉查找树(2-结点)和一些额外的信息(替换3-结点)来表示2-3树。
我们将树中的链接分为两个类型:红链接将两个2-结点连起来表示3-结点,黑链接则是2-3树中的普通链接
确切的说,我们将3-结点表示为用一条左斜的红色链接相连的两个2-结点
这种表示法的一个优点是,我们无需修改就可以直接使用标准二叉查找树的get()方法。对于任意的2-3树,只要对结点进行转换,我们都可以立即派生出一颗对应的二叉查找树。我们将用这种方式表示2-3树的二叉查找树称为红黑树。
红黑树的另一种定义是满足下列条件的二叉查找树:
⑴红链接均为左链接。
⑵没有任何一个结点同时和两条红链接相连。
⑶该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同。
如果我们将一颗红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如果我们将由红链接相连的结点合并,得到的就是一颗2-3树。
相反,如果将一颗2-3树中的3-结点画作由红色左链接相连的两个2-结点,那么不会存在能够和两条红链接相连的结点,且树必然是完美平衡的。
无论我们用何种方式去定义它们,红黑树都既是二叉查找树,也是2-3树。如果我们能保持一一对应的关系的基础上实现之前的插入算法,那我们就能同时拥有2-3树中高效的平衡插入算法和二叉树中的高效查找算法
(2-3树的深度很小,平衡性好,效率高,但是其有两种不同的结点,实际代码实现比较复杂。而红黑树用红链接表示2-3树中另类的3-结点,统一了树中的结点类型,使代码实现简单化,又不破坏其高效性。)
颜色表示:
因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中(若指向它的链接是红色的,那么该变量为true,黑色则为false)。
当我们提到一个结点颜色时,我们指的是指向该结点的链接的颜色。
private static final boolean RED=true;
private static final boolean BLACK=false;
public class Node{
Key key;//键
Value value;//相关联的值
Node left,right;//左右子树
int N;//这颗子树中的结点总数
boolean color;//由其父结点指向它的链接的颜色
public Node(Key key,Value value,int N,boolean color){
this.key = key;
this.value = value;
this.N = N;
this.color = color;
}
private boolean isRed(Node x){
if(x == null){
return false;
}
return x.color==RED;
}
}
旋转
修复红黑树,使得红黑树中不存在红色右链接或两条连续的红链接。
左旋
将红色的右链接转化为红色的左链接
Node rotateLeft(Node h){
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
右旋
将红色的左链接转化为红色的右链接,代码与左旋完全相同,只要将left换成right即可
Node rotateRight(Node h){
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = true;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
插入
在插入新的键时,我们可以使用旋转操作帮助我们保证2-3树和红黑树之间的一一对应关系,因为旋转操作可以保持红黑树的两个重要性质:有序性和完美平衡性。也就是说,我们在红黑树中进行旋转时无需为树的有序性或者完美平衡性担心。下面我们来看看应该如何使用旋转操作来保持红黑树的另外两个重要性质:不存在两条连续的红链接和不存在红色的右链接
1、向2-结点中插入新键
一棵红黑树含有一个2-结点。插入另一个键之后,我们马上就需要将他们旋转。如果新键小于老键,我们只需要新增一个红色的节点即可,新的红黑树和单个3-结点完全等价。如果新键大于老键,那么新增的红色节点将会产生一条红色的右链接。我们需要使用parent = rotateLeft(parent);来将其旋转为红色左链接并修正根结点的链接,插入才算完成。两种情况均把一个2-结点转换为一个3-结点,树的黑链接高度不变
(向红黑树中插入操作时,想想2-3树的插入操作。红黑树与2-3树在本质上是相同的,只是它们对3结点的表示不同。
向一个只含有一个2-结点的2-3树中插入新键后,2-结点变为3-结点。我们再把这个3-结点转化为红结点即可)
2.向一棵双键树(即一个3-结点)中插入新键
这种情况又可分为三种子情况:新键小于树中的两个键,在两者之间,或是大于树中的两个键。每种情况中都会产生一个同时链接到两条红链接的结点,而我们的目标就是修正这一点。
1、三者中最简单的情况是新键大于原树中的两个键,因此它被链接到3-结点的右链接。此时树是平衡的,根结点为中间大小的键,它有两条红链接分别和较小和较大的结点相连。如果我们将两条链接的颜色都由红变黑,那么我们就得到了一棵由三个结点组成,高为2的平衡树。它正好能够对应一棵2-3树,如图。其他两种情况最终也会转化为这两种情况。
2、如果新键小于原书中的两个键,它会被链接到最左边的空链接,这样就产生了两条连续的红链接,如果。此时我们只需要将上层的红链接右旋转即可得到第一种情况。
3、如果新键介于原书中的两个键之间,这又会产生两条连续的红链接,一条红色左链接接一条红色右链接。此时我们只需要将下层的红链接左旋即可看得到第二种情况。
总的来说,我们0次、1次、2次旋转以及颜色的变化得到了期望的结果。请你确认完全理解了这些转换,他们是红黑树动态变化的关键
3.颜色转换
如图3.3.21,我们专门用一个方法flipColors()来转换一个结点的两个红色字结点的颜色。除了将子结点的颜色由红变黑之外,我们同时还要将父节点的颜色由黑变红。这项操作最重要的性质在于它和旋转操作一样是局部变换,不会影响整棵树的黑色平衡性。根据这一点,我们马上就能在下面完整实现红黑树。
4.根结点总是黑色
颜色转换会使根结点变为红色,这也可能出现在很大的红黑树中。严格来说,红色的根节点说明根节点是一个3-结点的一部分。但实际情况并非如此,因此我们每次插入后会将根节点设为黑色。注意,每当根节点由红变黑时树的黑链接高度就会加1
5.向树底部的3-结点插入新键
现在假设我们需要在树的底部的一个3-结点下加入一个新结点。前面讨论过的三种情况都会出现。颜色转换会使指向中结点的链接变红,相当于将它送入了父结点。这意味着在父结点中继续插入一个新键,我们也会继续用相同的办法解决这个问题。
6.将红链接在树中向上传递
2-3树中的插入算法需要我们分解3-结点,将中间键插入父结点,如此这般直到遇到一个2-结点或是根结点。总之,只要谨慎地使用左旋,右旋,颜色转换这三种简单的操作,我们就能保证插入操作后红黑树和2-3树的一一对应关系。在沿着插入点到根结点的路径向上移动时在所经过的每个结点中顺序完成以下操作,我们就能完成插入操作:
1、如果右子结点是红色的而左子结点是黑色的,进行左旋转
2、如果左子结点是红色的且她的左子结点也是红色的,进行右旋
3、如果左右子结点均为红色,进行颜色转换。
public class RedBlackBST<Key extends Comparable<Key>,Value>{
private Node root;
private class Node
private boolean isRed(Node h);
private Node rotateLeft(Node h);
private Node rotateRight(Node h);
private void flipColors(Node h);
private int size();
public void put(Key key, Value val) {
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node h, Key key, Value val) {
if (h == null) //标准的插入操作,和父结点用红链接相连
return new Node(key, val, 1, RED);
int cmp = key.compareTo(h.key);
if (cmp < 0) {
h.left = put(h.left, key, val);
} else if (cmp > 0) {
h.right = put(h.right, key, val);
} else {
h.val = val;
}
if (isRed(h.right) && !isRed(h.left)) {
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
h.N = size(h.left) + size(h.right) + 1;
return h;
}
}
事实上红黑树还有有关删除的操作更为复杂难以实现,我以后再更
接下来会先给出一个红黑树的完整实现代码
我相信各位看官不管代码能不能自己写出来,最少肯定能够理解红黑树的概念与结构
public class RedBlackBST<Key extends Comparable<Key>, Value> implements OrderedST<Key, Value> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
private class Node {
public Node left;
public Node right;
public Value val;
public Key key;
int N;
boolean color;
public Node(Key key, Value val, int n, boolean color) {
this.val = val;
this.key = key;
N = n;
this.color = color;
}
}
private boolean isRed(Node x) {
if (x == null) {
return false;
}
return x.color = RED;
}
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
@Override
public void put(Key key, Value val) {
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node h, Key key, Value val) {
if (h == null) {
return new Node(key, val, 1, RED);
}
int cmp = key.compareTo(h.key);
if (cmp < 0) {
h.left = put(h.left, key, val);
} else if (cmp > 0) {
h.right = put(h.right, key, val);
} else {
h.val = val;
}
if (isRed(h.right) && !isRed(h.left)) {
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
h.N = size(h.left) + size(h.right) + 1;
return h;
}
@Override
public Value get(Key key) {
return null;
}
@Override
public int size() {
return 0;
}
private int size(Node x) {
return 0;
}
@Override
public Key min() {
return null;
}
@Override
public Key max() {
return null;
}
@Override
public Key floor(Key key) {
return null;
}
@Override
public Key ceiling(Key key) {
return null;
}
@Override
public int rank(Key key) {
return 0;
}
@Override
public Key select(int k) {
return null;
}
@Override
public Iterable<Key> key(Key lo, Key hi) {
return null;
}
}
红黑树的性质
【一颗大小为N的红黑树高度不会超过2lgN】
【一颗大小为N的红黑树,根结点到任意结点的平均路径长度为1.00lgN】