libevent源码学习(10):min_heap数据结构解析

min_heap类型定义

min_heap函数

构造/析构函数及初始化

判断event是否在堆顶

判断两个event之间超时结构体的大小关系

判断堆是否为空及堆大小

返回堆顶event

分配堆空间

堆元素的上浮

堆元素的下沉

堆插入元素

堆删除元素

弹出堆顶元素

以下源码均基于libevent-2.0.21-stable。

       在libevent中,使用min_heap这一数据结构来管理各个event的超时,也就是小顶堆,整个堆是根据各个event的超时时间来构成的,因此堆顶肯定就对应超时时间最小的event,这样就可以按照超时顺序进行处理了。

       关于小顶堆的性质,可以先参考堆排序中有关大顶堆的描述。
min_heap类型定义

       libevent中的min_heap是一个结构体类型,定义如下:

    typedef struct min_heap
    {
        struct event** p; //小顶堆的首地址
        unsigned n, a; //n为event *元素个数,a是event指针链表的长度(以event *p为单位)
    } min_heap_t;

       这里涉及到了3个成员变量,对于struct event ** p,这是一个二级指针,它指向一个struct  event *型的变量,而struct event *的话,就应该很熟悉了,它指向一个事件event。通过p这个二级指针也就可以实现小顶堆对应的数组了,原因在于:在C语言中,p[i]是等价于*(p+i)的,而p作为struct event的二级指针,其指向的元素类型为struct event *型,因此 p+i 实际上就是从首地址开始偏移到第i个struct event *型元素,即p+i在数值上就等于(int)p+i*sizeof(struct event *),即p+i为第i个struct event *型元素的地址,因此*(p+i)就是第i个struct event *型元素了,因此,对于struct event ** p来说,p[i]就表示第i个struct event *元素,偏移量为i*sizeof(struct event *)。这样看来struct event ** p实际上就相当于struct event *p[capacity]指针数组,之所以不使用指针数组而是使用二级指针,是因为C语言中不存在像C++中vector那样长度可变的动态容器,如果定义为数组必须指定数组大小,这是不符合要求的,因此直接使用二级指针,在需要添加元素的时候用malloc来分配一个所需大小的内存空间即可。

       而对于另外两个成员变量,我们暂且把p作为一个数组,其中每个元素都是strcut event *类型,那么n就表示这个p数组中当前的元素个数,而a则表示p数组最多能容纳的元素个数。(不得不吐槽一下这里变量的命名。。。)这就像C++中vector的size和capacity的区别,前者表示当前容器中的元素个数,后者表示当前容器最多能容纳的元素个数。

       在正式分析函数前,还需要知道的是,每个event中,都定义了一个min_heap_idx用来存储event在这个小顶堆p数组中的索引,虽然堆中的每个元素都是一个event指针,但是建堆的依据是这些event各自设置的超时结构体ev_timeout,这是定时器小顶堆实现依据。如下所示。

    struct event {
        ......
        union {
            ......
            int min_heap_idx;  //event在堆中的索引
        } ev_timeout_pos;
        ......
        struct timeval ev_timeout;  //超时时间
        ......
    };

min_heap函数
构造/析构函数及初始化

       虽然说C语言中没有构造函数和析构函数,但是min_heap也将这种思想进行了体现在了min_heap_ctor函数和min_heap_dtor函数上,从函数名上看就是constructor和destructor的简写,各自定义如下:

    void min_heap_ctor(min_heap_t* s) { s->p = 0; s->n = 0; s->a = 0; }//构造函数 初始化
    void min_heap_dtor(min_heap_t* s) { if (s->p) mm_free(s->p); } //析构函数 释放空间

       min_heap_elem_init函数用来初始化小顶堆中的event,将event的堆索引初始化为-1。其定义如下:

void min_heap_elem_init(struct event* e) { e->ev_timeout_pos.min_heap_idx = -1; }

判断event是否在堆顶

      min_heap_elt_is_top函数用于完成这一功能。显然,如果event的堆索引为0,那么这个event就在堆顶了。其定义如下:

    int min_heap_elt_is_top(const struct event *e)//判断event的超时是否在定时器堆顶
    {
        return e->ev_timeout_pos.min_heap_idx == 0;
    }

判断两个event之间超时结构体的大小关系

       min_heap_elem_greater函数传入两个event参数,用来判断第一个参数event的超时结构体是否大于第二参数的超时结构体,如果大于则返回1,否则返回0。比较两个超时结构体先比较秒数,再比较微妙数,函数中调用了宏函数,如下所示:

    #define    evutil_timercmp(tvp, uvp, cmp)                    
        (((tvp)->tv_sec == (uvp)->tv_sec) ?                
         ((tvp)->tv_usec cmp (uvp)->tv_usec) :                
         ((tvp)->tv_sec cmp (uvp)->tv_sec))
     
     
    int min_heap_elem_greater(struct event *a, struct event *b)
    {
        return evutil_timercmp(&a->ev_timeout, &b->ev_timeout, >);//先比较sec是否a大于b,如果a和b的sec相同,就比较usec;
    }

