最近在看libevent源码时,遇到了最小堆,正好通过分析该源码,学习一下最小堆。最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。libevent的最小堆是通过数组的形式实现的,索引从0开始,下面的图片表示堆与索引的关系,该索引与数组下标是相同的,其中最小堆即为索引0的元素不大于1和2的元素,1的元素不大于3和4的,2的元素不大于5和6的元素。
1、堆定义
typedef struct min_heap
{
struct event** p;
unsigned n, a;
} min_heap_t;
首先,就是该最小堆的结构体,p表示堆的内容,n表示堆现在的大小,a表示堆总的分配空间的大小(包括还没使用的)。
2、堆的构造
static inline void min_heap_ctor_(min_heap_t* s);
void min_heap_ctor_(min_heap_t* s) { s->p = 0; s->n = 0; s->a = 0; }
堆的所有操作使用的都是内联函数,构造函数很简单,就是将结构体的内容全部用0初始化。
3、堆的析构
static inline void min_heap_dtor_(min_heap_t* s);
void min_heap_dtor_(min_heap_t* s) { if (s->p) mm_free(s->p); }
堆的析构也很简单,就是单纯的判断一下是否为堆分配过空间,如果分配过,就释放。
4、堆的内容初始化
static inline void min_heap_elem_init_(struct event* e);
void min_heap_elem_init_(struct event* e) { e->ev_timeout_pos.min_heap_idx = -1; }
该函数对堆的内容,也就是结构体中p所指向的数组成员的初始化。将该元素的索引min_heap_idx置为-1。
5、判断堆是否为空
static inline int min_heap_empty_(min_heap_t* s);
int min_heap_empty_(min_heap_t* s) { return 0u == s->n; }
通过判断成员n是否为0。
6、判断堆的大小
static inline unsigned min_heap_size_(min_heap_t* s);
unsigned min_heap_size_(min_heap_t* s) { return s->n; }
成员n表示堆的大小。
7、堆的顶点元素
static inline struct event* min_heap_top_(min_heap_t* s);
struct event* min_heap_top_(min_heap_t* s) { return s->n ? *s->p : 0; }
p指向的第一个元素即为顶点元素
8、向堆中添加元素
static inline int min_heap_push_(min_heap_t* s, struct event* e);
int min_heap_push_(min_heap_t* s, struct event* e)
{
if (min_heap_reserve_(s, s->n + 1))
return -1;
min_heap_shift_up_(s, s->n++, e);
return 0;
}
该函数包含调用了两个函数接口,其中min_heap_reserve_定义如下:
static inline int min_heap_reserve_(min_heap_t* s, unsigned n);
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;
if (a < n)
a = n;
if (!(p = (struct event**)mm_realloc(s->p, a * sizeof *p)))
return -1;
s->p = p;
s->a = a;
}
return 0;
}
该函数很简单,n表示堆的元素的个数,首先判断元素a(堆总大小)是否小于n,如果小于,表示堆的空间不够用了,需要分配新的空间,通过realloc分配更大的空间,当原来堆为空时,默认分配8个元素的空间;当原来堆不为空,分配原来两倍的空间。
即在min_heap_push_函数中,先判断需不需要分配更大的空间,然后调用min_heap_shift_up_,该函数定义如下:
static inline void min_heap_shift_up_(min_heap_t* s, unsigned hole_index, struct event* e);
void min_heap_shift_up_(min_heap_t* s, unsigned hole_index, struct event* e)
{
unsigned parent = (hole_index - 1) / 2;
while (hole_index && min_heap_elem_greater(s->p[parent], e))
{
(s->p[hole_index] = s->p[parent])->ev_timeout_pos.min_heap_idx = hole_index;
hole_index = parent;
parent = (hole_index - 1) / 2;
}
(s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
}
该函数的意思是将索引为hole_index的元素e根据大小尝试向上移动。即先将新加入的元素默认为树的最后一个元素,然后通过该函数,将新加入的元素通过最小堆原则,即父不大于子的原则,向上移动到合适的位置。首先获取该节点的父节点,通过判断该节点的父节点是否大于该节点,如果大于,需要将父节点与本节点交换位置。并再次获取现在位置的父节点,继续判断,直到满足hole_index为0,即该节点已经为顶点节点,或者该节点的父节点不大于该节点为止。然后将当前的索引赋值给新加入的元素
9、从堆中拿出顶点元素
static inline struct event* min_heap_pop_(min_heap_t* s);
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]);
e->ev_timeout_pos.min_heap_idx = -1;
return e;
}
return 0;
}
从堆中拿出顶点元素后,需要将堆重新排列,组成新的堆。该函数使用的策略为先将最后一个元素升级为顶点元素,然后对该元素尝试向下移动,其中min_heap_shift_down_即实现尝试向下移动的功能。
static inline void min_heap_shift_down_(min_heap_t* s, unsigned hole_index, struct event* e);
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]);
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;
min_child = 2 * (hole_index + 1);
}
(s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
}
该函数尝试将索引为hole_index的元素e向下移动到合适的位置。该函数首先获取该节点的右子节点,然后通过获取它的两个子节点的最小值。min_child -= min_child == s->n || min_heap_elem_greater(s->p[min_child], s->p[min_child - 1]);该语句有一点复杂。首先,该语句先判断min_child==s->n,该判断如果相等,表示索引为hole_index的节点没有右子节点,其左子节点即为该堆的最后一个元素,那么||判断直接为真,min_child-=1来获取该左节点;如果min_child!=s->n,再通过判断右子节点是否大于左子节点,如果大于,||为真,min_child-=1获取该左子节点;否则表示右子节点小,min_child不需要减1。然后通过min_heap_elem_greater(e, s->p[min_child])判断该节点是否小于其最小的子节点,如果大于,需要将该节点与其子节点换位,并继续与之后的子节点做相同的处理。直到移动到合适的位置。
然后在min_heap_pop_函数中返回顶点元素。
10、判断某元素是否为顶点元素
static inline int min_heap_elt_is_top_(const struct event *e);
int min_heap_elt_is_top_(const struct event *e)
{
return e->ev_timeout_pos.min_heap_idx == 0;
}
通过判断该元素的索引是否为0
11、删除某个元素
static inline int min_heap_erase_(min_heap_t* s, struct event* e);
int min_heap_erase_(min_heap_t* s, struct event* e)
{
if (-1 != e->ev_timeout_pos.min_heap_idx)
{
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. */
if (e->ev_timeout_pos.min_heap_idx > 0 && min_heap_elem_greater(s->p[parent], last))
min_heap_shift_up_unconditional_(s, e->ev_timeout_pos.min_heap_idx, last);
else
min_heap_shift_down_(s, e->ev_timeout_pos.min_heap_idx, last);
e->ev_timeout_pos.min_heap_idx = -1;
return 0;
}
return -1;
}
首先判断该元素的索引是否有效,如果有效,将该元素与最后一个元素换位置(实际上还并没有真正交换,只是通过下面的处理,可以看出是将最后一个元素提到要删除的元素位置,并进行处理)。判断该元素的父节点与最后一个元素的关系,如果父节点大,需要尝试将最后一个元素向上移动(当前要将最后一个元素的位置按照删除的元素的位置处理);否则,尝试将最后一个节点向下移动。而这里的向上移动函数min_heap_shift_up_unconditional_(s, e->ev_timeout_pos.min_heap_idx, last)如下,
void min_heap_shift_up_unconditional_(min_heap_t* s, unsigned hole_index, struct event* e)
{
unsigned parent = (hole_index - 1) / 2;
do
{
(s->p[hole_index] = s->p[parent])->ev_timeout_pos.min_heap_idx = hole_index;
hole_index = parent;
parent = (hole_index - 1) / 2;
} while (hole_index && min_heap_elem_greater(s->p[parent], e));
(s->p[hole_index] = e)->ev_timeout_pos.min_heap_idx = hole_index;
}
可以看到,该函数几乎和min_heap_shift_up_是一样的,参数也是一样的,表示将位于hole_index的元素e向上移动,唯一的区别是,该函数先处理一遍循环再判断,原因很简单,在min_heap_erase_函数中已经判断过了if (e->ev_timeout_pos.min_heap_idx > 0 && min_heap_elem_greater(s->p[parent], last)),所以第一次执行不需要判断了。
12、调整某个元素的位置
static inline int min_heap_adjust_(min_heap_t *s, struct event* e);
int min_heap_adjust_(min_heap_t *s, struct event *e)
{
if (-1 == e->ev_timeout_pos.min_heap_idx) {
return min_heap_push_(s, e);
} else {
unsigned parent = (e->ev_timeout_pos.min_heap_idx - 1) / 2;
/* The position of e has changed; we shift it up or down
* as needed. We can't need to do both. */
if (e->ev_timeout_pos.min_heap_idx > 0 && min_heap_elem_greater(s->p[parent], e))
min_heap_shift_up_unconditional_(s, e->ev_timeout_pos.min_heap_idx, e);
else
min_heap_shift_down_(s, e->ev_timeout_pos.min_heap_idx, e);
return 0;
}
}
该函数首先判断该元素是否有效,即是否已经在堆中了,如果不在,直接调用min_heap_push_插入;否则判断其父节点与该节点的关系,分别调用min_heap_shift_up_unconditional_和min_heap_shift_down_进行上移或者下移,跟min_heap_erase_函数是一样的。