數據結構和算法面試題系列—二叉堆

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏git

0 概述

本文要描述的堆是二叉堆。二叉堆是一種數組對象,能夠被視爲一棵徹底二叉樹,樹中每一個結點和數組中存放該結點值的那個元素對應。樹的每一層都是填滿的,最後一層除外。二叉堆能夠用於實現堆排序,優先級隊列等。本文代碼地址在 這裏github

1 二叉堆定義

使用數組來實現二叉堆,二叉堆兩個屬性,其中 LENGTH(A) 表示數組 A 的長度,而 HEAP_SIZE(A) 則表示存放在A中的堆的元素個數,其中 LENGTH(A) <= HEAP_SIZE(A),也就是說雖然 A[0,1,...N-1] 均可以包含有效值,可是 A[HEAP_SIZE(A)-1] 以後的元素不屬於相應的堆。面試

二叉堆對應的樹的根爲 A[0],給定某個結點的下標 i ,能夠很容易計算它的父親結點和兒子結點。注意在後面的示例圖中咱們標註元素是從1開始計數的,而實現代碼中是從0開始計數。算法

#define PARENT(i) ( i > 0 ? (i-1)/2 : 0)
#define LEFT(i) (2 * i + 1)
#define RIGHT(i) (2 * i + 2)
複製代碼

注:堆對應的樹每一層都是滿的,因此一個高度爲 h 的堆中,元素數目最多爲 1+2+2^2+...2^h = 2^(h+1) - 1(滿二叉樹),元素數目最少爲 1+2+...+2^(h-1) + 1 = 2^h。 因爲元素數目 2^h <= n <= 2^(h+1) -1,因此 h <= lgn < h+1,所以 h = lgn 。即一個包含n個元素的二叉堆高度爲 lgnapi

2 保持堆的性質

本文主要創建一個最大堆,最小堆原理相似。爲了保持堆的性質,maxHeapify(int A[], int i) 函數讓堆數組 A 在最大堆中降低,使得以 i 爲根的子樹成爲最大堆。數組

void maxHeapify(int A[], int i, int heapSize)
{
    int l = LEFT(i);
    int r = RIGHT(i);

    int largest = i;

    if (l <= heapSize-1 && A[l] > A[i]) {
        largest = l;
    }

    if (r <= heapSize-1 && A[r] > A[largest]) {
        largest = r;
    }

    if (largest != i) { // 最大值不是i,則須要交換i和largest的元素,並遞歸調用maxHeapify。
        swapInt(A, i, largest);
        maxHeapify(A, largest, heapSize);
    }
}
複製代碼
  • 在算法每一步裏,從元素 A[i]A[left] 以及 A[right] 中選出最大的,將其下標存在 largest 中。若是 A[i] 最大,則以 i 爲根的子樹已是最大堆,程序結束。bash

  • 不然,i 的某個子結點有最大元素,將 A[i]A[largest] 交換,從而使i及其子女知足最大堆性質。此外,下標爲 largest 的結點在交換後值變爲 A[i],以該結點爲根的子樹又有可能違反最大堆的性質,因此要對該子樹遞歸調用maxHeapify()函數。數據結構

maxHeapify() 函數做用在一棵以 i 爲根結點的、大小爲 n 的子樹上時,運行時間爲調整A[i]A[left]A[right] 的時間 O(1),加上對以 i 爲某個子結點爲根的子樹遞歸調用 maxHeapify 的時間。i 結點爲根的子樹大小最多爲 2n/3(最底層恰好半滿的時候),因此能夠推得 T(N) <= T(2N/3) + O(1),因此 T(N)=O(lgN)數據結構和算法

下圖是一個運行 maxHeapify(heap, 2) 的例子。A[] = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1},堆大小爲 10函數

保持最大堆性質

3 創建最大堆

咱們能夠知道,數組 A[0, 1, ..., N-1] 中,A[N/2, ..., N-1] 的元素都是樹的葉結點。如上面圖中的 6-10 的結點都是葉結點。每一個葉子結點能夠看做是隻含一個元素的最大堆,所以咱們只須要對其餘的結點調用 maxHeapify() 函數便可。

