目录
1.红黑树
1.1红黑树的概念
1.2红黑树的性质
1.3红黑树节点的定义
1.4红黑树的插入操作
1.5红黑树的验证
1.6红黑树的删除
1.7红黑树与AVL树的比较
1.8红黑树的应用
1.红黑树
1.1红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
1.2红黑树的性质
1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
1.3红黑树节点的定义
我们将红色与黑色设置成枚举类型,使代码看起来更加规范,我们定义节点采用类模板的方式,内部成员分别是左节点指针,右节点指针,父亲节点指针,pair结构体类型的变量,然后就是代表我们颜色的变量。我们再对它们进行初始化列表就可以了,我们的每个节点初始颜色都得设置成红色,不然就无法满足我们上面给的5条红黑树的性质。
1.4红黑树的插入操作
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
1. 按照二叉搜索的树规则插入新节点
2. 检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何 性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连 在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红
cur和p均为红,违反了性质三,此处能否将p直接改为黑?——不能
解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
在我们搞清楚了插入的操做的原理之后,我们再来看它的代码就很容易明白了。
//插入 bool Insert(const pair<K, value>& kv) { if (_root == nullptr) { _root = new Node(kv); _root->_col = BLACK; return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (kv.first > cur->_kv.first) { parent = cur; cur = cur->_right; } else if (kv.first < cur->_kv.first) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(kv); cur->_col = RED; if (kv.first < parent->_kv.first) { parent->_left = cur; } else { parent->_right = cur; } cur->_parent = parent; //调整红黑树 //parent是黑的也直接结束 while (parent && parent->_col == RED) { //关键看叔叔 Node* grandf = parent->_parent; if (parent == grandf->_left) { Node* uncle = grandf->_right; //叔叔存在且为红,变色即可 if (uncle && uncle->_col == RED) { uncle->_col = parent->_col = BLACK; grandf->_col = RED; //继续往上处理 cur = grandf; parent = cur->_parent; } else//叔叔不存在,或则存在且为黑 { // g // p u //c if (cur == parent->_left) { parent->_col = BLACK; grandf->_col = RED; RotateR(grandf); } // g // p u // c else { RotateL(parent); grandf->_col = RED; cur->_col = BLACK; RotateR(grandf); } break; } } else { Node* uncle = grandf->_left; //叔叔存在且为红,变色即可 if (uncle && uncle->_col == RED) { uncle->_col = parent->_col = BLACK; grandf->_col = RED; //继续往上处理 cur = grandf; parent = cur->_parent; } else//叔叔不存在,或则存在且为黑 { // g // u p // c if (cur == parent->_right) { parent->_col = BLACK; grandf->_col = RED; RotateL(grandf); } // g // u p // c else { RotateR(parent); grandf->_col = RED; cur->_col = BLACK; RotateL(grandf); } break; } } } _root->_col = BLACK; return true; }
看过我AVL树或则二叉搜索树文章的同学肯定对前半部分代码非常熟悉了,就是二叉搜索树的方式进行插入,接下来我们直接分析红黑树的调整部分的代码。
我们可以看到我们循环的条件是父亲节点不为空且是红的就要一直进行调整,根据我们所画的图,我们需要一个祖父节点,我们发现接下来是有一套if...else,这是判断父亲节点是祖父的左孩子还是右孩子,方便我们确定叔叔节点的位置,我们所画的图只是if的情况,else的情况我没有画的原因是只要你把if的逻辑弄懂了,那么else就是反一下,没有什么新的东西,我等会稍微讲一讲就清楚了,我们重新回到if里,这种情况,叔叔节点就是祖父的右孩子,按照情况一,叔叔存在且为红,那么我们只需要进行变色操作就可以了,如果叔叔不存在,或则存在且为黑,那么我们就需要进行旋转操作,按照情况二的图里,如果cur是在父亲的左子树,那么直接右旋就可以了,不要忘了父亲节点和祖父节点要变色,如果cur是在父亲的右子树方向,那么我们就要进行左旋,变色,再右旋就可以了,跟我们画的图的思路是一模一样的。有了这个思路再看我们else的代码就会发现除了把旋转方向对换一下,指针方向稍微改改,其他可以说是一模一样。
旋转代码:
//右旋 void RotateR(Node* parent) { Node* SubL = parent->_left; Node* SubLR = SubL->_right; parent->_left = SubLR; if (SubLR) { SubLR->_parent = parent; } SubL->_right = parent; Node* ppnode = parent->_parent; parent->_parent = SubL; if (parent == _root) { _root = SubL; _root->_parent = nullptr; } else { if (ppnode->_left == parent) { ppnode->_left = SubL; } else if (ppnode->_right == parent) { ppnode->_right = SubL; } SubL->_parent = ppnode; } }
//左旋 void RotateL(Node* parent) { Node* SubR = parent->_right; Node* SubRL = SubR->_left; parent->_right = SubRL; if (SubRL) { SubRL->_parent = parent; } SubR->_left = parent; Node* ppnode = parent->_parent; parent->_parent = SubR; if (parent == _root) { _root = SubR; _root->_parent = nullptr; } else { if (ppnode->_left == parent) { ppnode->_left = SubR; } else if (ppnode->_right == parent) { ppnode->_right = SubR; } SubR->_parent = ppnode; } }
右旋和左旋的代码讲解大家可以去看我的C++实现——AVL树那篇文章,这里的左旋右旋代码跟那里的是一模一样的,只不过少了行平衡因子的更新而已,因为红黑树不用平衡因子。
1.5红黑树的验证
红黑树的检测分为两步:
1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
2. 检测其是否满足红黑树的性质
中序遍历代码:
//中序遍历 void InOrder() { _InOrder(_root); } void _InOrder(Node* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_kv.first<< ": " << root->_kv.second << endl; _InOrder(root->_right); }
中序遍历就不用我多说什么了吧,就是按照左根右的方式进行递归就可以了。
判断是否是红黑树:
//判断是否平衡 bool IsBalance() { if (_root->_col == RED) { return false; } int refNum = 0; Node* cur = _root; while (cur) { if (cur->_col == BLACK) { refNum++; } cur = cur->_left; } return Check(_root, 0, refNum); } bool Check(Node* root, int blacknum,const int refnum) { if (root == nullptr) { if (refnum != blacknum) { cout << "存在黑色节点不相等的路径" << endl; return false; } return true; } if (root->_col == RED && root->_parent->_col == RED) { cout << root->_kv.first << ": " << "存在红色连续" << endl; return false; } if (root->_col == BLACK) { blacknum++; } return Check(root->_left, blacknum, refnum) && Check(root->_right, blacknum, refnum); }
判断是否是红黑树我们要按照它的5条性质去设计。
首先,判断根节点是不是黑的,不是直接返回false,接下来我们再来看其它性质是否满足,我们先计算出一条路径,然后交给check函数去做。
当一条路走完后,我们看一下黑色是不是一样多,一样多我们就返回true,否则返回false。我们还要看是否有连续的红色存在,有就返回false。
最后就是以递归的方式遍历每条路径,遇到黑色节点记录上就好了。
1.6红黑树的删除
红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》
推荐博客:http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
1.7红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
1.8红黑树的应用
1. C++ STL库 -- map/set、mutil_map/mutil_set
2. Java 库
3. linux内核
4. 其他一些库