Libevent源碼分析(一):最小堆


Libevent中的timeout事件是使用最小堆來管理維護的.代碼位於<minheap-internal.h>.

看函數命名和代碼風格應該是一個C++程序員,函數名都挺好懂的,只是下面這個結構體變量命名比較坑....

typedef struct min_heap
{
    struct event** p;
    unsigned n, a;//n隊列元素的多少,a代表隊列空間的大小.
} min_heap_t;

注釋是我加的,這命名,n啊a啊的,鬼知道啥意思....必須吐槽一下.

 

先來說說什么是最小堆:

1.堆是一個二叉樹

2.最小堆:父節點的值總是小於等於子節點

如下圖:

上圖圓圈旁邊的數字代表其在數組中的下標.堆一般是用數組來存儲的,也就是說實際存儲結構是連續的,只是邏輯上是一棵樹的結構.這樣做的好處是很容易找到堆頂的元素,對Libevent來說,很容易就可以找到距當前時間最近的timeout事件.

 現在想想看我們要插入一個元素,我們要怎么移動數組中元素的位置,使其邏輯上仍然是一個小堆?結合下圖很容易看出來:

1.假設我們要插入的元素為6大於其父節點的值2.則把元素放在數組相應的index上,插入完成.

 

2.假設我們要插入的為2小於其父節點的值3.則交換該節點與其父節點的值.對於下圖來說,交換完畢后插入就算完成了.那要是交換完后發現index=2的元素還小於其父節點index=0的呢?就又得在一次交換,如此循環,直到達到根節點或是其不大於父節點.

到了這里我們看看libevent里的實現代碼就很清楚了

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;
}

//分配隊列大小.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;  //初始分配8個指針大小空間,否則原空間大小翻倍.
        if (a < n)
            a = n;   //翻倍后空間依舊不足,則分配n.
        if (!(p = (struct event**)mm_realloc(s->p, a * sizeof *p))) //重新分配內存
            return -1;
        s->p = p; //重新賦值隊列地址及大小.
        s->a = a; //
    }
    return 0;
}

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;
}

 實際上作者寫了一個比較通用的函數min_heap_shift_up(),與之相對應的還有min_heap_shift_down()

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是要填入的值.還是畫圖比較好理解:

當這個值(下標為hole_Index=1)比其父節點1(index = 0)小時,要向上移動調整.

當這個值(下標為hole_Index=1)比其最小子節點6(index = 3)還大時,要向下移動調整.

Libevent里是這么使用他們的:

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];//把最后一個值作為要填入hole_index的值
        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_(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;
}
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;
}

 

最后總結一下,由於堆這種結構在邏輯上的這種二叉樹的關系,其插入也好,刪除也好,就是一個與父節點或是子節點比較然后調整位置,這一過程循環往復直到達到邊界條件的過程.記住這一點,就不難寫出代碼了.

二叉樹節點i:父節點為(i-1)/2.子節點為2i+1,2(i+1)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM