author : Kevin Lynx
什么是min heap ?
首先看什么是heap,heap是這樣一種數據結構:1.它首先是一棵完成二叉樹;2.父親節點始終大于(或其他邏輯關系
)其孩子節點。根據父親節點與孩子節點的這種邏輯關系,我們將heap分類,如果父親節點小于孩子節點,那么這個heap
就是min heap。
就我目前所看到的實現代碼來看,heap基本上都是用數組(或者其他的連續存儲空間)作為其存儲結構的。這可以保證
數組第一個元素就是heap的根節點。這是一個非常重要的特性,它可以保證我們在取heap的最小元素時,其算法復雜度為
O(1)。
原諒我的教科書式說教,文章后我會提供我所查閱的比較有用的關于heap有用的資料。
What Can It Do?
libevent中的min_heap其實不算做真正的MIN heap。它的節點值其實是一個時間值。libevent總是保證時間最晚的節
點為根節點。
libevent用這個數據結構來實現IO事件的超時控制。當某個事件(libevent中的struct event)被添加時(event_add),
libevent將此事件按照其超時時間(由用戶設置)保存在min_heap里。然后libevent會定期地去檢查這個min_heap,從而實現
了超時機制。
實現
min_heap相關源碼主要集中在min_heap.h以及超時相關的event.c中。
首先看下min_heap的結構體定義:
typedef struct min_heap


{
struct event** p;
unsigned n, a;
} min_heap_t;

p指向了一個動態分配的數組(隨便你怎么說,反正它是一個由realloc分配的連續內存空間),數組元素為event*,這也是
heap中的節點類型。這里libevent使用連續空間去保存heap,也就是保存一棵樹。因為heap是完成樹,所以可以保證其元素在
數組中是連續的。n表示目前保存了多少個元素,a表示p指向的內存的尺寸。
struct event這個結構體定義了很多東西,但是我們只關注兩個成員:min_heap_idx:表示該event保存在min_heap數組中
的索引,初始為-1;ev_timeout:該event的超時時間,將被用于heap操作中的節點值比較。
接下來看幾個與堆操作不大相關的函數:
min_heap_elem_greater:比較兩個event的超時值大小。
min_heap_size:返回heap元素值數量。
min_heap_reserve:調整內存空間大小,也就是調整p指向的內存區域大小。凡是涉及到內存大小調整的,都有一個策略問
題,這里采用的是初始大小為8,每次增大空間時以2倍的速度增加。
看幾個核心的:真正涉及到heap數據結構操作的函數:往堆里插入元素、從堆里取出元素:
相關函數為:min_heap_push、min_heap_pop、min_heap_erase、min_heap_shift_up_、min_heap_shift_down_。
heap的核心操作:
- 往堆里插入元素:
往堆里插入元素相對而言比較簡單,如圖所示,每一次插入時都從樹的最右最下(也就是葉子節點)開始。然后比較即將插入
的節點值與該節點的父親節點的值,如果小于父親節點的話(不用在意這里的比較規則,上下文一致即可),那么交換兩個節點,
將新的父親節點與其新的父親節點繼續比較。重復這個過程,直到比較到根節點。
libevent實現這個過程的函數主要是min_heap_shift_up_。每一次min_heap_push時,首先檢查存儲空間是否足夠,然后直接
調用min_heap_shift_up_插入。主要代碼如下:
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])->min_heap_idx = hole_index;
hole_index = parent;
parent = (hole_index - 1) / 2;
}
(s->p[hole_index] = e)->min_heap_idx = hole_index;

- 從堆里取元素:
大部分時候,從堆里取元素只局限于取根節點,因為這個節點是最有用的。對于數組存儲結構而言,數組第一個元素即為根
節點。取完元素后,我們還需要重新調整整棵樹以使其依然為一個heap。
這需要保證兩點:1.依然是完成樹;2.父親節點依然小于孩子節點。
在具體實現heap的取元素操作時,具體到代碼層次,方法都是有那么點微小差別的。libvent里的操作過程大致如圖所示(實際上libevent中父節點的時間值小于子節點的時間值,時間值的比較通過evutil_timercmp實現):
主要過程分為兩步:
1.比較左右孩子節點,選擇最大的節點,移到父親節點上;按照這種方式處理被選擇的孩子節點,直到沒有孩子節點為止。例如,
當移除了100這個節點后,我們在100的孩子節點19和36兩個節點里選擇較大節點,即36,將36放置到100處;然后選擇原來的36的
左右孩子25和1,選25放置于原來的36處;
2.按照以上方式處理后,樹會出現一個空缺,例如原先的25節點,因為被移動到原先的36處,25處就空缺了。因此,為了保證完
成性,就將最右最下的葉子節點(也就是連續存儲結構中最后一個元素),例如這里的7,移動到空缺處,然后按照插入元素的方式處理
新插入的節點7。
完成整個過程。
libevent完成這個過程的函數主要是min_heap_shift_down_:

/**//* hole_index 為取出的元素的位置,e為最右最下的元素值 */
void min_heap_shift_down_(min_heap_t* s, unsigned hole_index, struct event* e)


{

/**//* 取得hole_index的右孩子節點索引 */
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])->min_heap_idx = hole_index;

/**//* 重復這個過程 */
hole_index = min_child;
min_child = 2 * (hole_index + 1);
}

/**//* 執行第二步過程,將最右最下的節點插到空缺處 */
min_heap_shift_up_(s, hole_index, e);
}


STL中的heap
值得一提的是,STL中提供了heap的相關操作算法,借助于模板的泛化特性,其適用范圍非常廣泛。相關函數為:
make_heap, pop_heap, sort_heap, is_heap, sort 。其實現原理同以上算法差不多,相關代碼在algorithm里。SGI的
STL在stl_heap.h里。
參考資料:
What is a heap?
Heap_(data_structure)
Heap Remove