c语言最小堆的实现-优先队列

一、背景

libevent 中有定时事件的管理,用户可以把超时的定时事件插入到 管理器中,当时间到了之后触发用户的回调函数处理;

查看了源码发现,定时器的数据结构其实是由最小堆来实现的。

二、相关知识

2.1 最小堆(最小优先队列)

“优先队列(priority)是一种用来维护由一组元素构成的集合S的数据结构”——算法导论P90
按照简单可以理解为 使用数组实现的二叉树的数据结构,并满足每个子节点key值都要大于父节点key值;
c语言最小堆的实现-优先队列_第1张图片

三、实现

以下代码基于 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;
}

对于数组的维护,里面实现的逻辑为最开始开辟了8个空间,当新增空间不够用时,再进行翻倍扩充;

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



你可能感兴趣的:(linux)