引言

最近打算补充下算法方面的知识,开一个《算法学习》专栏。

熟悉OpenWRT Linux系统的朋友都知道,OpenWRT里有一个基础库叫libubox,libubox是一些常用的C库的集合。 最常用的就是双向链表库”list.h”,和内核的链表头文件<linux/list.h>类似。此外还有md5、base64等常用工具库。 libubox中也有AVL库,本文基于libubox的AVL库代码,学习下AVL树的结构和相关操作。 当然不熟悉OpenWRT或libubox库完全不影响阅读本文。

什么是AVL树

什么是AVL树呢?AVL树是平衡二叉排序树,是以它的发现者Adelson-Velsky和Landis的名字命名的。

首先什么是二叉树:二叉树的每个节点至多只有两棵子树,并且子树有左右之分。

再看平衡二叉树:平衡二叉树的左子树和右子树都是平衡二叉树,且左右子树的深度之差的绝对值不超过1。(树的深度即从根节点到叶子节点的距离或步数)

再看二叉排序树:它的左子树如果不为空,则左子树上的所有节点的值均小于它的根节点的值;它的右子树如果不为空, 则右子树上所有节点的值均大于它根节点的值;它的左右子树也分别是二叉排序数。

二叉排序树又可以称为二叉查找树,所以平衡二叉排序树的英文全称可以是Balanced Binary Search Tree,简称BBST。

平衡二叉排序树通常用在动态数据表查找上。动态数据表的意思是要查找的数据表是动态生成的,不是一个静态的数据表。 这是因为对一个平衡二叉排序树的查找操作,类似于静态排序表的折半查找,效率是很高的,其时间复杂度为O(log n)。 这一点大家手画一个二叉排序树比划一下,就能感受出来。(严格数学证明不在本文范畴内^_^)

AVL树最大的特性是自平衡性。这里自平衡的意思是,当AVL树因为插入一个节点而失去平衡时, 只需对节点进行有限且有规律的被称为“左旋”或“右旋”的操作,就可以使树再次平衡。

AVL库代码学习

代码基于libubox的AVL库:libubox/avl.h, libubox/avl.c

AVL树对象定义

先看下AVL树对象的定义:

//AVL节点对象
struct avl_node {
    struct list_head list; //libubox的AVL库将所有节点串成一个双向链表
    struct avl_node *parent; //指向父节点
    struct avl_node *left; //左子节点
    struct avl_node *right; //右子节点
    const void *key; //节点存储的值
    signed char balance; //平衡因子: 取值0 -1 +1
                         //-1表示左子树深度大1,+1表示右子树深度大1,0表示一样大
    bool leader;
};

//AVL树对象
typedef int (*avl_tree_comp) (const void *k1, const void *k2, void *ptr);
struct avl_tree {
    struct list_head list_head; //libubox的AVL库将所有节点串成一个有序双向链表,方便遍历
    struct avl_node *root; //树的根节点
    unsigned int count; //树中节点的个数
    bool allow_dups; //树中是否允许key相同的节点存在,libubox的AVL库支持插入相同的节点
    avl_tree_comp comp; //节点大小比较函数
    void *cmp_ptr; //比较函数的第三个入参
};

接下来看AVL树的3个主要方法:查找、插入、删除。

查找节点 avl_find()

查找节点:avl_find()

//在一棵AVL树中,查找值为key的节点,找到则返回找到的节点,找不到则返回空
struct avl_node *avl_find(const struct avl_tree *tree, const void *key)
    struct avl_node *node;
    int diff;
    if (tree->root == NULL)
        return NULL; //树为空则直接返回空
    //递归查找节点。入参是节点、key值、比较函数,出参是比较值
    node = avl_find_rec(tree->root, key, tree->comp, tree->cmp_ptr, &diff)
        int diff = (*comp) (key, node->key, cmp_ptr); //计算传入的key和节点的key的差值
        if (diff < 0) //如果差值小于0,即匹配的key值应该在节点的左子树,继续去左子树寻找
            if (node->left != NULL)
                return avl_find_rec(node->right, key, comp, cmp_ptr, cmp_result);
            else
                return node //如果没有左子树了,则返回这个叶节点
        if (diff > 0) //如果差值大于0,即匹配的key值应该在节点的右子树,继续去右子树寻找
            if (node->right != NULL)
                return avl_find_rec(node->right, key, comp, cmp_ptr, cmp_result)
            else
                return node //如果没有右子树了,则返回这个叶节点
        return node //如果差值等于0,则返回这个匹配的节点

    return diff == 0 ? node : NULL //如果差值不等于0,表示没找到匹配的节点,返回空

插入节点 avl_insert()

插入节点:avl_insert()。本节包含AVL树的左旋或右旋操作,是AVL树的关键特性所在。

