算法導論筆記第6章 堆和堆排序

堆排序結合了插入排序和歸併排序的有點:它空間複雜度是O(1), 時間複雜度是O(nlgn).算法

要講堆排序,先講數據結構「堆」api

堆:

  堆是用數組來存放一個徹底二叉樹的數據結構。假設數組名是A,樹的根節點存放在A[1]。它的左孩子存放在A[2],右孩子存放在A[3]數組

  即:對於某個下標位i的節點,它的左孩子是A[2i],  右孩子是A[2i+1].  父節點是A[i/2]數據結構

PARENT(i)
   return i/2⌋ LEFT(i) return 2i RIGHT(i) return 2i + 1


這個結論很簡單也很好記憶,只是有一個小問題:算法導論的數組下標是從1開始的。而現實中大部分流行語言的數組下標倒是從0開始的。這裏有一個小技巧是將A[0]元素保留不使用,就能夠規避掉這個問題。

不過在C++的標準庫STL,或者是Golang的container/heap/heap.go裏,並無使用這個小技巧。
公式調整爲
PARENT(i)
   return ⌊(i-1)/2⌋ LEFT(i) return 2i + 1 RIGHT(i) return 2i + 2
不建議記這個公式,會形成混亂。只須要知道有這麼一回事就行。

堆有兩個應用:
  1. 堆排序時使用的是最大堆:除了根節點之外的每一個節點 A[PARENT(i)] >=A[i]
  2. 優先級隊列使用的是最小堆:除了根節點之外的每一個節點 A[PARENT(i)] <=A[i]

  堆的基本函數有:函數

   max_heapify。它是保持最大堆性質的關鍵函數。運行時間是o(lgn);ui

 

   build_max_heap 以線性時間運行,能夠在無序的輸入數組基礎上構建出最大堆spa

   heapsort 運行時間是O(nlgn), 能夠對一個數組進行原地排序。code

   max_heap_insert, heap_extract_max, heap_increase_keyheap_maximum運行時間爲O(lgn),可讓堆結構做爲優先隊列使用。orm

書上遞歸版本的max_heapifyblog

#define PARENT(i) ((i)/2)
#define LEFT(i) (2*(i))
#define RIGHT(i) (2*(i)+1)
void max_heapify(int* A, int heap_size, int i) {
  int l = LEFT(i);
  int r = RIGHT(i);
  int largest = 0;
  if ((l <= heap_size) && (A[l] > A[i])) {
    largest = l;
  }else {
    largest = i;
  }

  if ((r <= heap_size) && A[r] > (A[largest])) {
    largest = r;
  }

  if (largest != i) {
    int temp = A[i];
    A[i] = A[largest];
    A[largest] = temp;
    max_heapify(A,heap_size,largest);
  }
}

在算法的每一步裏,從元素A[i], A[LEFT(i)], A[RIGHT(i)]中找出最大的。並將其下標存放在largest中。若是A[i]是最大的,則覺得跟的子樹已是最大堆,程序結束。

不然i的某個子節點中有最大元素,則交換 A[i]和A[largest],從而使i及其子女知足堆性質。下標largest的節點在交換後的值是A[i],以該節點爲根的字數又有可能違反最大堆性質,所以要對子樹遞歸調用max_heapify。遞歸調用的次數是樹的高度。而徹底二叉樹的高度是lgn, 因此該算法的時間複雜度是O(lgn)

 

 

max_heapify的效率叫高,可是它使用了遞歸結構,可能會使某些編譯程序產生低效的代碼。所以有必要改爲迭代方式。

修改其實很簡單:

//保持最大堆的有序性
void max_heapify2(int *A, int heap_size, unsigned int i)
{
  while ( i < heap_size) {
    unsigned int l, r, largest;
    largest = i;
    l = lchild(i);
    r = rchild(i);

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

    if (r <= heap_size && A[r] > A[largest])
    {
        largest = r;
    }

    if (i != largest)
    {
        int temp;
        temp = A[i];
        A[i] = A[largest];
        A[largest] = temp;
     i = largest; }
else { return; } }
該算法的時間複雜度是O(lgn)


建堆:
void build_max_heap(int *A, int heap_size) {
  int i;
  for (i = heap_size / 2; i >0; i--) {
    max_heapify(A, heap_size, i);
  }
}

 

爲了證實算法是正確的,咱們用循環不變式來分析一下: 循環中不變的量是每一次迭代開始時,節點i+1, i+2,...,heap_size都是一個最大堆的根。

  1. 初始化:在第一輪循環迭代以前,i=(heap_size/2)。 節點(heap_size/2)+1, (heap_size/2) + 2...,heap_size都是葉節點,也是平凡最大堆的根。
  2. 保持:節點i的子節點的編號均比i大。因而根據循環不變式這些子節點都是最大堆的根。着也是調用函數max_heapify以是節點i成爲最大堆的根的前提條件。此外,max_heapify的調用保持了節點i+1,i+2,。。。。。n成爲最大堆的根的性質。在循環中遞減,記爲下一次迭代從新創建了循環不變式。
  3. 終止:過程終止時,i=0根據循環不變式,咱們知道節點1,2,...n中,每一個都是最大堆的根,節點1就是一個最大堆的根。

該算法的時間複雜度是O(n)

 

 

堆排序:

開始時,對排序算法先用build_max_heap將輸入數組A[1..n]構造陳關一個最大堆。所以數組中最大的元素在根A[1], 則能夠經過它與A[n]互換來達到最終正確的位置。

若是從堆中去掉節點n,能夠很容易將A[1...n-1]建成最大堆,原來根的子女還是最大堆。堆排算法不斷重複這個過程,堆的大小有n-1一直降到2

void heap_sort(int *A)
{
    int temp;
    int i;
    build_max_heap(A);

    for (i = heap_size - 1; i >= 2; i--)
    {
        temp = A[1];
        A[1] = A[i];
        A[i] = temp;
        heap_size--;
        max_heapify(A, heap_size, 1);
    }
}
相關文章
相關標籤/搜索