目录
1.概念
2.性质
3.节点定义
4.结构
5.插入
6.验证
7.删除
8.红黑树和avl树比较
9.应用
概念
是一种二叉搜索树,但在每个节点上增加一个存储位表示节点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长处两倍,因此是接近平衡的
性质
- 每个节点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点时红色的,则它的两个孩子结点是黑色的(不能有连续红色)
- 对于每个结点,从该结点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
- 每个叶子节点都是黑色的(此处的叶子结点指的是空结点)
第5条指的是空节点,方便数出二叉树的路径,上面的红黑树共有11条路径,每条路径都有2个黑色节点
最短路径:全黑
最长路径:一黑一红间隔
假设共有N个黑色节点,最短路径logN,最长路径2logN
假设每条路径都有N个黑色节点,每条路径的结点数量[N,2*N]之间
avl树是绝对平衡,红黑树是相对平横,最长路径不超过最短路径的2倍
节点定义
枚举一个颜色,红和黑,节点中加入颜色
enum color
{RED,BLACK
};template <class K, class V>
struct TreeNode
{struct TreeNode<K, V>* _parent;struct TreeNode<K, V>* _left;struct TreeNode<K, V>* _right;std::pair<K, V> _kv;color _col;TreeNode(std::pair<K, V> kv):_parent(nullptr), _left(nullptr), _right(nullptr), _kv(kv), _col(RED){}
};
为什么节点默认是红色?
因为每条路径的黑色节点数量都必须相等,如果插入结点默认为黑色,就会对所有路径造成影响。如果是红色,最多只会影响自己所在的路径
红黑树结构
为了后续实现关联式容器简单,红黑树的实现中可以增加一个头结点,因为根节点必须为黑色,为了与根节点区分,将头结点给成黑色,并且让头结点的pParent域指向红黑树的根节点,pLeft域指向红黑树中最小的结点,_pRight域指向红黑树最大的结点,如下,本节的实现不采用头结点
插入
红黑树是在二叉搜索树的基础上加入其平衡限制条件,因此红黑树的插入可分为两步:
插入结点
#pragma once
#include <iostream>
#include <assert.h>
#include <queue>enum color
{RED,BLACK
};template <class K, class V>
struct TreeNode
{struct TreeNode<K, V>* _parent;struct TreeNode<K, V>* _left;struct TreeNode<K, V>* _right;std::pair<K, V> _kv;color _col;TreeNode(std::pair<K, V> kv):_parent(nullptr), _left(nullptr), _right(nullptr), _kv(kv), _col(RED){}
};template <class K, class V>
class RBTree
{typedef TreeNode<K, V> node;
public:bool insert(const std::pair<K, V>& kv){if (_root == nullptr){_root = new node(kv);_root->_col = BLACK;return true;}node* parent = nullptr;node* cur = _root;while (cur){parent = cur;if (kv.first < cur->_kv.first){cur = cur->_left;}else if (kv.first > cur->_kv.first){cur = cur->_right;}else{return false;}}//插入cur = new node(kv);cur->_parent = parent;if (kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;} }private:node* _root = nullptr;
};
检测新节点插入后,红黑树的性质是否遭到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲颜色是红色时,就违反了性质三不能有连在一起的红色节点,此时分情况讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
1.当a/b/c/d/e都是空,cur就是新增
2.c/d/e是每条路径1个黑色节点的红黑树,那么就是x/y/z/r中任意一种
cur更改,变为黑色节点,左右各连a和b节点,cur就是在a和b插入,就会失衡
cde共有444种可能,插入位置有4个,就是64*4=256种可能
3.当每条路径变为2个黑色节点的子树
第一个是C88,C87…C80,再加上下面两个,第一个2个红色节点是C42,可以换一边,就是C42*2,第二个是C44,如果这些是100种可能,c,d,e就有1003种可能,其他更不用算了
从上面所有肯能找出一种规律,解决插入的相关问题
如果插入结点的双亲是黑色,不需要处理,是红色时才需要下面内容
- 情况一:cur为红,p为红,g为黑,u存在且为红
- 注意:此处看到的树,可能是一颗完整的树,也可能是一颗子树
上面的右边是左边变色而来,p和u变黑,g变红,就平衡了
如果g是根节点,调整完成后,需要改为黑色
如果g是子树的根,调整完成后,g的双亲如果是黑色,就要继续往上调整
解决方式,将p和u改为黑,g改为红,然后把g当做cur,继续向上调整
- 情况二,cur为红,p为红,g为黑,u不存在/存在且为黑
u的情况有两种:
1.如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4
2.如果u节点存在,则一定是黑色的,那么cur原来的颜色也是黑色,现在看到的红色是因为cur的子树在调整的过程中将cur节点的颜色由黑色变为红色
p为g的左孩子,cur为p的左孩子,则进行右单旋转,相反
p为g的右孩子,cur为p的右孩子,则进行左单旋转
- 情况三,cur为红,p为红,g为黑,u不存在/存在且为黑
和情况二类似
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转,旋转后变成了情况二,再右单旋转
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转,旋转后变成了情况二,再左单旋转
调整
//父节点是红色调整
while (parent && parent->_col == RED)
{node* gdparent = parent->_parent;node* uncle;if (gdparent->_left == parent){uncle = gdparent->_right;//第一种情况 叔叔节点是红色,变色if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;gdparent->_col = RED;}//第二种情况 分左右// g// par un// curelse{if (cur == parent->_left){RotateRight(gdparent);parent->_col = BLACK;gdparent->_col = RED;}// g// par un// curelse{RotateLeft(parent);RotateRight(gdparent);cur->_col = BLACK;parent->_col = RED;gdparent->_col = RED;}break;}}else{uncle = gdparent->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;gdparent->_col = RED;}else{if (cur == parent->_right){RotateLeft(gdparent);parent->_col = BLACK;gdparent->_col = RED;}// g// par un// curelse{RotateRight(parent);RotateLeft(gdparent);cur->_col = BLACK;//parent->_col = RED;gdparent->_col = RED;}break;}}cur = gdparent;parent = cur->_parent;
}//根必须是黑色
_root->_col = BLACK;
return true;
根节点在最后必须变为黑色,在函数最后部分统一改变
首先判断par在g的左边的三种情况,叔叔节点存在且是红色直接变色,更新节点向上。叔叔节点不存在或为黑色,判断cur在par的哪边,采取不同的旋转,然后改变节点颜色
par在g右边是相反
插入过程:
红黑树的验证
红黑树的验证分为两步:
1.检测是否满足二叉搜索树的(中序遍历是否有序序列)
2.检测是否满足红黑树的性质
先判断根节点是否为黑色,再记录一条路径黑色节点数量用来参考,递归红黑树,遇见黑色节点,黑色节点数量++。如果是红色节点,判断它的父节点是否为红色。遇到空,判断这条路径黑色节点数量和参考值是否一样
bool IsBalance()
{if (_root->_col == RED){std::cout << "根节点是红色" << std::endl;return false;}int refVal = 0; //记录最左路径黑色节点的数量,用来参考node* cur = _root;while (cur){if (cur->_col == BLACK){refVal++;}cur = cur->_left;}return _IsBalance(_root, 0, refVal);
}bool _IsBalance(node* cur, int blackNum, int refVal)
{if (cur == nullptr){//判断每条路径黑色节点数量正常if (blackNum != refVal){std::cout << "黑色节点数量不相等" << std::endl;return false;}return true;}if (cur->_col == BLACK){blackNum++;}if (cur->_col == RED && cur->_parent->_col == RED){std::cout << cur->_kv.first << " 连续红色节点" << std::endl;}return _IsBalance(cur->_left, blackNum, refVal) && _IsBalance(cur->_right, blackNum, refVal);
}
删除
参考《算法导论》或者《STL源码剖析》
https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
这里只提供思路
删除前部分内容和avl树都一样,如果删除的节点有两个孩子,就找前驱节点替换,这样所有情况都变成了删除节点只有一个孩子的情况,如果没有孩子也可以当做只有一个孩子处理
父亲节点称为par,删除节点del,孩子节点为child
- 第一种情况,删除的结点是红色,它如果有孩子一定是黑色,只需要将par链接到child
- 第二种情况,删除节点是黑色,child为红色,还是将par连接到child,将child改为黑色
- 第三种情况,删除节点是黑色,child也为黑色,这时考察del的兄弟节点,v有两种情况
情况1,v是黑色节点,v的左子女是w,根据w的颜色讨论:
下图g为祖父节点,u为del的child节点,v是del的兄弟节点,del节点在g的右孩子,删除后g连接到u
当删除了del时,g的右边ul和ur少了一个黑色节点,g的左边的每条路径wl,wr,vr都有相等的黑色节点
根据节点w的状态,有以下情况:
设平衡路径有2个黑色节点
1.w是红色节点,设wl,wr,vr都有2个黑色节点,wl,wr,vr中各有一个黑色节点,ul和ur无黑色节点。以g为中心右旋,vr接到g的左边,v变为红色,w和g改为黑色。此时,wl和wr里面各有1个黑色,w也是黑色,还是2个。vr中有1个黑色,加上g也是2个,u是黑色,g是黑色,ul和ur路径也是两个黑色节点
2.w是黑色节点,这时看w的有兄弟r节点,根据r分情况
① r是红色节点,通过一次先左后右的双旋,将g染成黑色
wl和wr无黑色节点,rl和rr各有1个黑色。旋转完后,右边增加一个黑色节点
② r是黑色节点,r是黑色节点,这时就要看g的颜色,如果g是红色,只要交换g和子女v的颜色。如果g是黑色,可以做一次右单旋转,将节点v上升染成双重黑色,消除节点u的双重黑色,双重黑色向根的方向移动
情况2,v是红色节点,考察有子女r,r一定是黑色节点,在看r的左子女s,根绝s的颜色分两种情况:
(1)s是红色,先左后右双旋,s染黑色,r上升,使包含u的路径的黑高度加1
(2) s是黑色,再看s的右兄弟节点t,根绝t的颜色分情况:
① 如果t为红色,先以t左旋,t替补r的位置,然后再以t先左后右的双旋,消除u的双重黑色
② 若节点t是黑色,以v为轴右单旋转,并改变v和r的颜色
节点u是g的左子女的情况是镜像的
红黑树和avl树的比较
都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2N log2N),红黑树不追求绝对平衡,只要保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以再经常增删的结构中更优,实际中红黑树用的更多
红黑树的应用
1.C++STL库,map/set、mutil_map/mutil_set
2.java库
3.linux内核
4.其他一些库