红黑树是一种相当复杂的数据结构,一种能够保持平衡的二叉查找树。如果条件极端,随机生成的二叉树可能就是一个单链表,深度为 $n$ ,而红黑树的高度,即使在最坏情况下也是 $\Theta(n)$ ,红黑树通过满足以下5条性质来保证这一点:
- 节点是红色或者黑色的。
- 根节点的黑色的。
- NIL节点时黑色的。
- 每个红色节点的左子节点和右子节点必定是黑色的。
- 任意叶子节点的黑深度相等。
注:这里以及下文的叶子节点是指真正的有意义的“叶子节点”而不是NIL节点。如:这是一颗红黑树,注意所有NIL节点其实都是一个节点。
我仔细研究了红黑树,并自己实现了它,这是一个多月来看《算法导论》给我带来成就感最大的一次。我改进了之前二叉查找树的代码,使用二叉树-二叉查找树-红黑树和二叉树节点-红黑树节点的继承关系链;并且,为了 增强 算法复杂部分 代码的可读性,我对部分功能函数实现了一些看上去有点累赘的重载。这篇博文可能不会分析这些比较简单的重载,但是完整的代码可以点击这里下载(方便起见,我将实现和定义全部写在一个头文件中)。
这篇博文参考了《算法导论》第12、13章和维基百科的“红黑树”词条,所用的示意图也来自于维基百科中,这里先作说明。此外,这一篇仅分析红黑树的实现,不设计章节后面的习题。
二叉树
二叉树是最简单的,我提供了一些基本的功能。我尽量使变量名和函数名 不言自明,所以不会作过多解释。
先看二叉树节点:
template <typename T> class xBinaryTreeNode{ public: xBinaryTreeNode(); xBinaryTreeNode(T val); T data; xBinaryTreeNode<T>* leftChild; xBinaryTreeNode<T>* rightChild; xBinaryTreeNode<T>* father; }; template <typename T> xBinaryTreeNode<T>::xBinaryTreeNode(){ leftChild = rightChild = father = NULL; } template <typename T> xBinaryTreeNode<T>::xBinaryTreeNode(T val){ data = val; leftChild = rightChild = father = NULL; }
然后看二叉树的声明:
template <typename T> class xBinaryTree{ public: xBinaryTree(); xBinaryTreeNode<T>* getHead(); bool isEmpty(); bool doesExit(xBinaryTreeNode<T>* node); bool isRoot(xBinaryTreeNode<T>* node); bool hasLeftChild(xBinaryTreeNode<T>* node); bool hasRightChild(xBinaryTreeNode<T>* node); xBinaryTreeNode<T>** getSelfFromFather(xBinaryTreeNode<T>* node); xBinaryTreeNode<T>** getBrother(xBinaryTreeNode<T>* node); protected: xBinaryTreeNode<T>* nilNode; };
有几点需要说明:
- nilNode是一个存在的“空节点”,是根节点(或称头结点)的父节点,也是所有叶子节点实际上的子节点。
- getSelfFromFather()/getBrother()函数返回一个 指向 父节点中指向自己/兄弟的指针 的指针,修改返回值的引用,就相当于直接修改父节点中指向自己/兄弟的指针,从而在修改二叉树时避免了累赘的判断。
template <typename T> xBinaryTreeNode<T>** xBinaryTree<T>::getSelfFromFather(xBinaryTreeNode<T>* node){ if (node->father->leftChild == node){ return &(node->father->leftChild); } else if(node->father->rightChild == node){ return &(node->father->rightChild); } return NULL; }
二叉查找树
二叉查找树确保节点左子树中所有节点小于节点值,右子树中所有节点大于节点值。二叉查找树继承了二叉树,声明如下:
template <typename T> class xBinarySearchTree : public xBinaryTree<T>{ public: xBinarySearchTree(); void insertVal(T val); xBinaryTreeNode<T>* searchVal(T val, xBinaryTreeNode<T>* node); void removeNode(xBinaryTreeNode<T>* node); void logInOrder(xBinaryTreeNode<T>* node = NULL); protected: xBinaryTreeNode<T>** descentNode(T val, xBinaryTreeNode<T>* node); void insertNode(xBinaryTreeNode<T>* newNode, xBinaryTreeNode<T>* node); xBinaryTreeNode<T>* minTreeNode(xBinaryTreeNode<T>* node); xBinaryTreeNode<T>* maxTreeNode(xBinaryTreeNode<T>* node); };
二叉查找树最关键的逻辑,就是将节点值和另一个值比较,并返回自己的左节点或右节点。插入和查询都使用了这一套逻辑,相比于上一篇中的代码,这一篇中我将这层逻辑抽象出来:
template <typename T> xBinaryTreeNode<T>** xBinarySearchTree<T>::descentNode(T val, xBinaryTreeNode<T>* node){ if (!doesExit(node)){ return &nilNode; } if (node->data <= val){ return &(node->rightChild); } else if (node->data > val){ return &(node->leftChild); } return NULL; }
在插入和查询的代码中有相应修改,删除节点的操作和上一篇中几乎一样。
红黑树
先看红黑树节点,继承自二叉树节点,扩展了一个属性,就是颜色是红或黑。 构造函数里将其设定为红色。
template <typename T> class xRBTreeNode : public xBinaryTreeNode<T>{ public: xRBTreeNode(); xRBTreeNode(T val); bool isRed; }; template <typename T> xRBTreeNode<T>::xRBTreeNode(){ isRed = true; } template <typename T> xRBTreeNode<T>::xRBTreeNode(T val){ data = val; isRed = true; }
然后看红黑树的声明,有一些函数有较多的重载,只是为了接受不同类型的参数(二叉树节点或红黑树节点),其功能是一致的。
template <typename T> class xRBTree: public xBinarySearchTree<T>{ public: xRBTree(); void output(); void insertVal(T val); void removeNode(xRBTreeNode<T>* node); void test(); private: xRBTreeNode<T>* nilNodeDup; /* A xRBTreeNode_type copy of nilNode, pointing to the same node */ void output(xRBTreeNode<T>* node, int step, xRBTreeNode<T>* node, int step, ofstream* file); bool isRed(xRBTreeNode<T>* node); bool isBlack(xRBTreeNode<T>* node); bool isRed(xBinaryTreeNode<T>* node); bool isBlack(xBinaryTreeNode<T>* node); void setRed(xRBTreeNode<T>* node); void setBlack(xRBTreeNode<T>* node); void setRed(xBinaryTreeNode<T>* node); void setBlack(xBinaryTreeNode<T>* node); bool rotateLeft(xRBTreeNode<T>* node); bool rotetaRight(xBinaryTreeNode<T>* node); bool rotateLeft(xBinaryTreeNode<T>* node); bool rotetaRight(xRBTreeNode<T>* node); bool isLeftChild(xBinaryTreeNode<T>* node); bool isRightChild(xBinaryTreeNode<T>* node); bool isLeftChild(xRBTreeNode<T>* node); bool isRightChild(xRBTreeNode<T>* node); void insertNode(xRBTreeNode<T>* newNode, xRBTreeNode<T>* node = NULL); void fixInsert(xRBTreeNode<T>* node); void fixRemove(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case1(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case2(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case3(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case4(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case5(xBinaryTreeNode<T>* P, bool NisLeft); void fixRemove_case6(xBinaryTreeNode<T>* P, bool NisLeft); };
注意红黑树的一个基本操作是旋转。对具有右子节点的节点,可以左旋;对具有左子节点的节点,可以右旋。以左旋为例,旋转后右子结点成为新的(子树的)根节点,其原先的左子树被换到了原根节点的右子树位置,因此二叉查找树的性质(即某节点左子树中所有的值小于节点值,右子树中所有的值大于节点值)却没有受到影响。左旋和右旋的示意图如下:
左旋和右旋的实现如下:
template <typename T> bool xRBTree<T>::rotateLeft(xBinaryTreeNode<T>* node){ if (!doesExit(node->rightChild)){ return false; } xBinaryTreeNode<T>* passenger = node->rightChild->leftChild; *getSelfFromFather(node) = node->rightChild; node->rightChild->father = node->father; node->rightChild->leftChild = node; node->father = node->rightChild; node->rightChild = passenger; if (doesExit(passenger)){ passenger->father = node; } return true; } template <typename T> bool xRBTree<T>::rotetaRight(xBinaryTreeNode<T>* node){ if (!doesExit(node->leftChild)){ return false; } xBinaryTreeNode<T>* passenger = node->leftChild->rightChild; *getSelfFromFather(node) = node->leftChild; node->leftChild->father = node->father; node->leftChild->rightChild = node; node->father = node->leftChild; node->leftChild = passenger; if (doesExit(passenger)){ passenger->father = node; } return true; }
红黑树的插入
重点来了。红黑树具有本文一开始提到的那几条性质,插入和删除节点的过程可能会破坏那几条性质,我们就要设法恢复它。被破坏的性质可以这样描述,通过某个节点的所有叶子节点的黑深度比没通过该节点的叶子节点的黑深度少/多了1,我们要做的就是监视这些叶子节点(实际上是监视子树),在恢复后子树的黑深度保持一致。
红黑树的插入一个红色节点的逻辑较为复杂,大致如下:
按照二叉查找树中的方法插入节点 N ,然后执行fixInsert(N)
- 如果N的父节点P是黑色的
- 如果P是nilNode(即N是根节点),那么将N染成黑色,插入完成了。
- 如果P不是nilNode而是其他什么黑色节点(N不是根节点),那么什么也不用做,插入完成了。
- 如果N的父节点P是红色的
- 如果N的叔叔U是红色的,那么将U和P染成黑色,将N的爷爷节点G染成红色,然后执行fixInsert(G)。
- 如果N的叔叔U是黑色:
- 如果N为左子节点且P为左子节点(如图) / N为右子节点且P为右子结点(与图对称),那么右旋/左旋P并交换P和G的颜色①
- 如果N为右子结点且P为左子结点(如图) / N为左子结点且P为右子结点(与图对称),那么左旋/右旋P转化为①中的情形
- 如果N为左子节点且P为左子节点(如图) / N为右子节点且P为右子结点(与图对称),那么右旋/左旋P并交换P和G的颜色①
- 如果N的叔叔U是红色的,那么将U和P染成黑色,将N的爷爷节点G染成红色,然后执行fixInsert(G)。
与删除比较,插入操作还算比较简单,只将逻辑聚合在两个函数里。我的实现如下:
template <typename T> void xRBTree<T>::insertNode(xRBTreeNode<T>* newNode, xRBTreeNode<T>* node = NULL){ if (node == NULL){ node = (xRBTreeNode<T>*)getHead(); } xBinarySearchTree<T>::insertNode((xBinaryTreeNode<T>*)newNode, (xBinaryTreeNode<T>*)node); fixInsert(newNode); } template <typename T> void xRBTree<T>::fixInsert(xRBTreeNode<T>* node){ if (node == getHead()){ setBlack(node); return; } /* black father */ if (isBlack(node->father)){ return; } /* red father */ else if (isRed(node->father)){ /* red uncle */ if (isRed(*getBrother(node->father))){ setBlack(node->father); setBlack(*getBrother(node->father)); setRed(node->father->father); fixInsert((xRBTreeNode<T>*)node->father->father); } /* black uncle */ else if (isBlack(*getBrother(node->father))){ if (isLeftChild(node) && isLeftChild(node->father)){ setBlack(node->father); setRed(node->father->father); rotetaRight(node->father->father); }else if (isLeftChild(node) && isRightChild(node->father)){ setBlack(node); setRed(node->father->father); rotetaRight(node->father); rotateLeft(node->father); } else if (isRightChild(node) && isLeftChild(node->father)){ setBlack(node); setRed(node->father->father); rotateLeft(node->father); rotetaRight(node->father); } else if (isRightChild(node) && isRightChild(node->father)){ setBlack(node->father); setRed(node->father->father); rotateLeft(node->father->father); } } } setBlack(getHead()); }
红黑树的删除
红黑树的删除是最复杂的,大致的逻辑如下:
- 如果待删除节点有右子树,则查找右子树中具有最小值的节点Z。交换待删除节点和最小值节点中的值,并删除最小值节点(该节点一定没有右子树),执行removeNode(Z)。
- 如果待删除节点没有右子树,直接执行removeNode(Z)。
删除一个没有右子树的节点Z,即removeNode(Z)过程:
- 如果Z为红色,那么直接调用基类二叉查找树的方法删除Z。
- 如果Z为黑色(Z只可能有左子节点):
- 如果Z的左子节点为红色,那么直接调用基类二叉树的方法删除Z。
- 如果Z的左子结点为黑色,那么先调用基类二叉树的方法删除Z,此时Z的左子树N(现在已经是Z的父亲的左子树或右子树,取决于Z本身是左子树还是右子树)的黑深度少了1,对Z的父亲P调用fixRemove(P, NisLeft),布尔值NisLeft值表示Z是左子树还是右子树,实际上就蕴含着了N是左子树或右子树。为什么不直接将N作为参数传入?因为有的情况下Z没有左子树,这是N就是nilNode,这种情况是合法的可能出现,但此时已经无法通过N(nilNode)访问P了。
节点P的左子树/右子树(取决于NisLeft)的黑深度少1,修正节点P,即fixRemove(P, NisLeft)过程:
- 如果被删除节点Z的左节点N就是根节点(case1),那么结束。
/* Case1 : N is root */ template <typename T> void xRBTree<T>::fixRemove_case1(xBinaryTreeNode<T>* P, bool NisLeft){ return; }
- 如果N不是根节点:
- N的兄弟节点S是红节点(case2),那么N的父节点P和S互换颜色,并左旋(如图)/右旋(与图对称)P,转入case4,case5或case6。
/* Case2 : N's brother S is red */ template <typename T> void xRBTree<T>::fixRemove_case2(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; setBlack(S); setRed(P); if (NisLeft){rotateLeft(P);} else{rotetaRight(P);} if (NisLeft){fixRemove(P, true);} else{fixRemove(P, false);} }
- N的兄弟节点S为黑节点(以下并列选项取决于NisLeft,若该值为真,与图示一致,并列选项取前者):
- S的右节点SR/S的左节点SL为黑
- SR/SL为黑
- N的父节点P为黑(case3),那么将S染为红色,再对P执行fixRemove(P, PisLeft),这里PisLeft需要传入节点P是否是P的父节点的左节点,需提前算出。
/* Case3 : S is Black, S-left, S-right are both Black, N's father P is black, */ template <typename T> void xRBTree<T>::fixRemove_case3(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; setRed(S); bool PisLeft = (P->father->leftChild==P) ? true : false; fixRemove(P->father, PisLeft); }
- N的父节点P为红(case4),那么将S和P调换颜色即可。
/* Case4 : S is Black, S-left, S-right are both Black, P is Red, */ template <typename T> void xRBTree<T>::fixRemove_case4(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; if (S != nilNodeDup){setRed(S);} setBlack(P); }
- N的父节点P为黑(case3),那么将S染为红色,再对P执行fixRemove(P, PisLeft),这里PisLeft需要传入节点P是否是P的父节点的左节点,需提前算出。
- SR/SL为红(case5),那么交换S和SL的颜色,并右旋/左旋S,进入case6。
/* Case5 : S is Black, S-right is Black, S-left is Red (P is unknown) under NisLeft True S is Black, S-left is Black, S-right is Red (P is unknown) under NisLeft False */ template <typename T> void xRBTree<T>::fixRemove_case5(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; xBinaryTreeNode<T>* SL = S->leftChild; xBinaryTreeNode<T>* SR = S->rightChild; if (NisLeft){ setBlack(SL); setRed(S); rotetaRight(S); } else{ /* N is right */ setBlack(SR); setRed(S); rotateLeft(S); } fixRemove_case6(P, NisLeft); }
- SR/SL为黑
- SR/SL为红(case6),那么交换P和S的颜色,并左旋/右旋P。
1
/* Case6 : S is Black, S-right is Red (S-left and P are unknown) under NisLeft True S is Black, S-left is Red (S-right and P are unknown) under NisLeft True */ template <typename T> void xRBTree<T>::fixRemove_case6(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; if (NisLeft){rotateLeft(P);} else{rotetaRight(P);} if (isRed(P)){setRed(S);} setBlack(P); }
- S的右节点SR/S的左节点SL为黑
- N的兄弟节点S是红节点(case2),那么N的父节点P和S互换颜色,并左旋(如图)/右旋(与图对称)P,转入case4,case5或case6。
过程fixRemove(P, NisLeft)的实现如下:
/* FIX_REMOVE PROGRESS * REF : zh.wikipedia.org/wiki/红黑树 * Begin * N is Root -> case1 * N is not Root: * S is Red -> case2 * S is Black: * S-right is Black: * S-left is Black: * P is Black -> case3 * P is Red -> case4 * S-left is Red -> case5 * S-right is Red -> case6 * End */ template <typename T> void xRBTree<T>::fixRemove(xBinaryTreeNode<T>* P, bool NisLeft){ xBinaryTreeNode<T>* N = NisLeft ? P->leftChild : P->rightChild; if (N == getHead()){ fixRemove_case1(P, NisLeft); } else{ /* N is not head */ xBinaryTreeNode<T>* S = NisLeft ? P->rightChild : P->leftChild; if (isRed(S)){ fixRemove_case2(P, NisLeft); } else{ // S is Black if (NisLeft){ xBinaryTreeNode<T>* SR = S->rightChild; if (isBlack(SR)){ xBinaryTreeNode<T>* SL = S->leftChild; if (isBlack(SL)){ if (isBlack(P)){ fixRemove_case3(P, NisLeft); } else{ /* P is Red */ fixRemove_case4(P, NisLeft); } } else{ /* SL is Red */ fixRemove_case5(P, NisLeft); } } else{ /* nodeSRight is Red */ fixRemove_case6(P, NisLeft); } } else{ /* N is Right */ xBinaryTreeNode<T>* SL = S->leftChild; if (isBlack(SL)){ xBinaryTreeNode<T>* SR = S->rightChild; if (isBlack(SR)){ if (isBlack(P)){ fixRemove_case3(P, NisLeft); } else{ /* P is Red */ fixRemove_case4(P, NisLeft); } } else{ /* SL is Red */ fixRemove_case5(P, NisLeft); } } else{ /* nodeSRight is Red */ fixRemove_case6(P, NisLeft); } } } } }
花了这么多功夫,当然要把测试结果摆上来。咳咳,我的意思是,前面几篇虽然没有测试结果,但我实际上也是测试过才发到博客里面来的。
先生成一个有31个元素的红黑树,
void xRBTree<int>::test(){ for (int i=1; i<32; i++){ insertVal(rand()%100); } /*removeNode((xRBTreeNode<T>*)getHead()); removeNode((xRBTreeNode<T>*)searchVal(36)); removeNode((xRBTreeNode<T>*)searchVal(62)); removeNode((xRBTreeNode<T>*)searchVal(95));*/ output(); }
输出并观察一下,嗯,插入应该没什么问题。
41:Black 24:Red 4:Black 0:Black 2:Red 16:Red 5:Black 21:Black 18:Red 27:Black 27:Black 34:Black 36:Red 62:Red 58:Black 45:Red 42:Black 53:Black 47:Red 61:Black 81:Black 69:Red 67:Black 64:Red 78:Black 91:Red 91:Black 82:Red 95:Black 92:Red 95:Red
取消注释,删掉根节点,删掉36,62,95三个节点,再输出,也没什么问题。注意,原先有两个值为95的节点,一个是另一个的父亲节点,看上去好像删掉的是儿子节点,实际上先搜索到的是父亲节点, 删除的也是它,现在处在父亲节点位置的实际上是原来的儿子节点。
42:Black 24:Red 4:Black 0:Black 2:Red 16:Red 5:Black 21:Black 18:Red 27:Black 27:Black 34:Black 64:Red 58:Black 47:Red 45:Black 53:Red 61:Black 81:Black 69:Red 67:Black 78:Black 91:Red 91:Black 82:Red 95:Black 92:Red
这样,红黑树的解释就结束了。