代码已在leetcode验证了。是正确的,内存稍微多一些,时间上要好一些。大家可以看看下提交记录中的内存和时间差异(对比AVL和跳表)。
这事干起来像还愿。。
之前代码手撕的两个数据结构
在红黑树的学习过程中,我接触到两种红黑树的描述,一种是左偏红黑树,另一种是算法导论中一般意义上的红黑树。前者是红黑树的简化版本。另外左偏红黑树不能在三次旋转以内解决被破坏的黑平衡,这种红黑树,拿来理解是比较好的。
本文只是原理和实现,没有说应用。
红黑树的左偏和右偏版本
左偏红黑树被设计为和一种叫2-3树的等价树,左偏红黑树是2-3树的另一种结构表现形式。
这很好,因为2-3树是绝对平衡的二叉搜索树!
2-3树的内部节点只有两种,一种只存一个节点,两个孩子,叫二节点
另一种则有两个节点,三个孩子,叫三节点。
当插入一个元素的时候,会按照二叉搜索树性质把元素合并到叶子节点。当叶子节点只有一个元素时,合并。当有两个元素时,中间的元素会向上传递,与父节点合并,父亲节点有一个或两个节点,分情况重复在叶子节点的合并过程,知道合并到达跟节点,或者生成新的根节点。
那么左偏红黑树是如何与2-3树等价的呢?
很简单,对2-3树的三节点做些等价表变换,将左边节点变成右边节点的左孩子。
你如果反着来,那就叫做右偏树。
不难验证,这样变换后的树是复合红黑树定义的。
以红黑树的黑平衡为例,2-3树的绝对平衡可推出黑色节点的平衡。其他性质稍加思考不难验证其正确性,这里不再赘述。
同理,红黑树的插入和删除的等价于2-3树的插入删除等价操作。具体不展开。不是本文的重点,本文的重点是实现一般意义上的红黑树。
算法导论中的红黑树
有人说,导论中的红黑树也等价于一棵什么什么树,我觉得不是很重要,因为算法导论讲的已经相当好了。在这里也只是复述一些关键节点,从而能写出正确的代码。本文中的代码在leetcode 设计跳表中验证通过,可靠性比较高。
我们不去关心红黑树怎么来的,这是一种设计。因为这么设计,可以达到黑平衡,可以有logn的时间复杂度,插入删除也比较高效。红黑树定义请大家移步算法导论。
特别的,红黑树的叶子节点是黑色空节点,具体在定义的时候,所有叶子节点统一指向同一个哨兵节点
根节点的父节点也指向哨兵节点,所以程序初始化时候,0号点为哨兵的话,那0号先染黑
红黑树和二叉搜索树一样,具有查找,增加,删除元素的接口。我们允许保存重复元素。哦,原来增删改查已经很难了,不明白为什么有人觉得CRUD无脑。
关于查找,递归循环都可以,与二叉树实现一致。难的是增加和删除。
如何分情况讨论,把事情讲清楚,是红黑树算法的核心。
在开始之前,我们先来说两种操作,旋转和颜色反转。
红黑树的左旋和右旋,与其他平衡树的旋转是一样的,它们不改变其二叉搜索树的性质,只不过会打破红黑树的性质。后边说。
颜色反转是什么,考虑一个节点是黑色的,两个孩子节点是红色的,我们把孩子染成黑色,父亲是红色,这就是颜色反转,有什么作用呢,可以看下算法导论。我后边也可能会有蛛丝马迹。
进入正题。我建议下面对着算法导论的图看,或者,直接看算法导论也挺好的。
插入元素
首先按照二叉搜索树的插入在某个空子树插入一个节点,我们约定,新插入的节点都是红色的。
好了,问问题的本能被激发了。我新来的善良的节点扪心自问,我会不会破坏红黑树性质?
原来那棵树,有可能是空的
我把自己染成黑色,自己变成根节点,没问题
就算原来那树不是空的,我一定有父节点,那我的父节点是黑的的怎么办?
黑的就黑的,也不用调整了。
那父节点是红的呢?
这就麻烦了,需要分情况调整了,要么把红的移到叔叔那个家族里去,前提是它们能接纳的话。不然只能往祖宗传了,让爷爷的叔叔解决。
我们只讨论父亲节点是左子树的情况,另一种情况是对称的
我们分三种情况讨论:
-
叔叔节点是红色的
把爷爷和它的两个孩子颜色反转,把锅甩给爷爷,意思就是爸爸叔叔都没事了,爷爷你和你的爸爸叔叔商量吧,好了,下个迭代看爷爷的了。 -
叔叔节点是黑色的,我是右孩子
以父节点为轴左旋一次。转换为情况3。 -
叔叔节点是黑色的,我是左孩子
以爷爷节点为轴,向右旋转一次,并且把爸爸染成爷爷的颜色,爷爷变红了,爷爷变成了爸爸的儿子,红色成功被转嫁给叔叔,哈哈哈,新的叔叔,原来的爷爷,爷爷年轻了50岁,我有种莫名的自责,为什么要用这样的比喻,罪过罪过。
父节点是右孩子的情况与上述情况对称。
删除元素
首先按照二叉树删除方式删除一个元素,记录下来填补删除空缺的元素的索引(你不是用数组模拟的就叫指针)。
二叉树的删除还记得吗,
就是首先找到那个元素
如果有一个子树是空的,就让非空儿子替代它,孩子都是空的就直接删除就行了
如果都不是空的,就找右子树最左节点,也就是后继节点,删除后拿过来替代被删除的元素。
好了,问题来了,不妨设被删除的元素是颜色是 c1,替代它的是c2。
c1 是红色的,删了就删了,红的我们希望越少越好。
要是c1是黑的,那平衡大概率会被打破。除非c1是根节点。
我们不妨把这节点删除了,把黑色留下来被替代它的节点继承,这样的树依然黑平衡。来研究一下这个具备二重颜色的特殊节点吧。
这个节点是根节点就好了,去掉一层不改变红黑树的特性。
如果不是根节点。那就麻烦了,我们分情况讨论。我们设这个节点为x
x是个红黑的,直接染成黑色的完事。
x是个双重黑的,那我们看能不能让兄弟挤一挤拿出1重黑上交父亲,我也能如释重负,让这两种黑锅给爸爸背吧,爸爸再和叔叔商量,最终总能解决掉的不是吗,根祖宗什么事都能给办了。要是可以,一百重黑在根祖宗哪里也轻松拿掉99层没任何压力,也不用和谁商量。
同样只考虑x为左孩子的情况,右孩子的情况是对称的。
- 兄弟节点是红色的
以父节点为轴,左旋一次,往这边派了一个红色当双重黑的爸爸,下次循环就黑色就被扔给这个红色的新爸爸,其实还是原来的爸爸,只不过爸爸的某个孙子变成了儿子了,这是左旋造成的。基本下次循环就处理结束。 -
兄弟节点是黑色的,兄弟的两个孩子也是黑的。
兄弟上交一层黑色,这边也上交一层黑色,爸爸背负多一层黑色,就可以进入下次循环 -
兄弟节点是黑色的,左孩子是红色的,右孩子是黑色的
以兄弟为轴右旋一次,右孩子变它爸(我兄弟)的颜色,兄弟变成右儿子,原来右孩子是黑色,所以把我原来的兄弟(现在的右儿子)染成红色就可以,这就转化为情况4 - 兄弟节点是黑色的,右孩子是红色的。
以父节点为轴,左旋一次,兄弟的红色右孩子继承兄弟的黑色,兄弟变成了爸爸节点,夜色也继承了,爸爸节点被转到左边变成了红色,下次循环就承接了黑色(算法导论图直接染成黑色了,应该是多做了一步,我代码对于B的染色放到了下次循环做,是等价的)。
代码
class Skiplist {
public:
static const int N = 50010;
static const bool BLACK_NODE = true;
static const bool RED_NODE = false;
//下面变量初始化是必须的,不然在leetcode会有各种各样的错误
struct Node {
int parent = 0, left = 0, right = 0;
int key = -1;//, val;
bool color = false;
void init(int k, int v) {
parent = 0, left = 0, right = 0; // 指向哨兵节点
key = k; //, val = v;
color = RED_NODE;
}
} tr[N];
Skiplist() {
tr[0].color = BLACK_NODE;
}
bool search(int target) {
return find(target) != 0;
}
void add(int num) {
add(num, num);
}
bool erase(int num) {
return remove(num);
}
int root = 0, idx = 0;
// 启动垃圾回收机制
int nodes[N], tt = -1;
// idx = 0 的节点是哥哨兵,根节点父节点和叶子节点都指向哨兵
// 注意基本操作当中维护颜色信息,以及父辈信息,因为在讨论的过程中需要区分叔节点的颜色
// 维护全局的根节点的信息
/* 原地调整
x y
\ -> /
y x
*/
void leftRotate(int x) {
int y = tr[x].right;
// 连接 y 左子树和x
tr[x].right = tr[y].left;
if (tr[y].left) tr[tr[y].left].parent = x;
// 连接 y 和 x 的父亲节点
if (!tr[x].parent) root = y;
else if ( tr[tr[x].parent].left == x) tr[tr[x].parent].left = y;
else tr[tr[x].parent].right = y;
// 对调 y 和 x
tr[y].parent = tr[x].parent;
tr[y].left = x;
tr[x].parent = y;
}
/*
x y
/ -> \
y x
*/
void rightRotate(int x) {
int y = tr[x].left;
// 连接 y 的右子树和 x
tr[x].left = tr[y].right;
if (tr[y].right) tr[tr[y].right].parent = x;
// 连接y与x的父亲节点
if (!tr[x].parent) root = y;
else if(tr[tr[x].parent].left == x) tr[tr[x].parent].left = y;
else tr[tr[x].parent].right = y;
// 对调 x 和 y
tr[y].parent = tr[x].parent;
tr[y].right = x;
tr[x].parent = y;
}
/*
node(BLCAK) node(RED_NODE)
/ \ -> / \
l(RED_NODE) r(RED_NODE) l(BLACK_NODE) r(BLACK_NODE)
*/
int flipColor(int node) {
int l = tr[node].left, r = tr[node].right;
bool color = tr[node].color;
tr[node].color = tr[l].color;
tr[l].color = color;
tr[r].color = color;
return node;
}
int newNode(int k, int v) {
if (tt < 0) nodes[++tt] = ++idx;
int u = nodes[tt--];
tr[u].init(k, v);
return u;
}
//思考一个问题,为什么tt之前不会有u
void gcNode(int u) {
nodes[++tt] = u;
}
// 现在讨论叔叔节点的红与黑
void fixRBAfterAdd(int x) {
while (tr[tr[x].parent].color == RED_NODE) {
//若冲突一定存在叔叔节点,哨兵节点也算,而且爷爷一定是黑的
int parent = tr[x].parent;
int pparent = tr[parent].parent;
if (tr[pparent].left == tr[x].parent) {
// 当x的父亲节点是祖父节点的左孩子时
if (tr[tr[pparent].right].color == RED_NODE) {
// 情况1: x 的叔叔节点是红色的
flipColor(pparent);
x = pparent;
} else {
if (tr[parent].right == x) {
// 情况2: x 的叔叔节点是黑色的,x 是父亲节点的右孩子, 转化为情况3来考虑
leftRotate(parent);// 保证旋转的时候不会跑到另一侧
}
// 情况3:x 的叔叔节点是黑色的, x 是父亲节点的左孩, 父亲节点旋转到叔叔一侧
tr[tr[pparent].left].color = BLACK_NODE;
tr[pparent].color = RED_NODE;
x = tr[pparent].left;
rightRotate(pparent);
}
} else {
// 当x的父亲节点是祖父节点的右孩子时
if (tr[tr[pparent].left].color == RED_NODE) {
// 情况1: x的大叔叔颜色是红色的
flipColor(pparent);
x = pparent;
} else {
if (tr[parent].left == x) {
// 情况2:x 的大叔叔颜色是黑色的,x是父节点的左孩子
rightRotate(parent);
}
// 情况3: x 的大叔叔夜色是黑色的,x是父节点的右孩子
tr[tr[pparent].right].color = BLACK_NODE;
tr[pparent].color = RED_NODE;
leftRotate(pparent);
x = parent;// assert parent == tr[x].parent
}
}
}
tr[root].color = BLACK_NODE;
}
//v 子树替代替代 u 子树
void transplant(int u, int v) {
if (!tr[u].parent) root = v;
else if (tr[tr[u].parent].left == u) tr[tr[u].parent].left = v;
else tr[tr[u].parent].right = v;
tr[v].parent = tr[u].parent;
}
void add(int k, int v) {
int y = 0;
int x = root;
while(x) {
y = x;
if (k < tr[x].key) x = tr[x].left;
else x = tr[x].right;
}
int u = newNode(k, v);
tr[u].parent = y;
if (!y)
root = u;
else if (k < tr[y].key)
tr[y].left = u;
else
tr[y].right = u;
// 当前根节点如果是红不被修复
fixRBAfterAdd(u);
}
//返回最靠左的非零元素
int minimum(int node) {
int last = 0;
int u = node;
while(u) {
last = u;
u = tr[u].left;
}
return last;
}
//x指向了一个双重黑色的节点
void fixAfterRemove(int x) {
//cc++;
while (x != root && tr[x].color == BLACK_NODE) {
if (x == tr[tr[x].parent].left) {
// 双重黑色节点在左侧
int w = tr[tr[x].parent].right;
if (tr[w].color == RED_NODE) {
// 情况1: 兄弟节点w是红色的,毫无疑问,根据红黑树的性质w的两个孩子也是黑色的
tr[w].color = BLACK_NODE;
tr[tr[x].parent].color = RED_NODE; // w 是红色,所以x和w的父亲都是黑色
leftRotate(tr[x].parent);
x = tr[x].parent;
// w 不必重新求
} else {
// 情况2: 兄弟节点w是黑色的,两个孩子也是黑色的
// 将x和w各抽离掉一层黑色往上传递, w抽掉黑色不影响黑平衡
if (tr[tr[w].left].color == BLACK_NODE && tr[tr[w].right].color == BLACK_NODE) {
tr[w].color = RED_NODE;
x = tr[x].parent;
}
// 情况3:兄弟节点w是黑色的,左孩子是红色,右孩子是黑色的
// w一侧匀过去一个红色节点承接一层黑色,w往上移动了一次,左侧少了一层黑色,所以希望w的原来的右孩子是红的
// 可以在旋转之前把w的原始左孩子旋转过去,让右孩子变成红色,也就是情况四
else if (tr[tr[w].right].color == BLACK_NODE) {
tr[tr[w].left].color = BLACK_NODE;
tr[w].color = RED_NODE;
rightRotate(w);
}
// 情况4:兄弟节点w是黑色的,右孩子是红色
// 匀到左侧一个节点,这边把顶替上来的红色染成黑色
else {
tr[w].color = tr[tr[x].parent].color;
tr[tr[x].parent].color = RED_NODE;
tr[tr[w].right].color = BLACK_NODE;// 弥 w被抽离了黑色到,父亲节点颜色保持原状并无叠加
leftRotate(tr[x].parent);
x = tr[x].parent;
}
}
} else {
// 双重黑色节点在左侧
int w = tr[tr[x].parent].left;
if (tr[w].color == RED_NODE) {
// 情况1:兄弟节点是w是红色的
tr[w].color = BLACK_NODE;
tr[tr[x].parent].color = RED_NODE;
rightRotate(tr[x].parent);
x = tr[x].parent;
} else {
if (tr[tr[w].left].color == BLACK_NODE && tr[tr[w].right].color == BLACK_NODE) {
// 情况2:兄弟节点是w是黑色的,两个孩子节点都是黑色的
tr[w].color = RED_NODE;
x = tr[x].parent;
} else if (tr[tr[w].left].color == BLACK_NODE) {
// 情况3 : 兄弟节点的左孩子是黑色的
tr[tr[w].right].color = BLACK_NODE;
tr[w].color = RED_NODE;
leftRotate(w);
} else {
tr[w].color = tr[tr[x].parent].color;
tr[tr[x].parent].color = RED_NODE;
tr[tr[w].left].color = BLACK_NODE;
rightRotate(tr[x].parent);
x = tr[x].parent;
}
}
}
}
tr[x].color = BLACK_NODE;
}
bool rbDelete(int z) {
// 记录被移除节点的颜色, z有一个子树值是空就移除它本身
bool yColor = tr[z].color;
int y = z;
int x;
if (!tr[z].left) {
x = tr[z].right;
transplant(z, tr[z].right);
} else if (!tr[z].right) {
x = tr[z].left;
transplant(z, tr[z].left);
} else {
// 找到 z 的后继节点, z 的左右孩子都不为空
y = minimum(tr[z].right);
yColor = tr[y].color;
x = tr[y].right;// x 有可能是一个黑色的空节点,但是不影响
yColor = tr[y].color;
// 移花接木y,并且先接入y的右孩子
if (tr[y].parent == z) {
tr[x].parent = y; // 这是一句废话
} else {
transplant(y, tr[y].right);
tr[y].right = tr[z].right;
tr[tr[y].right].parent = y;
}
transplant(z, y);
// 再接入y的左孩子
tr[y].left = tr[z].left;
tr[tr[y].left].parent = y;
tr[y].color = tr[z].color;
gcNode(z);
// 接下来只需要考虑被移除节点的颜色即可
if (yColor == BLACK_NODE)
fixAfterRemove(x);
}
return true;
}
int find(int k) {
int u = root;
while(u) {
if (k < tr[u].key) u = tr[u].left;
else if (k > tr[u].key) u = tr[u].right;
else return u;
}
return 0;
}
bool remove(int k) {
int z = find(k);
if(z) return rbDelete(z);
return false;
}
};