红黑树
红黑树
红黑树特点
红黑树和别的树的优势在于:
- 首先它是一个二叉树,这意味着二叉树的插入,查找和删除的平均复杂度在最快情况下都是
O(logN)
,因此在频繁插入删除和查找的情况下效率非常高。 - 那为什么不采用二叉搜索树呢,这是因为二叉搜索树在插入的时候可能会导致树变成链表(比如按顺序插入从小到大的结点,一直插入到右子树),这样查找的效率就变成
O(N)
了。 - 既然二叉搜索树太弱了,那为什么不用二叉平衡树呢,即AVL。这是因为二叉平衡树需要左右子树高度差<=1, 而红黑树允许左右子树高度差大于1,只是限定了路径上黑色结点数目相同,并且不存在连续的红色结点(最长路径为最短路径的两倍,最长路径是红黑交替的,最短路径为全黑的)。这样通过放宽平衡条件可以换取更少的旋转操作,更少的旋转操作意味着在频繁插入删除的场景下比AVL更高效。
总的来说,红黑树具备以下特点
- 它是一棵二叉搜索树
- 每一个节点都会被着色,不是黑色就是红色
- 根节点必须为黑色
- 对于一个红色节点,它的孩子或为空,或是黑色。也就是说路径上不能有连续红色节点
- 从根节点到NULL节点的所有路径上,黑色节点的数量都相同
Linux中红黑树的实现
OK有了以上直观的观念后,咱们来看看Linux中实现红黑树的源码。
首先是红黑树的定义(Linux 6.11版本), 在内核文件include/linux/rbtree_types.h
中,可以看到其实就是一个传统的数据结构,三个指针,分别指向父节点,两个左右子树,但是这个父节点这个值(__rb_parent_color
)做了一个优化,具体看注释:
1 | struct rb_node { |
有了数据结构定义之后,我们再来看看相关的一些宏定义用来操作这个数据结构的。主要代码在文件在include/linux/rbtree.h
中
1 | // include/linux/rbtree.h |
然后再来看看第二个比较重要的宏定义 contain_of
, 这是因为Linux的数据结构,例如链表和红黑树,都是嵌入到结构体里面的,因此指针指向的也是rb_node结构,并不是包含该rbnode的结构体(例如mm_struct),要提取真实的mm_struct结构体,则需要使用类似这种container_of宏。
1 | // include/linux/container_of.h |
假设有一个结构体
1 | struct person { |
并且假设我们现在有一个struct rb_node* node
指针,我们使用如下语句就能获取到该结构体:
1 | struct person *p = container_of(node, struct person, rb); |
此外还有一些比较常用的函数:
1 | // lib/rbtree.c |
增删改查
我们最为关心的增删改查。
在rb_tree中增加一个node
1 | // include/linux/rbtree.h |
我们先来看看rb_link_node函数。其实就是把link的值改成node。原本link是null。
1 | // include/linux/rbtree.h |
然后是rb_insert_color函数, 内核代码里都写了很好的注释。其中很关键的点都画图了,这里注释小写证明红色(n, p, g, u),大写证明黑色(N,P,G,U)
1 | // lib/rbtree.c |
这里看着代码比较复杂,其实就是利用递归来做旋转和变色,具体如下:
场景1:红黑树为空树
直接把插入结点作为根节点就可以了
另外:根据红黑树性质 2根节点是黑色的。还需要把插入节点设置为黑色
场景2:插入节点的Key已经存在
更新当前节点的值,为插入节点的值。
情景3:插入节点的父节点为黑色
由于插入的节点是红色的,当插入节点的父节点是黑色时,不会影响红黑树的平衡,
所以: 直接插入无需做自平衡。
情景4:插入节点的父节点为红色
根据性质2:根节点是黑色。
如果插入节点的父节点为红色节点,那么该父节点不可能为根节点,所以插入节点总是存在祖父节点(三代关系)。
根据性质4:每个红色节点的两个子节点一定是黑色的。不能有两个红色节点相连。
此时会出现两种状态:
- 父亲和叔叔为红色
- 父亲为红色,叔叔为黑色
场景4.1:父亲和叔叔为红色节点
根据性质4:红色节点不能相连 ==》祖父节点肯定为黑色节点:
父亲为红色,那么此时该插入子树的红黑树层数的情况是:黑红红。
因为不可能同时存在两个相连的红色节点,需要进行 变色, 显然处理方式是把其改为:红黑红
变色 处理:黑红红 ==> 红黑红
1.将F和V节点改为黑色
2.将P改为红色
3.将P设置为当前节点,进行后续处理
将P设置为红色了,
如果P的父节点是黑色,那么无需做处理;
但如果P的父节点是红色,则违反红黑树性质了,所以需要将P设置为当前节点,继续插入操作, 作自平衡处理,直到整体平衡为止。
场景4.2:叔叔为黑色,父亲为红色,并且插在父亲的左节点
分为两种情况,当前结点是左节点和当前结点是右结点
- 场景4.2.1 LL型失衡
把F设置成黑色,P设置成红色,然后右旋。
- 场景4.2.2 LR型失衡
把K和F进行左旋,转换成4.2.1
同理RR失衡和RL失衡则不再重复。