判断堆是否为空及堆大小

       前面说了min_heap中的成员变量n描述堆中实际存在的元素数目,因此直接判断n是否为0即可:

    int min_heap_empty(min_heap_t* s) { return 0u == s->n; }   //堆是否为空
    unsigned min_heap_size(min_heap_t* s) { return s->n; }   //堆大小

返回堆顶event

struct event* min_heap_top(min_heap_t* s) { return s->n ? *s->p : 0; }

分配堆空间

      min_heap是在插入新的event时,如果空间不足是可以自动扩容的,该函数需要传入n表明需要让堆装下n个元素。由函数min_heap_reserve实现如下:

    int min_heap_reserve(min_heap_t* s, unsigned n)
    {
        if (s->a < n)
        {
            struct event** p;
            unsigned a = s->a ? s->a * 2 : 8;//如果堆中本身是空的,就直接分配为8,否则就直接加倍,这样就防止每add一个event都需需要realloc
            if (a < n)//如果加倍仍然无法满足条件,就直接用n
                a = n;
            if (!(p = (struct event**)mm_realloc(s->p, a * sizeof *p))) //realloc分配内存
                return -1;
            s->p = p; //保存分配内存的地址
            s->a = a; //保存堆的容量
        }
        return 0;
    }

       前面说过,min_heap的成员变量a描述的是堆最大所能容纳的元素数目,也就是堆的容量。如果传入的n本身小于a,说明当前堆完全可以装下n个元素,因此无需再扩容了。

       如果传入的n不小于a,说明此时的堆刚刚能装下或者装不下n个元素,此时就需要对堆进行扩容。min_heap这里分了两种情况:如果堆本身为空,那么就直接为堆分配8个元素的空间;如果堆本身不为空,那么就先将堆原本的空间加倍,作为堆的新容量,如果堆非空时加倍之后或者堆空时分配8个元素空间还放不下n个元素,那么就直接把n作为堆的新容量。这样做的好处是不用每次插入一个新的event都去重新分配空间。

       此外,如果min_heap需要分配更大的空间,这里使用的是realloc函数,会先调用malloc函数进行指定大小空间的分配,再把原来的内存数据复制到新空间中。
