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


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

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


{

/**//* 獲取父節(jié)點 */
unsigned parent = (hole_index - 1) / 2;

/**//* 只要父節(jié)點還大于子節(jié)點就循環(huán) */
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;

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

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


{

/**//* 取得hole_index的右孩子節(jié)點索引 */
unsigned min_child = 2 * (hole_index + 1);
while(min_child <= s->n)

{

/**//* 有點惡心的一個表達式,目的就是取兩個孩子節(jié)點中較大的那個孩子索引 */
min_child -= min_child == s->n || min_heap_elem_greater(s->p[min_child], s->p[min_child - 1]);

/**//* 找到了位置,這里似乎是個優(yōu)化技巧,不知道具體原理 */
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);
}

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


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