nginx 基数树

ngx_radix_tree_t基数树与ngx_rbtree_t红黑树一样都是二叉查找树,ngx_rbtree_t红黑树具备的优点,ngx_radix_tree_t基数树同样也有,但ngx_radix_tree_t基数树的应用范围要比ngx_rbtree_t红黑树小,因为ngx_radix_tree_t要求元素必须以整型数据作为关键字,所以大大减少了它的应用场景。然而,由于ngx_radix_tree_t基数树在插入、删除元素时不需要做旋转操作,因此它的插入、删除效率一般要比ngx_rbtree_t红黑树高。选择使用哪种二叉查找树取决于实际的应用场景。不过,ngx_radix_tree_t基数树的用法要比ngx_rbtree_t红黑树简单许多。
基数树也是一种二叉查找树,然而它却不像红黑树一样应用广泛(目前官方模块中仅geo模块使用了基数树)。这是因为ngx_radix_tree_t基数树要求存储的每个节点都必须以32位整型作为区别任意两个节点的唯一标识,而红黑树则没有此要求。ngx_radix_tree_t基数树与红黑树不同的另一个地方:ngx_radix_tree_t基数树会负责分配每个节点占用的内存。因此,每个基数树节点也不再像红黑树中那么灵活——可以是任意包含ngx_rbtree_node_t成员的结构体。基数树的每个节点中可以存储的值只是1个指针,它指向实际的数据。

本节将以一棵完整的ngx_radix_tree_t基数树来说明基数树的原理和用法,这棵树的深度为3,它包括以下4个节点:0X20000000、0X40000000、0X80000000、0Xc0000000。这里书写成十六进制是为了便于理解,因为基数树实际是按二进制位来建立树的,上面4个节点如果转换为十进制无符号整型(也就是7.6.3节例子中的ngx_uint_t),它们的值分别是536870912、1073741824、2147483648、2684354560;如果转换为二进制,它们的值分别为:00100000000000000000000000000000、01000000000000000000000000000000、10000000000000000000000000000000、11000000000000000000000000000000。在图7-9中,可以看到这4个节点如何存储到深度为3的基数树中。

ngx_radix_tree_t基数树的原理
基数树具备二叉查找树的所有优点:基本操作速度快(如检索、插入、删除节点)、支持范围查询、支持遍历操作等。但基数树不像红黑树那样会通过自身的旋转来达到平衡,基数树是不管树的形态是否平衡的,因此,它插入节点、删除节点的速度要比红黑树快得多!那么,基数树为什么可以不管树的形态是否平衡呢?
红黑树是通过不同节点间key关键字的比较来决定树的形态,而基数树则不然,它每一个节点的key关键字已经决定了这个节点处于树中的位置。决定节点位置的方法很简单,先将这个节点的整型关键字转化为二进制,从左向右数这32个位,遇到0时进入左子树,遇到1时进入右子树。因此,ngx_radix_tree_t树的最大深度是32。有时,数据可能仅在全部整型数范围的某一小段中,为了减少树的高度,ngx_radix_tree_t又加入了掩码的概念,掩码中为1的位节点关键字中有效的位数同时也决定了树的有效高度。例如,掩码为11100000000000000000000000000000(也就是0Xe0000000)时,表示树的高度为3。如果1个节点的关键字为0X0fffffff,那么实际上对于这棵基数树而言,它的节点关键字相当于0X00000000,因为掩码决定了仅前3位有效,并且它也只会放在树的第三层节点中。
如图7-9所示,0X20000000这个节点插到基数树后,由于掩码是0Xe0000000,因此它决定了所有的节点都将放在树的第三层。下面结合掩码看看节点是如何根据关键字来决定其在树中的位置的。掩码中有3个1,将节点的关键字0X20000000转化为二进制再取前3位为001,然后分3步决定节点的位置。

  • 首先找到根节点,取010的第1位0,表示选择左子树。
  • 第2位为0,表示再选择左子树。
  • 第3位为1,表示再选择右子树,此时的节点就是第三层的节点,这时会用它来存储0X20000000这个节点。

    《nginx 基数树》 3层基数树示意图

    ngx_radix_tree_t基数树的每个节点由ngx_radix_node_t结构体表示,代码如下所示

