用户空间使用Linux数据结构系列之红黑树
红黑树由来:
他是在1972年 由Rudolf Bayer发明的,他称之为“对称二叉B树”,它现代的名字是Leo J. Guibas和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
red-black tree(RB树)
是一种平衡二叉树,它主要用于存储或者说索引可排序的键值对数据。RB树(红黑树)与radix树和hash表都不同。radix树是一种比较适合用于存储稀疏的数据集而且将用一个大整数进行插入,删除,查找的操作基础。而hash表并不是以某种排序顺序进行存储,而且必须指定大小和hash函数。RB树与AVL树很相似,但是比AVL树有更好的插入和删除最坏情况的时间复杂度,以及O(log n)的最坏查找时间复杂度。
RB树定义
1.每个结点要么是红色要么是黑色;
2.根结点必须是黑色;
3.每个叶节点,即空节点(NULL)是黑色的。
4.红结点如果有孩子,其孩子必须都是黑色(红节点周边3个节点都是黑色);
5.从根结点到叶子的每条路径必须包含相同数目的黑结点。
红黑树是满足一定特征的二叉树,其本质还是二叉树。红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。既然是二叉查找树,那么它必满足二叉查找树的一般 性质。下面,在具体介绍红黑树之前,先来了解下 二叉查找树的一般性质:
1.在一棵二叉查找树上,执行查找、插入、删除等操作的时间复杂度为O(log n)。因为,一棵由n个结点,随机构造的二叉查找树的高度为logn,所以顺理成章,一般操作的执行时间为O(log n)。至于n个结点的二叉树高度为O(log n)的证明,可参考算法导论 第12章 二叉查找树 第12.4节。
2.但若是一棵具有n个结点的线性链,则此些操作最坏情况运行时间为O(n)。而红黑树,能保证在最坏情况下,基本的动态几何操作的时间均为 O(log n)。
引用:
在Linux中有很多地方用到了RD树。anticipatory, deadline, 和CFQ I/O调度都使用的是RB树进行请求跟踪,还有CD/DVD驱动的包管理也是如此。高精度计时器(high-resolutiontimer)使用RB树组织定时请求。
EXT3文件系统也使用RB树来管理目录。虚拟存储管理系统也是有RB树进行VMAs(Virtual Memory Areas)的管理。当然还有文件描述符,密码钥匙,“等级令牌桶”调度的网络数据包都是用RB数据进行组织和管理的。另外C++ STL中,关联式容器 set 和 map 的底层实现是基于 RB-tree。
相关资料:
Linux Weekly News article onred-black trees
http://lwn.net/Articles/184495/
Wikipedia entry on red-blacktrees
http://en.wikipedia.org/wiki/Red-black_tree
可见RB树(红黑树)在Linux内核中的重要性。
在Linux内核源代码中rb树的实现在
lib/rbtree.c
include/linux/rbtree.h//在使用rb的地方,可以通过#include "linux/rbtree.h"进行使用。
rbtree.txt //描述如何使用红黑树的说明文档
在Linux内核中的RB树实现与传统的实现方式有些不同。它对针对内核对速度的需要做了些优化。每一个rb_node节点是嵌入在用RB树进行组织的数据结构中,而不是用rb_node指针进行数据结构的组织。
如:
在RB树里的数据节点包含了一个rb_node数据结构节点。如下:
struct mytype { struct rb_node node; char *keystring; };
可以通过container_of宏取得包含了rb_node的数据结构。也可以通过
rb_entry(node,type,member)取得。
其实是#definerb_entry(node,type,member) container_of(node,type,member)
这里顺便说一下如何通过container_of(非常重要的一个宏)取得包含rb_node的数据结构指针:
在Linux内核代码里有这样的宏定义:
#define container_of(ptr, type, member) ({\ const typeof( ((type *)0)->member )*__mptr = (ptr);\ (type *)( (char *)__mptr -offsetof(type,member) );}) #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
那么对于struct mytype,如何通过其成员node来取得走首地址呢:
我们肯定已经知道node的地址,假设为pnode;
struct mytype* x = rb_entry(pnode, structmytype, node);
意思是,取得pnode所被包含的数据结构对象的首地址,pnode是指向struct mytype的
成员node的指针。
宏替换完后就是这个样子:
({ const typeof( ((struct mytype*)0)->node ) *__mptr = (pnode); (struct mytype *)((char*)__mptr-((size_t)&((struct mytype*)0)->node)); })
typeof取得struct mytype中node成员的类型,用这个类型定义一个新的指针,把
pnode的值赋给它,在这里起到了类型检查的作用。
offsetof(type,member)是取得member在type对象中相对于此对象首地址的偏移量。
那么用__mptr减去这个偏移量就得到了type对象的起始地址。也就得到了把rb_node
作为嵌入的管理数据结构的对象起始地址。
x = y - offset ---------- rb_node; while (node) { struct mytype *data =container_of(node, struct mytype, node); int result; result = strcmp(string,data->keystring); if (result rb_left; else if (result > 0) node = node->rb_right; else return data; } return NULL; }
向RB树中插入一个数据
在插入一个数据之前先要查找到适合插入的位置,然后将节点加入到树中并将树调整
到平衡状态。
int my_insert(struct rb_root *root, structmytype *data) { struct rb_node **new =&(root->rb_node), *parent = NULL; /* Figure out where to put new node */ while (*new) { struct mytype *this =container_of(*new, struct mytype, node); int result =strcmp(data->keystring, this->keystring); parent = *new; if (result rb_left); else if (result > 0) new =&((*new)->rb_right); else return FALSE; } /* Add new node and rebalance tree. */ rb_link_node(data->node, parent,new); rb_insert_color(data->node, root); return TRUE; }
删除一个数据节点
struct mytype *data = mysearch(mytree,"walrus"); if (data) { rb_erase(data->node, mytree); myfree(data); }
可以通过调用
void rb_replace_node(struct rb_node *old,struct rb_node *new,
struct rb_root *tree);
来替换一个节点,但是替换完成后并不会对RB树做任何调整,所以如果新节点的值与
被替换的值有所不同时,可能会出现问题。
下面将对rb-tree的主要数据结构进行分析(源码目录include/linux/rbtree.h)
先到include/linux/rbtree.h中看一下红黑树的一些定义,如下:
//节点描述
Struct rb_node { unsigned long rb_parent_color; //保存父节点的指针值(父节点的地址)同时保存节点的color #defineRB_RED 0 #defineRB_BLACK 1 structrb_node *rb_right; //节点右孩子 structrb_node *rb_left; //节点左孩子 }__attribute__((aligned(sizeof(long))));
//根节点描述
struct rb_root { struct rb_node *rb_node; };
对于rb_root只是rb_node*的一个封装 。
再来看看rb_node结构体,被一个”__attribute__((aligned(sizeof(long))))”所包装(非常重要,技巧就在这!),
”__attribute__((aligned(sizeof(long))))“的意思是把结构体的地址按“sizeof(long)”对齐,
对于32位机,sizeof(long)为4 (即结构体内的数据类型的地址为4的倍数)。对于64位机,sizeof(long)为8(结构体内的数据类型的地址为8的倍数).这个地方需要补一下字节对齐的知识:
|
所以以4(或8)为倍数的地址以二进制表示的特点是:以4为倍数(字节对齐)的地址(32位机器)最后两位肯定为零(看好了是存储变量的地址,而不是变量),对于64位机器是后三位肯定为零。
对于rb-tree 中每一个节点,都需要标记一个颜色(只有两种选择:红 vs 黑),而这里的技巧就在“__attribute__((aligned(sizeof(long))))”,因为红黑树的每个节点都用rb_node结构来表示,利用字节对齐技巧,任何rb_node结构体的地址的低两位肯定都是零,与其空着不用,还不如用它们表示颜 色,反正颜色就两种,其实一位就已经够了。unsigned long rb_parent_color变量有两个作用(见名知义):
1.存储父节点的地址(注意IA32父节点的地址为4的倍数,地址后面的2位没有用上 ,IA64则父节点的地址为8的倍数,地址后面的3位没有用上)。
2.用后2位,标识此节点的color(:红 vs 黑 )
include/linux/rbtree.h中有几个与rb_parent_color相关的宏 #definerb_parent(r) ((struct rb_node*)((r)->rb_parent_color & ~3)) //取 r节点的父节点的地址,即把r节点的后两位清零后与r节点存储的父节点进行与操作。 //假设r->rb_parent_color 的内容为 0x20000001(表示父节点的地址为0x20000000,节点颜色为黑),那么执行//此操作后,取得父节点的地址为 0x20000000 #definerb_color(r) ((r)->rb_parent_color& 1) // 设置r节点的颜色为黑 ,只要看最后一位即可. #definerb_is_red(r) (!rb_color(r)) // 测试r节点是否为红 #definerb_is_black(r) rb_color(r) //测试r节点是否为黑 #definerb_set_red(r) do {(r)->rb_parent_color &= ~1; } while (0) //设置r节点是为红 #definerb_set_black(r) do {(r)->rb_parent_color |= 1; } while (0) //设置r节点是为黑 //内联函数,设置节点的父节点 static inline voidrb_set_parent(struct rb_node *rb, struct rb_node *p) { rb->rb_parent_color =(rb->rb_parent_color & 3) | (unsigned long)p; } //内联函数,设置节点的color static inline voidrb_set_color(struct rb_node *rb, int color) { rb->rb_parent_color =(rb->rb_parent_color & ~1) | color; }