堆元素的上浮

       在小顶堆(大顶堆)中,当堆中元素需要进行调整时,就会对相应的元素进行上浮或者下沉,之所以要这样做,是因为堆中元素调整后不一定还满足小顶堆(大顶堆)的性质,因此就要重新进行调整,让堆重新满足原来的特性。

       min_heap的堆元素上浮是通过min_heap_shift_up_函数实现的,该函数定义如下:

    //hole_index是需要调整的结点索引
    void min_heap_shift_up_(min_heap_t* s, unsigned hole_index, struct event* e)
    {
        unsigned parent = (hole_index - 1) / 2;  //找到其父节点的索引
        //如果父节点的超时值大于当前event结点的超时值,不满足小顶堆性质,就上浮
        while (hole_index && min_heap_elem_greater(s->p[parent], e))  
        {
        //将原来的父节点event换到hole_index的位置上并改变父节点event的堆索引值
        (s->p[hole_index] = s->p[parent])->ev_timeout_pos.min_heap_idx = hole_index;
        hole_index = parent;  //此时就上浮到了parent的位置,现在以parent出发继续判断
        parent = (hole_index - 1) / 2;  //计算新的父节点索引
        }
        //执行到这里hole_index就是需要调整的event的最终位置,然后就直接将event放到该位置并设置event中的堆索引值即可
        (s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
    }

       上浮,顾名思义就是判断当前结点与其父节点的关系是否满足小顶堆的性质,如果不满足那么就应当将当前结点和父节点互换,然后从当前结点的新位置出发继续上浮,直到结点关系满足小顶堆性质为止。
堆元素的下沉

       与堆元素的上浮相似,由min_heap_shift_down_函数实现,其定义如下:

    //hole_index为需要调整的event的堆索引
    void min_heap_shift_down_(min_heap_t* s, unsigned hole_index, struct event* e)
    {
        unsigned min_child = 2 * (hole_index + 1);  //计算右子结点的堆索引
        while (min_child <= s->n) //如果右子结点存在
        {
        //如果右子结点超时值大于左子结点或者只有左子结点,那么左子结点值就是较小的(或唯一的),此时就只用比较左子结点和当前结点,否则就比较当前结点和右子结点
        min_child -= min_child == s->n || min_heap_elem_greater(s->p[min_child], s->p[min_child - 1]);
        //到这里min_child的值就是左右子结点中较小结点的索引
        if (!(min_heap_elem_greater(e, s->p[min_child]))) //如果当前结点就是三个结点中的最小值说明满足小顶堆性质,无需下沉直接退出,否则就往下调整
            break;
        //将较小结点赋值到当前结点,并修改其堆索引
        (s->p[hole_index] = s->p[min_child])->ev_timeout_pos.min_heap_idx = hole_index;
        hole_index = min_child;//更新hole_index到原最小结点的索引
        min_child = 2 * (hole_index + 1);  //继续计算右子结点索引
        }
        //此时已经找到合适的位置,直接更新event的索引及位置。
        (s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
    }

       堆元素的下沉与上浮是差不多的,只不过上浮是从子结点出发判断子结点与父节点的关系进行调整,而下浮则是从父节点出发判断父节点和子结点的关系进行调整。如果下沉过程中当前结点不是它自身与其左右子结点三者间的最小值,那么就将当前结点与最小结点进行互换,然后互换后的当前结点继续从新位置出发下沉调整,直到满足小顶堆性质。
堆插入元素

       向堆中插入一个新元素,由min_heap_push实现,其定义如下:

    int min_heap_push(min_heap_t* s, struct event* e)//向堆中添加event指针
    {
        if (min_heap_reserve(s, s->n + 1))  //为待插入的event重新分配一个位置
            return -1;
        min_heap_shift_up_(s, s->n++, e);  //虽然heap空间可能加倍,但是还是从当前heap的有效结点的后一个位置插入event,然后上浮,push后n加1
        return 0;
    }

      由于是向堆中插入元素,因此需要先使用min_heap_reserve函数来为新插入元素分配足够大小的堆内存。新元素的位置实际上是之前最有一个“有效元素”的后面一个,这里的“有效元素”只是为了说明新插入元素的位置并非是min_heap分配空间中的最后一个位置。由于是从堆的“尾部”插入一个新元素,那么自然就需要调整该元素,进行“上浮 ”操作。
堆删除元素

       需要注意的一点是,由于堆末尾的元素对于整个堆来说,删除它对于堆是没有任何影响的,因此,如果要对堆中的任意一个元素进行删除,就可以将需要删除的元素先和堆尾元素互换,然后不考虑需要删除的元素,对互换后的堆进行调整,最终得到的堆就是删除了该元素的堆了。由min_heap_erase实现,由于其定义如下:

    int min_heap_erase(min_heap_t* s, struct event* e)//
    {
        if (-1 != e->ev_timeout_pos.min_heap_idx)//堆索引为-1表示不在堆上
        {
            struct event *last = s->p[--s->n]; //获取堆中的最后一个元素
            unsigned parent = (e->ev_timeout_pos.min_heap_idx - 1) / 2; //找到需要删除的结点的父节点的堆索引
            /* we replace e with the last element in the heap.  We might need to
               shift it upward if it is less than its parent, or downward if it is
               greater than one or both its children. Since the children are known
               to be less than the parent, it can't need to shift both up and
               down. */
            //如果要删除的event不在堆顶,并且最后一个结点的超时值小于父节点的超时值
            if (e->ev_timeout_pos.min_heap_idx > 0 && min_heap_elem_greater(s->p[parent], last))
                min_heap_shift_up_(s, e->ev_timeout_pos.min_heap_idx, last); //相当于把最后一个event换到了要删除的结点位置,此时换过来的结点及其子结点必然也是满足小顶堆性质的,因此从该结点出发进行上浮调整
            else //如果要删除的event本身就是堆顶,或者最后一个结点的超时值不小于父节点的超时值,就将最后一个结点的超时值换到要删除的结点位置,然后下沉
                min_heap_shift_down_(s, e->ev_timeout_pos.min_heap_idx, last);
            e->ev_timeout_pos.min_heap_idx = -1;  //被删除的结点堆索引值重置为-1
            return 0;
        }
        return -1;   //说明需要删除的结点本身就不在堆上
    }

弹出堆顶元素

       弹出堆顶元素和返回堆顶元素时两码事,前者会改变堆,而后者则只是查询。弹出堆顶元素实际上就是删除堆顶元素,由min_heap_pop函数实现,其定义如下:

    struct event* min_heap_pop(min_heap_t* s)
    {
        if (s->n)
        {
            struct event* e = *s->p;  //找到第一个元素
            min_heap_shift_down_(s, 0u, s->p[--s->n]);  // --s->n为最后一个结点的的堆索引,这就相当于将最后一个event换到堆索引为0的位置,然后下沉调整这个堆,调整后堆顶就是新的最小值了
            e->ev_timeout_pos.min_heap_idx = -1;  //弹出后堆索引重置为-1
            return e;
        }
        return 0;  //如果堆空就返回NULL
    }

 
————————————————
版权声明:本文为CSDN博主「HerofH_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_28114615/article/details/95342338

原文地址:https://www.cnblogs.com/cnhk19/p/14517334.html