int avl_insert(struct avl_tree *tree, struct avl_node *new)
    //对要插入new节点除了key属性以外的其它属性赋初值
    new->parent = NULL;
    new->left = NULL;
    new->right = NULL;
    new->balance = 0;
    new->leader = true;

    if (tree->root == NULL)
        //如果这个树没有节点存在
        list_add(&new->list, &tree->list_head) //new节点直接插入树中
        tree->root = new //树的根节点指向这个new节点
        tree->count = 1 //此时树的节点个数变为1
        return 0

    //递归查找与new节点相同或相近的节点node
    node = avl_find_rec(tree->root, new->key, tree->comp, tree->cmp_ptr, &diff);
    last = node;

    //遍历last的next节点,即如果有key值相同的节点,取所有相同key值节点的最后一个
    while (!list_is_last(&last->list, &tree->list_head)) {
        next = avl_next(last);
        if (next->leader)
          break;
        last = next;

    diff = (*tree->comp) (new->key, node->key, tree->cmp_ptr); //再次计算差值,这一步是多余的?
    if (diff == 0)
        if (!tree->allow_dups)
          return -1; //不允许插入相同key值节点时,直接返回-1
        new->leader = 0; //leader置0表示已存在相同key值的节点
        avl_insert_after(tree, last, new); //将此节点插入双向链表
        return 0;

    if (node->balance == 1)
        //节点的平衡度为1,表示右子树深度大。此时直接将new节点插入node节点左侧即可
        avl_insert_before(tree, node, new);
        node->balance = 0; //插入new节点后node节点左右深度相同
        new->parent = node;
        node->left = new; //将new节点插入到node节点左侧
        return 0;

    if (node->balance == -1)
        //节点的平衡度为-1,表示左子树深度大。此时直接将new节点插入node节点右侧即可
        avl_insert_after(tree, last, new);
        node->balance = 0; //插入new节点后node节点左右深度相同
        new->parent = node;
        node->right = new; //将new节点插入到node节点的右侧
        return 0;

    if (diff < 0) { //new节点小于node节点,插入node节点左侧
        avl_insert_before(tree, node, new);
        node->balance = -1; //左子树深度变大
        new->parent = node;
        node->left = new; //将new节点插入到node节点左侧
        post_insert(tree, node); //插入后处理,后面专门看这个函数
        return 0;

    //new节点大于node节点,插入node节点右侧
    avl_insert_after(tree, last, new);
    node->balance = 1;
    new->parent = node;
    node->right = new;
    post_insert(tree, node); //插入后处理

/*
 总结下插入逻辑:
 在没有相同节点的情况下,插入分两大类。
 大类一:node节点平衡因子不为0,插入new后node节点的平衡因子变为0,不需要后处理
 大类二:node节点平衡因子为0,插入new后node节点平衡因子不为0,需要后处理
 */

//接下来看下后处理函数,这里是AVL树最具特点的地方,
//即插入一个新节点后,如果二叉树不再平衡,只需要特定的左旋右旋操作,二叉树就可再次平衡
static void post_insert(struct avl_tree *tree, struct avl_node *node)
    struct avl_node *parent = node->parent; //得到node节点的父节点
    if (parent == NULL)
        return //如果父节点为空,说明node节点就是整个AVL树的根节点,此时什么都不用做

    if (node == parent->left)
        //如果node节点在左侧,即插入的new节点在父节点的左子树
        parent->balance--; //父节点左侧深度增加
        if (parent->balance == 0)
            return; //如果父节点左右深度相同,则什么都不用做
        if (parent->balance == -1)
            //如果左子树深度大1,递归调用post_insert,也就是修改所有父节点的balance为-1
            post_insert(tree, parent);
            return;

        //函数走到此,说明parent->balance=-2,父节点不再平衡,需要旋转操作
        if (node->balance == -1)
            //如果node节点左子树深度大1,则进行右旋操作,后面单独看旋转操作函数
            avl_rotate_right(tree, parent);
            return;
        //如果node节点右子树大1,则先左旋,再右旋
        avl_rotate_left(tree, node);
        avl_rotate_right(tree, node->parent->parent);
        return;

    //以下是node节点在parent节点右侧的情况,与上面的代码对称
    parent->balance++; //父节点的右侧深度增加
    if (parent->balance == 0)
        return; //如果父节点左右深度相同,则什么都不用做
    if (parent->balance == 1)
        //如果左子树深度大1,递归调用post_insert,也就是修改所有父节点的balance为1
        post_insert(tree, parent);
        return;

    //函数走到此,说明parent->balance=2,父节点不再平衡,需要旋转操作
    if (node->balance == 1)
        //如果node节点右子树深度大1,则进行左旋操作
        avl_rotate_left(tree, parent);
        return;
    //如果node节点左子树大1,则先右旋,再左旋
    avl_rotate_right(tree, node);
    avl_rotate_left(tree, node->parent->parent);

//接下来看一下旋转操作的代码
//左旋
static void avl_rotate_left(struct avl_tree *tree, struct avl_node *node)
    struct avl_node *right, *parent;
    right = node->right; //记录node节点的右节点和父节点
    parent = node->parent;

    right->parent = parent; //右节点和node节点互换位置
    node->parent = right;
    if (parent == NULL) //如果父节点是空,则修改树的根节点
        tree->root = right;
    else
        //将父节点的子节点node修改为right
        if (parent->left == node)
            parent->left = right;
        else
            parent->right = right;
    node->right = right->left; //right的左节点赋给node右节点
    right->left = node; //node置为right节点的左节点
    if (node->right != NULL)
        node->right->parent = node; //如果node的右节点不为空,改一下其父节点
    node->balance -= 1 + avl_max(right->balance, 0); //修正平衡因子,至少减1,即右子树深度至少减1
    right->balance -= 1 - avl_min(node->balance, 0);

//右旋。与左旋相反
static void avl_rotate_right(struct avl_tree *tree, struct avl_node *node)
    struct avl_node *left, *parent;
    left = node->left;
    parent = node->parent;

    left->parent = parent; //左节点和node节点互换
    node->parent = left;
    if (parent == NULL)
        tree->root = left;
    else
        if (parent->left == node)
            parent->left = left;
        else
            parent->right = left;
    node->left = left->right;
    left->right = node;
    if (node->left != NULL)
        node->left->parent = node;
    node->balance += 1 - avl_min(left->balance, 0);
    left->balance += 1 + avl_max(node->balance, 0);

删除节点 avl_delete()

删除节点avl_delete()

//删除节点逻辑也比较长,这里只介绍下梗概
void avl_delete(struct avl_tree *tree, struct avl_node *node)
    //如果node是一个重复节点,则直接删除,不涉及二叉树结构的变动

    //真正从二叉树删除一个节点
    avl_delete_worker(tree, node)
        //node是叶子节点时:
        if (node->left == NULL && node->right == NULL)
            //根据node父节点的平衡因子,进行后处理和旋转操作
        //node的左节点为空时:
        if (node->left == NULL)
            //将node的右节点代替node节点,并调用avl_post_delete
        //node的右节点为空时:
        if (node->right == NULL)
            //将node的左节点代替node节点,并调用avl_post_delete
        //node的左右节点都不为空时
        min = avl_local_min(node->right); //取node右子树的最小值节点min,此节点要么是叶子节点要么左节点为空
        avl_delete_worker(tree, min); //嵌套调用avl_delete_worker,删除这个min节点
        //用min节点代替node节点,从而删除了node节点

遍历节点

libubox的AVL库已经把所有的节点串成了一个有序的双向链表,所以对AVL树的遍历就变成了对双向链表的遍历。 AVL库对外提供了很多遍历宏,而这些宏的最终调用的是 avl_for_element_range avl_for_element_range_safe avl_for_element_range_reverse avl_for_element_range_reverse_safe

我们来看一下avl_for_element_range

//这里入参first和last是指向包含avl_node结构的大结构体的指针,即遍历的起始大结构体和结束大结构体
//element是指向每一个遍历到的大结构体,node_member是avl_node结构体在大结构体中的成员名
#define avl_for_element_range(first, last, element, node_member) \
  for (element = (first); \
       element->node_member.list.prev != &(last)->node_member.list; \
       element = avl_next_element(element, node_member))

//avl_next_element,即取avl_node.list->next,然后利用container_of取到大结构体
#define avl_next_element(element, node_member) \
  container_of((&(element)->node_member.list)->next, typeof(*(element)), node_member.list)

再看下全节点遍历avl_for_each_element

//全节点遍历即调用avl_for_element_range,然后入参是AVL树的第一个节点和最后一个节点
//即从树的第一个节点 遍历到树的最后一个节点
#define avl_for_each_element(tree, element, node_member) \
  avl_for_element_range(avl_first_element(tree, element, node_member), \
                        avl_last_element(tree, element,  node_member), \
                        element, node_member)

AVL库实际使用举例

如文章开头提到的AVL树非常适合用在动态数据表查找上,查找的时间复杂度是O(log n)。 我们以有1000个数据的数据表举例,如果这个数据表用线性链表存储,查找一个数据最大可能搜索1000次; 如果用AVL树存储,最大只需要搜索10次。

OpenWRT中有一个重要的进程间通信组件ubus,就是用的AVL树维护ubus总线上各对象的信息。

这样在ubus总线上,即使有很多进程注册了很多ubus对象,ubusd内部可以根据目标对象ID或名称字符串, 在AVL树上很快搜索到目标对象。

//ubus/ubusd_proto.c
static int ubusd_handle_lookup()
	char *objpath;
	objpath = blob_data(attr[UBUS_ATTR_OBJPATH]);
	...
	obj = avl_find_element(&path, objpath, obj, path) //AVL树查找
	...