typedef struct ngx_radix_node_s ngx_radix_node_t;
struct ngx_radix_node_s {
  // 指向右子树,如果没有右子树,则值为null空指针
  ngx_radix_node_t *right;
  // 指向左子树,如果没有左子树,则值为null空指针
  ngx_radix_node_t *left;
  // 指向父节点,如果没有父节点,则(如根节点)值为null空指针
  ngx_radix_node_t parent;
  /* value存储的是指针的值,它指向用户定义的数据结构。如果这个节点还未使用,value的值将是NGX_RADIX_NO_VALUE */
  uintptr_t value;
};

如图7-9所示,value字段指向用户自定义的、有意义的数据结构。另外,基数树也不像红黑树一样还有哨兵节点。基数树节点的left和right都是有可能为null空指针的。
与红黑树不同的是,红黑树容器不负责分配每个树节点的内存,而ngx_radix_tree_t基数树则会分配ngx_radix_node_t结构体,这样使用ngx_radix_node_t基数树时就会更简单一些。但ngx_radix_node_t基数树是如何管理这些ngx_radix_node_t结构体的内存呢?下面来看一下ngx_radix_node_t容器的结构,代码如下。

typedef struct {
  // 指向根节点
  ngx_radix_node_t *root;
  // 内存池,它负责给基数树的节点分配内存
  ngx_pool_t pool;
  /* 管理已经分配但暂时未使用(不在树中)的节点,free实际上是所有不在树中节点的单链表 */
  ngx_radix_node_t free;
  // 已分配内存中还未使用内存的首地址
  char *start;
  // 已分配内存中还未使用的内存大小
  size_t size;
} ngx_radix_tree_t;

上面的pool对象用来分配内存。每次删除1个节点时,ngx_radix_tree_t基数树并不会释放这个节点占用的内存,而是把它添加到free单链表中。这样,在添加新的节点时,会首先查看free中是否还有节点,如果free中有未使用的节点,则会优先使用,如果没有,就会再从pool内存池中分配新内存存储节点。
对于ngx_radix_tree_t结构体来说,仅从使用的角度来看,我们不需要了解pool、free、start、size这些成员的意义,仅了解如何使用root根节点即可。

基数树的使用方法
相比于红黑树,ngx_radix_tree_t基数树的使用方法要简单许多,只需表7-7中列出的4个方法即可简单地操作基数树

《nginx 基数树》

《nginx 基数树》 ngx_radix_tree_t基数树提供的方法

使用基数树的例子
本节以图7-9中的基数树为例来构造radixTree这棵基数树。首先,使用
ngx_radix_tree_create方法创建基数树,代码如下。

ngx_radix_tree_t * radixTree = ngx_radix_tree_create(cf->pool, -1);

将预分配节点简单地设置为–1,这样pool内存池中就会只使用1个页面来尽可能地分配基数树节点。接下来,按照图7-9构造4个节点数据,这里将它们所使用的数据结构简单地用无符号整型表示,当然,实际使用时可以是任意的数据结构。

ngx_uint_t testRadixValue1 = 0x20000000;
ngx_uint_t testRadixValue2 = 0x40000000;
ngx_uint_t testRadixValue3 = 0x80000000;
ngx_uint_t testRadixValue4 = 0xa0000000;

接下来将上述节点添加到radixTree基数树中,注意,掩码是0xe0000000。

int rc;
rc = ngx_radix32tree_insert(radixTree,
0x20000000, 0xe0000000, (uintptr_t)&testRadixValue1);
rc = ngx_radix32tree_insert(radixTree,
0x40000000, 0xe0000000, (uintptr_t)&testRadixValue2);
rc = ngx_radix32tree_insert(radixTree,
0x80000000, 0xe0000000, (uintptr_t)&testRadixValue3);
rc = ngx_radix32tree_insert(radixTree,
0xa0000000, 0xe0000000, (uintptr_t)&testRadixValue4);

下面来试着调用ngx_radix32tree_find查询节点,代码如下。

ngx_uint_t* pRadixValue = (ngx_uint_t *) ngx_radix32tree_find( radixTree, 0x80000000);

注意,如果没有查询到,那么返回的pRadixValue将会是NGX_RADIX_NO_VALUE。
下面调用ngx_radix32tree_delete删除1个节点,代码如下。

rc = ngx_radix32tree_delete(radixTree, 0xa0000000, 0xe0000000);

本文暂取自代码、网络或书籍,只用作学习备忘,不作盈利等他用,如有侵权,请联系Linkerist@163.com

    原文作者:Linkerist
    原文地址: https://www.jianshu.com/p/383e4430076f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