1. 程式人生 > >[data structure] heap 堆

[data structure] heap 堆

  • 定義

1  (二叉) 堆是一顆完全二叉樹。

2   任何一個節點都小於它的後裔節點。(最小堆),相應的,最大堆中的任何一個節點大於它的後裔節點。

由於二叉堆是一個完全二叉樹,因此對於二叉堆中的任何一個節點i,它的子節點的編號為:

A[ left ]  =  A[ 2*i + 1 ]     ;      A[ right ] = A [ 2*i + 2 ]   (注:這是與實際實現時相對應的,比如如下的堆:)

由於堆的完全二叉樹的性質,我們使用陣列來表示堆即可。如下圖所示:

     

左圖是一個大頂堆,堆頂的元素是最大的。下方的陣列是與該堆對應的。(注:上述的陣列的有序是純屬偶然的。)

首要的,堆的操作最重要的性質便是保持堆序性。

也就是說我們在加入元素以及刪除元素(一般是刪除堆頂)之後,需要對堆作出調整,使得堆依舊滿足堆序性。

  • 插入元素

向堆中加入新的元素i,輸入:要新增的元素elem。使得加入後依然是堆。簡單的示意圖如下:

                 插入元素15,那麼需要判斷15是否破壞了堆序性,即是否該元素放在了正確的位置,顯然 15 > 10,不滿足堆序性,因此15上濾,得到右圖,由於 15 < 18,說明15到達了正確的位置,因此成功插入了元素15.

  • 保持堆序性:

這是堆的所有操作的最重要的一個函式:通過遞迴的比較當前元素與它的左右子節點,決定是否將該元素下濾,直到到達正確的位置。

C++程式碼如下:

//max heapify for the i-th elem in A
void max_heapify(int A[], int  i, int heap_size)
{
    int largest = i;
    int left = 2*i+1;
    int right = 2*i+2;
    if(left < heap_size && A[left] > A[largest])
    {
        largest = left;
    }
    if(right < heap_size && A[right] > A[largest])
    {
        largest = right;
    }
    if(largest != i)
    {
        swap(A[i],A[largest]);
        max_heapify(A,largest,heap_size);
    }
}
  • 建堆

給定陣列A,要求將A變成一個堆,也就是說逐漸改變陣列A中的元素位置,使得陣列保持堆序性。

假設給定陣列A   =  {  3   13    2    8   18   10 },要將該陣列構建成一個最大堆。

首先,在構建最大堆之前,必須明確:

1    由完全二叉樹的性質,陣列的至少一半的元素為葉子節點:

一個簡單的例子:對於一顆整的完全二叉樹而言,第 i  層的元素個數為 2^(i-1),(根節點為第0層!!注意與平時樹的高度的區別)。那麼有如下示意圖:


因此,前i層的節點總數為:

1 + 2 + 2^2 + ... + 2^(i-1) = 2^i - 1

第i+1層(即葉子節點層)的節點個數為2^i ,因此葉子節點的個數大約為總節點個數的一半。

另一方面想,由於完全二叉樹中父節點與子節點之間的位置關係,也可以知道對於某個節點i而言,如果它的右孩子編號大於整個陣列的大小,那麼顯然節點i是最後一箇中間節點,它後面的節點應該全部為葉子節點。即

2*i+2 =  len, 那麼 i = ( len - 2 ) / 2。因此基於此我們建堆的過程只需要考慮陣列中的前一半的元素,程式碼如下:

void build_max_heap(int A[], int len)
{
    for(int i = len/2 ; i >= 0 ; i--)
    {
        max_heapify(A,i,len);
    }
}
  • 堆排序:

由於每一次從堆中找出最大的元素(大頂堆)是很直接的,直接取出對頂元素即可,那麼在刪除堆頂的元素之後:

1    堆中的元素個數會減一

2    堆頂空缺,需要將剩下的最大的元素放置在堆頂

如下的簡單示意圖:


按照上述一直到堆中的元素為0,那麼陣列中的元素也就有序了(從小到大有序)。

程式碼如下:

void heapSort(int A[], int len)
{
    build_max_heap(A,len);
    //printHeap(A,len);
    for(int i = len-1; i >= 0 ; --i)
    {
        swap(A[i],A[0]);
        max_heapify(A,0,i);
    }
}

因此,堆排序實現完畢。

注:如果你是要將陣列從大到小的排序,那麼你應該建立的是小頂堆,否則為大頂堆。

堆的各種操作大致都是堆的高度O(logn)量級的。

總結:堆是很重要的資料結構,在任務優先順序排程方面可作為優先順序佇列來使用,每次選擇優先順序最高的任務。

堆的操作還有很多,比如改變某個關鍵字的值,或者是刪除中間的某個關鍵字。值得一提的是,堆排序是一種in-place排序,複雜度為O(nlogn)。

補充:由於在實現堆的時候,老是要去畫二叉樹,於是自己實現了一個簡陋的只針對於完全二叉樹的程式:

static void printSpace(int number)
{
    while(number--)cout << " ";
}
void printHeap(int A[], int len)
{
    if(len == 0 )
    {
        cout << "len == 0 !!!" << endl;
        return;
    }
    int level = log10(len)/log10(2)+1;//得到樹的高度
    cout << "level = " << level << endl;
    printSpace(level*2);
    cout << A[0];
    printSpace(level*2);
    level--;
    cout << endl;
    int index = 1;
    for(int i = 0;i < level; ++i )
    {
        int count = pow(2,i);
        while(count--)
        {
            printSpace((level-i)*2);
            if(index < len) cout << A[index++];
            printSpace((level-i)*2);
            if(index < len)cout << A[index++];
            printSpace(count);
        }
        cout << endl;
    }
}

PS:簡陋的程式,只能用於畫完全二叉樹。