void buildMaxHeap(int A[], int n)
{
    int i;
    for (i = n/2-1; i >= 0; i--) {
        maxHeapify(A, i, n);
    }
}
複製代碼

之因此這個函數是正確的,咱們須要來證實一下,可使用循環不變式來證實。

循環不變式:在for循環開始前,結點 i+一、i+2...N-1 都是一個最大堆的根。

初始化:for循環開始迭代前,i = N/2-1, 結點 N/2, N/2+1, ..., N-1都是葉結點,也都是最大堆的根。

保持:由於結點 i 的子結點標號都比 i 大,根據循環不變式的定義,這些子結點都是最大堆的根,因此調用 maxHeapify() 後,i 成爲了最大堆的根,而 i+1, i+2, ..., N-1仍然保持最大堆的性質。

終止:過程終止時,i=0,所以結點 0, 1, 2, ..., N-1都是最大堆的根,特別的,結點0就是一個最大堆的根。

創建最大堆

雖然每次調用 maxHeapify() 時間爲 O(lgN),共有 O(N) 次調用,可是說運行時間是 O(NlgN) 是不確切的,準確的來講,運行時間爲 O(N),這裏就不證實了,具體證實過程參見《算法導論》。

4 堆排序

開始用 buildMaxHeap() 函數建立一個最大堆,由於數組最大元素在 A[0],經過直接將它與A[N-1] 互換來達到最終正確位置。去掉 A[N-1],堆的大小 heapSize 減1,調用maxHeapify(heap, 0, --heapSize) 保持最大堆的性質,直到堆的大小由N減到1。

void heapSort(int A[], int n)
{
    buildMaxHeap(A, n);
    int heapSize = n;
    int i;
    for (i = n-1; i >= 1; i--) {
        swapInt(A, 0, i);
        maxHeapify(A, 0, --heapSize);
    }
}
複製代碼

5 優先級隊列

最後實現一個最大優先級隊列,主要有四種操做,分別以下所示:

  • insert(PQ, key):將 key 插入到隊列中。
  • maximum(PQ): 返回隊列中最大關鍵字的元素
  • extractMax(PQ):去掉並返回隊列中最大關鍵字的元素
  • increaseKey(PQ, i, key):將隊列 i 處的關鍵字的值增長到 key

這裏定義一個結構體 PriorityQueue 便於操做。

typedef struct PriorityQueue {
    int capacity;
    int size;
    int elems[];
} PQ;
複製代碼

最終優先級隊列的操做實現代碼以下:

/**
 * 從數組建立優先級隊列
 */
PQ *newPQ(int A[], int n)
{
    PQ *pq = (PQ *)malloc(sizeof(PQ) + sizeof(int) * n);
    pq->size = 0;
    pq->capacity = n;

    int i;
    for (i = 0; i < pq->capacity; i++) {
        pq->elems[i] = A[i];
        pq->size++;
    }
    buildMaxHeap(pq->elems, pq->size);

    return pq;
}

int maximum(PQ *pq)
{
    return pq->elems[0];
}

int extractMax(PQ *pq)
{
    int max = pq->elems[0];
    pq->elems[0] = pq->elems[--pq->size];
    maxHeapify(pq->elems, 0, pq->size);
    return max;
}

PQ *insert(PQ *pq, int key)
{
    int newSize = ++pq->size;
    if (newSize > pq->capacity) {
        pq->capacity = newSize * 2;
        pq = (PQ *)realloc(pq, sizeof(PQ) + sizeof(int) * pq->capacity);
    }
    pq->elems[newSize-1] = INT_MIN;
    increaseKey(pq, newSize-1, key);
    return pq;
}

void increaseKey(PQ *pq, int i, int key)
{
    int *elems = pq->elems;
    elems[i] = key;

    while (i > 0 && elems[PARENT(i)] < elems[i]) {
        swapInt(elems, PARENT(i), i);
        i = PARENT(i);
    }
}
複製代碼

參考資料

  • 算法導論第6章《堆排序》
相關文章
相關標籤/搜索