版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stayneckwind2/article/details/57081808
一、背景
libevent 中有定时事件的管理,用户可以把超时的定时事件插入到 管理器中,当时间到了之后触发用户的回调函数处理;
查看了源码发现,定时器的数据结构其实是由最小堆来实现的。
二、相关知识
2.1 最小堆(最小优先队列)
“优先队列(priority)是一种用来维护由一组元素构成的集合S的数据结构”——算法导论P90
按照简单可以理解为 使用数组实现的二叉树的数据结构,并满足每个子节点key值都要大于父节点key值;
三、实现
以下代码基于 libevent-2.0.20-stable/minheap-internal.h 进行修改,提高了一些可读性;
static inline void min_heap_ctor(min_heap_t * pheap);
static inline void min_heap_dtor(min_heap_t * pheap);
static inline void min_heap_elem_init(struct event *e);
static inline int min_heap_elt_is_top(const struct event *e);
static inline int min_heap_elem_greater(struct event *max_size, struct event *b);
static inline int min_heap_empty(min_heap_t * pheap);
static inline unsigned min_heap_size(min_heap_t * pheap);
static inline struct event * min_heap_top(min_heap_t * pheap);
static inline int __reserve(min_heap_t * pheap, unsigned used_num);
static inline int min_heap_push(min_heap_t * pheap, struct event *e);
static inline struct event * min_heap_pop(min_heap_t * pheap);
static inline int min_heap_erase(min_heap_t * pheap, struct event *e);
static inline void __shift_up(min_heap_t * pheap, unsigned hole_idx, struct event *e);
static inline void __shift_down(min_heap_t * pheap, unsigned hole_idx, struct event *e);
数据结构如下,原文 struct event 实际对应的为 事件的上下文,在这里进行了一些简化;
struct event {
u32 idx; /* node index */
int key; /* custom key */
void *pdata; /* custom data */
};
typedef struct min_heap
{
struct event **p;
u32 used_num;
u32 max_size;
} min_heap_t;
#define __key_cmp(a, b) ((a)->key > (b)->key)
#define __get_parent(idx) (((idx) - 1) / 2)
#define __get_child_left(idx) (2 * ((idx) + 1))
相关操作宏,然后是 上滤(push操作用)、下滤操作(pop操作用)
static void __shift_up(min_heap_t *pheap, u32 hole_idx, struct event *e)
{
u32 parent = __get_parent(hole_idx);
while ( hole_idx && __key_cmp(pheap->p[parent], e) ) {
pheap->p[hole_idx] = pheap->p[parent];
pheap->p[hole_idx]->idx = hole_idx;
hole_idx = parent;
parent = __get_parent(hole_idx);
}
pheap->p[hole_idx] = e;
pheap->p[hole_idx]->idx = hole_idx;
}
static void __shift_down(min_heap_t * pheap, u32 hole_idx, struct event *e)
{
u32 min_child = 2 * (hole_idx + 1);
while ( min_child <= pheap->used_num ) {
#if 1
if ( min_child == pheap->used_num ||
__key_cmp(pheap->p[min_child], pheap->p[min_child - 1]) ) {
min_child--;
}
#else
min_child -= min_child == used_num || __key_cmp(pheap->p[min_child], pheap->p[min_child - 1]);
#endif
if ( !(__key_cmp(e, pheap->p[min_child])) ) {
break;
}
(pheap->p[hole_idx] = pheap->p[min_child])->idx = hole_idx;
hole_idx = min_child;
min_child = 2 * (hole_idx + 1);
}
(pheap->p[hole_idx] = e)->idx = hole_idx;
}
static int __reserve(min_heap_t *pheap, u32 expect)
{
if ( pheap->max_size < expect ) {
struct event **p;
u32 max_size = pheap->max_size ? pheap->max_size * 2 : 8;
if ( max_size < expect ) {
max_size = expect;
}
p = (struct event **)realloc(pheap->p, max_size * sizeof(*p));
if ( !p ) {
return FAILURE;
}
pheap->p = p;
pheap->max_size = max_size;
}
return SUCCESS;
}
准备工作做完,然后就是 push操作,通过在数组尾部插入元素,再进行上滤调整;
pop操作则是 对堆顶(最小值)进行剔除,然后进行下滤调整堆的结构;
int min_heap_push(min_heap_t *pheap, int key, void *pdata)
{
if ( __reserve(pheap, pheap->used_num + 1) ) {
return FAILURE;
}
struct event *e = (struct event *)calloc(1, sizeof(struct event));
if ( !e ) {
return FAILURE;
}
e->key = key;
e->pdata = pdata;
__shift_up(pheap, pheap->used_num++, e);
return SUCCESS;
}
void *min_heap_pop(min_heap_t *pheap)
{
void *pdata = NULL;
if ( !pheap || !pheap->used_num ) {
LOGW("NULL or empty\n");
return NULL;
}
struct event *e = *pheap->p;
__shift_down(pheap, 0u, pheap->p[--pheap->used_num]);
e->idx = -1;
pdata = e->pdata;
FREE_POINTER(e);
return pdata;
}
最后,附上一个简单的测试,最后构造出来的效果为上一章节的结构图;
int __on_traverse(void *args, int key, void *pdata)
{
printf("%d %d\n", key, *(char *)pdata);
return SUCCESS;
}
int main(int argc, char *argv[])
{
int ret = FAILURE;
int ix = 0;
int *c = NULL;
struct test {
int key;
int value;
} test[] = {
{3, 3},
{14, 14},
{7, 7},
{15, 15},
{8, 8},
{2, 2},
{9, 9},
{10, 10},
{16, 16},
};
min_heap_t heap = {0};
for ( ix = 0; ix < sizeof(test)/sizeof(struct test); ix++ ) {
ASSERT(SUCCESS, ret = min_heap_push(&heap, test[ix].key, &test[ix].value));
}
for ( ix = 0; ix < sizeof(test)/sizeof(struct test); ix++ ) {
ASSERT_FAIL(NULL, c = min_heap_pop(&heap)); printf("%d\n", *c);
}
ret = SUCCESS;
goto _E1;
_E1:
printf("Result:\t\t\t\t[%s]\n", ret ? "Failure" : "Success");
exit(ret ? EXIT_FAILURE : EXIT_SUCCESS);
}
四、总结
优先队列为完全二叉树,所以在插入调整的时间复杂度为 O(N),弹出的复杂度为O(1);
但是针对优先队列的特性来说,查找、删除操作会比较麻烦;仔细看 libevent 的miniheap里面也有删除接口,
是给定struct event的位置进行删除的,原来是libevent 内部还结合了红黑树、链表进行struct event维护的,这样就合理了;
参考文章:
[1] 关于源码分析 http://www.cnblogs.com/sdu20112013/p/4104439.html
[2] 关于时间复杂度 http://blog.csdn.net/don_lvsml/article/details/19546997