二叉查找树的基本操作包括搜索、插入、删除、取最大和最小值等都能够在O(h)时间复杂度内实现,因此能在期望时间O(lgn)下实现,但是二叉查找树的平衡性在这些操作中并没有得到维护,因此其高度可能会变得很高,当其高度较高时,而二叉查找树的性能就未必比链表好了,所以二叉查找树的集合操作是期望时间O(lgn),最坏情况下为O(n)。
红黑树也是一种二叉查找树,它拥有二叉查找树的性质,同时红黑树还有其它一些特殊性质,这使得红黑树的动态集合基本操作在最坏情况下也为O(lgn),红黑树通过给节点增加颜色和其它限制条件,使得红黑树中的某一节点到叶节点的任一路径不会比其它路径长两倍,因此可以保持树的平衡性。红黑树中节点包括五个域:颜色、关键字、左节点、右节点和父节点,并将没有子节点或父节点的指针指向一个 nil 哨兵节点,这个nil节点称着叶节点,也是外节点,其它包含关键字的节点叫着内节点。之所以要增加这样一个为空的外节点,是为了方便红黑树上的一些边界操作,特别是在删除节点的时候,这个nil节点就很有用了。
红黑树的五个性质:
1、每个节点或者是红的,或者是黑的。
2、根节点是黑的。
3、叶节点(nil节点)是黑的。
4、如果一个节点是红的,则其孩子节点都是黑的。
5、对于任意节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点。
从某个节点出发到达叶节点的任意一条路径上,黑节点的个数称为该节点的黑高度bh(x)。红黑树的黑高度就定义为根节点的黑高度。红黑树在执行一些搜索,取最大最小值,取前驱后继节点等操作没有修改红黑树,因此跟二叉查找树上的操作相同,但是对于插入和删除操作,红黑树由于被修改了,必须执行一些额外的操作来维护其红黑树的性质,实际上这些额外的操作也不会改变插入和删除操作的时间复杂度,仍然为O(lgn)。
为了维护红黑树的性质,需要执行一些红黑树特有的操作:旋转,包括左旋和右旋。
左旋:对节点x做左旋,必须保证它的右孩子y 不为nil节点,旋转以x到y之间的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的左孩子,y原来的左孩子就成为x的右孩子。时间复杂度为O(1)。
右旋:与左旋相反,对节点x做右旋,必须保证它的左孩子y不为nil节点,旋转以x到y的支轴进行,让y取代x的位置成为该子树新的根,而x成为y的右孩子,y原来的右孩子就成为x的左孩子。时间复杂度为O(1)。
在对红黑树执行插入和删除之后,需要执行额外的维护操作,这些操作就是建立在旋转之上的。下面分析一下在红黑树中插入和删除节点之后可能出现的问题,及解决思路:
1、插入节点
向红黑树中插入一个新的节点,为了不增加黑节点的个数(从而影响黑高度),我们将新插入的节点涂为红色,这样插入之后它可能会违反性质2和4,在未付性质2和4的同时还得注意不能违反性质5,主要考虑这三个性质即可。因此维护的主要任务就是通过修改红黑树中节点的结构和颜色让根节点为黑色,同时让插入的红节点的父节点变成黑色(如果不是黑色)。
2、删除节点
从红黑树中删除一个节点之后,如果这个节点是红的,则红黑性质没有被破坏,因为被删除的是红节点,所以不是根节点,性质2没问题,同时黑节点没有变,所以性质5没问题,因为被删除节点的父节点是黑,所以无论替补上去的节点是什么颜色都不会破坏性质4,性质1和3没有被影响,所以所有性质都没有破坏。如果被删除的节点是黑色的,则节点的黑高度变化了,性质5被破坏了,维护的主要任务就是通过修改节点的结构和颜色从而增加该子树一个黑高度。
在插入或者删除节点之后,维护红黑树的性质的时候,都是只有一种情形可以直接通过变换来保持红黑树的性质,其它情形就必须通过变换转化到这一种可以直接解决的出口情形。至于具体的解决过程请见下面的代码注释。
/* * 算法导论 第十三章 红黑树 */ #include <iostream> using namespace std; //定义颜色 enum Color { BLACK = 0, RED = 1 }; //定义红黑树节点 typedef struct RBTNode { Color color; int key; RBTNode *left, *right, *parent; RBTNode() { this->color = Color::RED; this->key = 0; this->left = this->right = this->parent = NULL; } RBTNode(Color c, int k, RBTNode* l, RBTNode* r, RBTNode* p) { this->color = c; this->key = k; this->left = l; this->parent = p; this->right = r; } }RBTNode, RBTree; //定义一个哨兵nilNode,以方便处理边界问题 //特别是在删除红黑树元素时很有用 // 如果删除节点没有孩子,nilNode作为其孩子处理起来就方便多了 RBTNode* nilNode = NULL; /* * 中序遍历 * 递归 */ void inOrderTreeWalkRecursion(RBTree* tree) { if (tree && tree != nilNode) { inOrderTreeWalkRecursion(tree->left); cout<<tree->key<<" "; inOrderTreeWalkRecursion(tree->right); } } /* * 查找二叉排序树中的最小元素 * 即为最左元素 * 时间复杂度为O(lgn) */ RBTNode* rbTreeMinimum(RBTree* tree) { if (! tree || tree == nilNode) return NULL; while (tree->left != nilNode) { tree = tree->left; } return tree; } /* * 求二叉排序树中指定节点node的中序遍历后继 * 如果node右子树不为空,则后继则为node右子树中的最小节点 * 否则node右子树为空,必须向上回溯找到第一个节点:该节点为其父节点的左孩子 * 后继即为其父节点,如果不存在这样的节点,说明node为最右节点,后继为空 * 时间复杂度为O(lgn) */ RBTNode* rbTreeSuccessor(RBTNode* node) { if (! node || node == nilNode) return NULL; if (node->right != nilNode) { return rbTreeMinimum(node->right); } RBTNode* temp = node->parent; while (temp != nilNode && node == temp->right) { node = temp; temp = node->parent; } return temp; } /* * 红黑树节点左旋 */ void leftRotate(RBTree* &tree, RBTNode* node) { //如果node为空或者其右节点为空,就返回 if (! node || node == nilNode || node->right == nilNode) return; //让node右节点的左孩子成为node的右孩子 RBTNode* temp = node->right; node->right = temp->left; if (temp->left != nilNode) { temp->left->parent = node; } //让node的右孩子取代node的位置 temp->parent = node->parent; if (node->parent == nilNode) {//说明node是根节点,修改根节点的指针 tree = temp; } else { if (node == node->parent->left) { node->parent->left = temp; } else { node->parent->right = temp; } } //让node成为其右孩子的左孩子 temp->left = node; node->parent = temp; } /* * 红黑树节点右旋 */ void rightRotate(RBTree* &tree, RBTNode* node) { //如果node为空或者其左节点为空,就返回 if (! node || node == nilNode || node->left == nilNode) return; //让node左节点的右孩子成为node的左孩子 RBTNode* temp = node->left; node->left = temp->right; if (temp->right != nilNode) { temp->right->parent = node; } //让node的右左孩子取代node的位置 temp->parent = node->parent; if (node->parent == nilNode) {//说明node是根节点,修改根节点的指针 tree = temp; } else { if (node == node->parent->left) { node->parent->left = temp; } else { node->parent->right = temp; } } //让node成为其左孩子的右孩子 temp->right = node; node->parent = temp; } /* * 红黑树插入元素后,对树进行调整以保持红黑树的性质 * 主要耗时在将违规的位置上升,因为高度为O(lgn),所以其时间复杂度依然为O(lgn) * 而旋转一共不超过2次,每次时间复杂度为O(1) * 所以总的时间复杂度为O(lgn) */ void rbInsertFixup(RBTree* &tree, RBTNode* node) { if (! tree || ! node) return; // 因为新插入的node颜色为红,只可能违反性质2(根节点为黑色)和性质4(红节点的孩子节点只能为黑) // 所以如果node的父节点不是nilNode,则只有在其父节点也为红的时候才违反红黑树的性质,需要调整 // 否则不用调整 while (node->parent->color == Color::RED) { //主要考虑node的叔叔节点 RBTNode* uncleNode = NULL; if(node->parent == node->parent->parent->left) { uncleNode = node->parent->parent->right; if (uncleNode->color == Color::RED) { // 情形1:node的叔叔节点为红,则将违规位置上移 // 因为node的父节点和叔叔节点均为红,则node的祖父节点必然为黑 // 因此可以将node的父节点和叔叔节点均变成黑,而祖父节点变成红 // 这样依然保持了整体的黑高度没变,保持了性质5(任意节点到其子孙节点的路径上黑节点个数相同) node->parent->color = Color::BLACK; uncleNode->color = Color::BLACK; node->parent->parent->color = Color::RED; node = node->parent->parent; } else { // 情形2:node的叔叔节点为黑 // 情形2.1:node为其父节点的右孩子,将情形2.1转化成2.2以便处理,因为node和其父节点都是红的,所以演父节点旋转没有<span style="white-space:pre"> </span> 影响 if (node == node->parent->right) { node = node->parent; leftRotate(tree, node);//从2.1变成了2.2 } // 情形2.2:node为其父节点的左孩子,这种情形为出口情形 // 因为父节点为红,叔叔节点为黑,则祖父节点一定为黑,所以将父节点变为黑 // 祖父节点变为红,再沿祖父节点右旋则黑高度没变,同时node的父节点变黑了 // 达到主要目的:让node的父节点变成黑色,以保持性质4 // 在这个过程中又不能改变黑高度,因为要保持性质5 node->parent->color = Color::BLACK; node->parent->parent->color = Color::RED; rightRotate(tree, node->parent->parent); } } else { //叔叔节点的位置相反,处理方法同上,只是旋转时候的方向相反 uncleNode = node->parent->parent->left; if (uncleNode->color == Color::RED) { // 情形1:node的叔叔节点为红,则将违规位置上移 node->parent->color = Color::BLACK; uncleNode->color = Color::BLACK; node->parent->parent->color = Color::RED; node = node->parent->parent; } else { // 情形2:node的叔叔节点为黑 // 情形2.1:node为其父节点的左孩子 if (node == node->parent->left) { node = node->parent; rightRotate(tree, node);//从2.1变成了2.2 } // 情形2.2:node为其父节点的右孩子,这种情形为出口情形 node->parent->color = Color::BLACK; node->parent->parent->color = Color::RED; leftRotate(tree, node->parent->parent); } } } //保持性质2,让根节点为黑 tree->color = Color::BLACK; } /* * 插入元素 * 搜索插入点时间为O(lgn) * 维护红黑树的性质时间为O(lgn) * 所以总的时间复杂度为O(lgn) */ void rbInsert(RBTree* &tree, RBTNode* node) { if (! tree || ! node) return; RBTNode* posNode = nilNode; RBTNode* t = tree; while (t != nilNode) { posNode = t; if (node->key < t->key) { t = t->left; } else { t = t->right; } } node->parent = posNode; if (posNode == nilNode) { tree = node; } else { if (node->key < posNode->key) { posNode->left = node; } else { posNode->right = node; } } //不同于二叉排序树的地方 node->left = node->right = nilNode; node->color = Color::RED;//插入红节点以保证黑高度不变 //维护红黑树性质 rbInsertFixup(tree, node); } /* * 删除红黑树节点后维护红黑树的性质 * 主要耗时仍然是将违规位置上升,时间复杂度为O(lgn) * 旋转最多3次,共O(1) * 所以时间复杂度为O(lgn) */ void rbDeleteFixup(RBTree* &tree, RBTNode* node) { //如果node是根节点,则整个树的黑高度都减一,没有影响 //如果node的颜色是红色,则直接涂黑就可以了 // 否则就要维护了,红黑树节点删除之后,维护性质主要与node的兄弟节点有关 while (node != tree && node->color == Color::BLACK) { //当前节点node为非根黑节点,考虑其兄弟节点 RBTNode* brotherNode = NULL; if (node == node->parent->left) { brotherNode = node->parent->right; //情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2 //应为node节点为黑,兄弟节点为红,则其父节点必然为黑 // 且兄弟节点的子节点必然为黑,因此可以将父节点变红,兄弟节点变黑 // 然后沿父节点左旋,则黑高度没有被影响,同时node的新的兄弟节点变成了原兄弟节点的左黑孩子了 // 因此node的兄弟节点变为黑节点了 if (brotherNode->color == Color::RED) { node->parent->color = Color::RED; brotherNode->color = Color::BLACK; leftRotate(tree, node->parent); brotherNode = node->parent->right; } //情形2:兄弟节点为黑 if (brotherNode->right->color == Color::BLACK && brotherNode->left->color == Color::BLACK) { //情形2.1:兄弟节点的右孩子为黑,且其左孩子为黑 //这时node为黑,其兄弟节点和两个孩子节点也均为黑 //因为此时node子树比其兄弟节点的子树黑高度小1,所以可以把兄弟节点变红,从而二者黑高度相同 // 从而让node的父节点子树整个黑高度降低1(违反规则5),让node的父节点变成新的node,使违规的位置上升 // 继续循环 brotherNode->color = Color::RED; node = node->parent; } else { if (brotherNode->right->color == Color::BLACK) { //情形2.2:兄弟节点的右孩子为黑,且其左孩子为红,将这种情形转换成2.3 //因为兄弟节点为黑,其左孩子为红,右孩子为黑,因此可以将兄弟节点的左孩子变黑 //兄弟节点变红,然后沿兄弟节点右旋,从而进入情形2.3,且为改变黑高度 brotherNode->left->color = Color::BLACK; brotherNode->color = Color::RED; rightRotate(tree, brotherNode); brotherNode = node->parent->right; } //情形2.3:兄弟节点的右孩子为红,左孩子任意,这时出口情形,经过以下变换,算法结束 // 因为兄弟节点为黑,但其右孩子为红,可以把兄弟节点变成父节点的颜色,把其右孩子和node父节点变黑 // 然后沿node父节点左旋,则node的兄弟节点的位置仍然是黑色(为brother的右孩子),该子树的黑高度不变 // 而node节点所在子树因为增加了node父节点这个黑节点而黑高度增加1,所以两边的黑高度相同了 // (原来node子树比其兄弟子树黑高度少1),从而保持了性质5 brotherNode->color = node->parent->color; node->parent->color = Color::BLACK; brotherNode->right->color = Color::BLACK; leftRotate(tree, node->parent); //问题已解决,让node指向根节点,从而退出循环 node = tree; } } else {//兄弟节点的位置相反,原理同上 brotherNode = node->parent->left; //情形1:兄弟节点为红,将其转换成兄弟节点为黑的情形2 if (brotherNode->color == Color::RED) { node->parent->color = Color::RED; brotherNode->color = Color::BLACK; rightRotate(tree, node->parent); brotherNode = node->parent->left; } //情形2:兄弟节点为黑 if (brotherNode->left->color == Color::BLACK && brotherNode->right->color == Color::BLACK) { //情形2.1:兄弟节点的左孩子为黑,且其右孩子为黑 brotherNode->color = Color::RED; node = node->parent; } else { if (brotherNode->left->color == Color::BLACK) { //情形2.2:兄弟节点的左孩子为黑,且其右孩子为红,将这种情形转换成2.3 brotherNode->right->color = Color::BLACK; brotherNode->color = Color::RED; leftRotate(tree, brotherNode); brotherNode = node->parent->left; } //情形2.3:兄弟节点的左孩子为红,右孩子任意,这时出口情形,经过以下变换,算法结束 brotherNode->color = node->parent->color; node->parent->color = Color::BLACK; brotherNode->left->color = Color::BLACK; rightRotate(tree, node->parent); //问题已解决,让node指向根节点,从而退出循环 node = tree; } } } node->color = Color::BLACK; } /* * 删除红黑树节点 * 主要耗时是在获取删除节点的中序遍历后继节点,时间复杂度为O(lgn) * 而维护红黑树性质的时间复杂度为O(lgn) * 所以总的时间复杂度为O(lgn) */ RBTNode* rbDelete(RBTree* &tree, RBTNode* node) { if (! tree || ! node) return; RBTNode* delNode = NULL; if (node->left == nilNode || node->right == nilNode) { delNode = node; } else { delNode = rbTreeSuccessor(node); } RBTNode* fillNode = NULL; if (delNode->left != nilNode) { fillNode = delNode->left; } else { fillNode = delNode->right; } fillNode->parent = delNode->parent; if (delNode->parent == nilNode) { tree = fillNode; } else { if (fillNode == fillNode->parent->left) { fillNode->parent->left = fillNode; } else { fillNode->parent->right = fillNode; } } if (delNode != node) { node->key = delNode->key; } //如果被删除节点是黑色,则树中某些的黑高度被减一,必须维护性质5 //将该节点相关路径上增加一个黑节点 if (delNode->color == Color::BLACK) { rbDeleteFixup(tree, fillNode); } return delNode; }