最大堆(建立、刪除、插入和堆排序)

關於最大堆

什麼是最大堆和最小堆?最大(小)堆是指在樹中,存在一個結點並且該結點有兒子結點,該結點的data域值都不小於(大於)其兒子結點的data域值,而且它是一個徹底二叉樹(不是滿二叉樹)。注意區分選擇樹,由於選擇樹(selection tree)概念和最小堆有些相似,他們都有一個特色是「樹中的根結點都表示樹中的最小元素結點」。同理最大堆的根結點是樹中元素最大的。那麼來看具體的看一下它長什麼樣?(最小堆這裏省略)node

 
圖-1

 

這裏須要注意的是:在多個子樹中,並非說其中一個子樹的父結點必定大於另外一個子樹的兒子結點。最大堆是樹結構,並且必定要是徹底二叉樹。數組

最大堆ADT

那麼咱們在作最大堆的抽象數據類型(ADT)時就須要考慮三個操做:
(1)、建立一個最大堆;
(2)、最大堆的插入操做;
(3)、最大堆的刪除操做;
最大堆ADT以下:函數

struct Max_Heap {
  object: 由多個元素組成的徹底二叉樹,其每一個結點都不小於該結點的子結點關鍵字值
  functions:
    其中heap∈Max_Heap,n,max_size∈int,Element爲堆中的元素類型,item∈ Element
    Max_Heap createHeap(max_size)       := 建立一個總容量不大於max_size的空堆
    void max_heap_insert(heap, item ,n) := 插入一個元素到heap中
    Element max_heap_delete(heap,n)     := if(heap不爲空) {return 被刪除的元素 }else{return NULL}
}
///其中:=符號組讀做「定義爲」

最大堆內存表現形式

咱們只是簡單的定義了最大堆的ADT,爲了可以用代碼實現它就必需要考慮最大堆的內存表現形式。從最大堆的定義中,咱們知道不論是對最大堆作插入仍是刪除操做,咱們必需要保證插入或者刪除完成以後,該二叉樹仍然是一個徹底二叉樹。基於此,咱們就必需要去操做某一個結點的父結點。
  第一種方式,咱們使用鏈表的方式來實現,那麼咱們須要添加一個額外的指針來指向該結點的父結點。此時就包括了左子結點指針、右子結點指針和父結點指針,那麼空鏈的數目有多是很大的,好比葉子結點的左右子結點指針和根結點的父結點指針,因此不選擇這種實現方式(關於用鏈表實現通常二叉樹時處理左右子結點指針的問題在線索二叉樹中有說起)。
  第二種方式,使用數組實現,在二叉樹進行遍歷的方法分爲:先序遍歷、中序遍歷、後序遍歷和層序遍歷。咱們能夠經過層序遍歷的方式將二叉樹結點存儲在數組中,因爲最大堆是徹底二叉樹不會存在數組的空間浪費。那麼來看看層序遍歷是怎麼作的?對下圖的最大堆進行層序遍歷:學習

 
 
 
 
層序遍歷流程變化
從這裏能夠看出最後獲得的順序和上面圖中所標的順序是同樣的。
  那麼對於數組咱們怎麼操做父結點和左右子結點呢?對於徹底二叉樹採用順序存儲表示,那麼對於任意一個下標爲i(1 ≤ i ≤ n)的結點:
(1)、父結點爲: i / 2(i ≠ 1),若i = 1,則i是根節點。
(2)、左子結點: 2i(2i ≤ n), 若不知足則無左子結點。
(3)、右子結點: 2i + 1(2i + 1 ≤ n),若不知足則無右子結點。
 
 

 

最終咱們選擇數組做爲最大堆的內存表現形式。編碼

基本定義:spa

#define MAX_ELEMENTS 20
#define HEAP_FULL(n) (MAX_ELEMENTS - 1 == n)
#define HEAP_EMPTY(n) (!n)
typedef struct {
    int key;
}element;
element heap[MAX_ELEMENTS];

下面來看看最大堆的插入、刪除和建立這三個最基本的操做。指針

最大堆的插入

最大堆的插入操做能夠簡單當作是「結點上浮」。當咱們在向最大堆中插入一個結點咱們必須知足徹底二叉樹的標準,那麼被插入結點的位置的是固定的。並且要知足父結點關鍵字值不小於子結點關鍵字值,那麼咱們就須要去移動父結點和子結點的相互位置關係。具體的位置變化,能夠看看下面我畫的一個簡單的圖。code

void insert_max_heap(element item ,int *n){
    if(HEAP_FULL(*n)){
      return;
    }
    int i = ++(*n);
    for(;(i != 1) && (item.key > heap[i/2].key);i = i / 2){/// i ≠ 1是由於數組的第一個元素並無保存堆結點
      heap[i] = heap[i/2];/// 這裏其實和遞歸操做相似,就是去找父結點
    }
    heap[i] = item;
}

 

 
最大堆的插入

因爲堆是一棵徹底二叉樹,存在n個元素,那麼他的高度爲: log2(n+1),這就說明代碼中的for循環會執行 O(log2(n))次。所以插入函數的時間複雜度爲: O(log2(n))

 

最大堆的刪除

最大堆的刪除操做,老是從堆的根結點刪除元素。一樣根元素被刪除以後爲了可以保證該樹仍是一個徹底二叉樹,咱們須要來移動徹底二叉樹的最後一個結點,讓其繼續符合徹底二叉樹的定義,從這裏能夠看做是最大堆最後一個結點的下沉(也就是下文提到的結點1)操做。例如在下面的最大堆中執行刪除操做:排序

 
 

如今對上面👆最大堆作刪除, 對於最大堆的刪除,咱們不能本身進行選擇刪除某一個結點,咱們只能刪除堆的根結點。(⚠️⚠️⚠️)

 

  • 第一步,咱們刪除上圖中的根結點20;
  • 當刪除根結點20以後明顯不是一個徹底二叉樹,更確切地說被分紅了兩棵樹。
  • 咱們須要移動子樹的某一個結點來充當該樹的根節點,那麼在(15,2,14,10,1)這些結點中移動哪個呢?顯然是移動結點1,若是移動了其餘結點(好比14,10)就再也不是一個徹底二叉樹了。

對上面三步圖示以下:遞歸

 
 
 
 

顯然如今看來該二叉樹雖然是一個徹底二叉樹,可是它並不符合最大堆的相關定義,咱們的目的是要在刪除完成以後,該徹底二叉樹依然是最大堆。所以就須要咱們來作一些相關的操做!

1)、此時在結點(15,2)中選擇較大的一個和1作比較,即15 > 1的,因此15上浮到以前的20的結點處。
2)、同第1步相似,找出(14,10)之間較大的和1作比較,即14>1的,因此14上浮到原來15所處的結點。
3)、由於原來14的結點是葉子結點,因此將1放在原來14所處的結點處。
 
 
element delete_max_heap(int *n){
  int parent, child;
  element temp, item;
  temp = heap[--*n];
  item = heap[1];
  parent = 1,child=2;
  for(;child <= *n; child = child * 2){
   if( (child < *n) && heap[child].key < heap[child+1].key){/// 這一步是爲了看當前結點是左子結點大仍是右子結點大,而後選擇較大的那個子結點
        child++;
      }
      if(temp.key >= heap[child].key){
        break;
      }
      heap[parent] = heap[child];///這就是上圖中第二步和第三步中黃色部分操做
      parent = child;/// 這其實就是一個遞歸操做,讓parent指向當前子樹的根結點
   }
  heap[parent] = temp;
  return item;
}

同最大堆的插入操做相似,一樣包含n個元素的最大堆,其高度爲:log2(n+1),其時間複雜度爲:O(log2(n))

總結:由此能夠看出,在已經肯定的最大堆中作刪除操做,被刪除的元素是固定的,須要被移動的結點也是固定的,這裏我說的被移動的元素是指最初的移動,即最大堆的最後一個元素。移動方式爲從最大的結點開始比較。

最大堆的建立

爲何要把最大堆的建立放在最後來說?由於在堆的建立過程當中,有兩個方法。會分別用到最大堆的插入和最大堆的刪除原理。建立最大堆有兩種方法:
(1)、先建立一個空堆,而後根據元素一個一個去插入結點。因爲插入操做的時間複雜度爲O(log2(n)),那麼n個元素插入進去,總的時間複雜度爲O(n * log2(n))
(2)、將這n個元素先順序放入一個二叉樹中造成一個徹底二叉樹,而後來調整各個結點的位置來知足最大堆的特性。
如今咱們就來試一試第二種方法來建立一個最大堆:假如咱們有12個元素分別爲:

{79,66,43,83,30,87,38,55,91,72,49,9}

將上訴15個數字放入一個二叉樹中,確切地說是放入一個徹底二叉樹中,以下:

 
 

可是這明顯不符合最大堆的定義,因此咱們須要讓該徹底二叉樹轉換成最大堆!怎麼轉換成一個最大堆呢?
  最大堆有一個特色就是 其各個子樹都是一個最大堆,那麼咱們就能夠從把最小子樹轉換成一個最大堆,而後依次轉換它的父節點對應的子樹,直到最後的根節點所在的整個徹底二叉樹變成最大堆。那麼從哪個子樹開始調整?

 

咱們從該徹底二叉樹中的最後一個非葉子節點爲根節點的子樹進行調整,而後依次去找倒數第二個倒數第三個非葉子節點...

具體步驟

在作最大堆的建立具體步驟中,咱們會用到最大堆刪除操做中結點位置互換的原理,即關鍵字值較小的結點會作下沉操做

  • 1)、就如同上面所說找到二叉樹中倒數第一個非葉子結點87,而後看以該非葉子結點爲根結點的子樹。查看該子樹是否知足最大堆要求,很明顯目前該子樹知足最大堆,因此咱們不須要移動結點。該子樹最大移動次數爲1
     
     
  • 2)、如今來到結點30,明顯該子樹不知足最大堆。在該結點的子結點較大的爲72,因此結點72和結點30進行位置互換。該子樹最大移動次數爲1
     
     
  • 3)、一樣對結點83作相似的操做。該子樹最大移動次數爲1
     
     
  • 4)、如今來到結點43,該結點的子結點有{87,38,9},對該子樹作一樣操做。因爲結點43多是其子樹結點中最小的,因此該子樹最大移動次數爲2
     
     
  • 5)、結點66一樣操做,該子樹最大移動次數爲2
     
     
  • 6)、最後來到根結點79,該二叉樹最高深度爲4,因此該子樹最大移動次數爲3
     
     

自此經過上訴步驟建立的最大堆爲:

 
 
 

因此從上面能夠看出,該二叉樹總的須要移動結點次數最大爲:10

代碼實現

void create_max_heap(void){
        int total = (*heap).key;
        /// 求倒數第一個非葉子結點
        int child = 2,parent = 1;
        for (int node = total/2; node>0; node--) {
            parent = node;
            child = 2*node;
            int max_node = 2*node+1;
            element temp = *(heap+parent);
            for (; child <= total; child *= 2,max_node = 2*parent+1) {
                if (child+1 <= total && (*(heap+child)).key < (*(heap+child+1)).key) {
                    child++;
                }
                if (temp.key > (*(heap+child)).key) {
                    break;
                }
                *(heap+parent) = *(heap+child);
                parent = child;
            }
            *(heap+parent) = temp;
        }
    }

/**
 *
 * @param heap  最大堆;
 * @param items 輸入的數據源
 * @return 1成功,0失敗
 */
int create_binary_tree(element *heap,int items[MAX_ELEMENTS]){
    int total;
    if (!items) {
        return 0;
    }
    element *temp = heap;
    heap++;
    for (total = 1; *items;total++,(heap)++,items = items + 1) {
        element ele = {*items};
        element temp_key = {total};
        *temp = temp_key;
        *heap = ele;
    }
    return 1;
}
///函數調用
int items[MAX_ELEMENTS] = {79,66,43,83,30,87,38,55,91,72,49,9};
element *position = heap;
create_binary_tree(position, items);
for (int i = 0; (*(heap+i)).key > 0; i++) {
  printf("binary tree element is %d\n",(*(heap + i)).key);
}
create_max_heap();
for (int i = 0; (*(heap+i)).key > 0; i++) {
  printf("heap element is %d\n",(*(heap + i)).key);
}

上訴代碼在我機器上可以成功的構建一個最大堆。因爲該徹底二叉樹存在n個元素,那麼他的高度爲:log2(n+1),這就說明代碼中的for循環會執行O(log2(n))次。所以其我理解的平均運行時間爲:O(log2(n))。而其上界爲當該二叉樹爲滿二叉樹時其時間複雜度爲O((n)。

堆排序

堆排序要比空間複雜度爲O(n)的歸併排序要慢一些,可是要比空間複雜度爲O(1)的歸併排序要快!
  經過上面最大堆建立一節中咱們可以建立一個最大堆。出於該最大堆太大,我將其進行縮小以便進行畫圖演示。

 
 

最大堆的排序過程實際上是和最大堆的刪除操做相似,因爲最大堆的刪除只能在根結點進行,當將根結點刪除完成以後,就是將剩下的結點進行整理讓其符合最大堆的標準。

 

  • 1)、把最大堆根結點91「刪除」,第一次排序圖示:
     
     

    進過這一次排序以後,91就處在最終的正確位置上,因此咱們只須要對餘下的最大堆進行操做!這裏須要注意一點:

⚠️⚠️⚠️注意,關於對餘下進行最大堆操做時:
並不須要像建立最大堆時,從倒數第一個非葉子結點開始。由於在咱們只是對第一個和最後一個結點進行了交換,因此只有根結點的順序不知足最大堆的約束,咱們只須要對第一個元素進行處理便可

  • 2)、繼續對結點87進行相同的操做:

     
     

    一樣,87的位置肯定。

     

  • 3)、如今咱們來肯定結點83的位置:

     
     

     

  • 4)、通過上訴步驟就不難理解堆排序的原理所在,最後排序結果以下:

     
     
     

通過上訴多個步驟以後,最終的排序結果以下:

[3八、4三、7二、7九、8三、8七、91]

很明顯這是一個正確的從小到大的順序。

編碼實現

這裏須要對上面的代碼進行一些修改!由於在排序中,咱們的第0個元素是不用去放一個哨兵的,咱們的元素從原來的第一個位置改成從第0個位置開始放置元素。

void __swap(element *lhs,element *rhs){
    element temp = *lhs;
    *lhs = *rhs;
    *rhs = temp;
}

int create_binarytree(element *heap, int items[MAX_SIZE], int n){
    if (n <= 0) return 0;
    for (int i = 0; i < n; i++,heap++) {
        element value = {items[i]};
        *heap = value;
    }
    return 1;
}

void adapt_maxheap(element *heap ,int node ,int n){
    int parent = node - 1 < 0 ? 0 : node - 1;
    int child = 2 * parent + 1;/// 由於沒有哨兵,因此在數組中的關係由原來的:parent = 2 * child => parent = 2 * child + 1
    int max_node = max_node = 2*parent+2 < n - 1 ? 2*parent+2 : n - 1;
    element temp = *(heap + parent);
    for (;child <= max_node; parent = child,child = child * 2 + 1,max_node = 2*parent+2 < n - 1 ? 2*parent+2 : n - 1) {
        if ((heap + child)->key <= (heap + child + 1)->key && child + 1 < n) {
            child++;
        }
        if ((heap + child)->key < temp.key) {
            break;
        }
        *(heap + parent) = *(heap + child);
    }
    *(heap + parent) = temp;
}

int create_maxheap(element *heap ,int n){

    for (int node = n/2; node > 0; node--) {
        adapt_maxheap(heap, node, n);
    }
    return 1;
}

void heap_sort(element *heap ,int n){
    ///建立一個最大堆
    create_maxheap(heap, n);
    ///進行排序過程
    int i = n - 1;
    while (i >= 0) {
        __swap(heap+0, heap + i);/// 將第一個和最後一個進行交換
        adapt_maxheap(heap, 0, i--);///將總的元素個數減一,適配成最大堆,這裏只須要對首元素進行最大堆的操做
    }
}

調用:

/// 堆排序
int n = 7;
int items[7] = {87,79,38,83,72,43,91};
element heap[7];
create_binarytree(heap, items, n);
heap_sort(heap, n);///38,43,72,79,83,87,91

在實現堆排序時最須要注意的就是當沒有哨兵以後,父結點和左右孩子結點之間的關係發生了變化:

parent = 2 * child + 1;///左孩子
parent = 2 * child + 2;///右孩子

關於對排序相關的知識點已經整理完了。其時間複雜度和歸併排序的時間時間複雜度是同樣的O(N*LogN)

結束語

當咱們在作和徹底二叉樹有關的操做時,對於徹底二叉樹採用順序存儲表示,須要記住對於任意一個下標爲i(1 ≤ i ≤ n)的結點:父結點爲:i / 2(i ≠ 1),若i = 1,則i是根節點。左子結點:2i(2i ≤ n), 若不知足則無左子結點。右子結點:2i + 1(2i + 1 ≤ n),若不知足則無右子結點。   關於最大堆的相關操做(插入、刪除、建立和排序)已經一一學習完畢。這些操做中,刪除、建立和排序思想很是相似,都是操做結點下沉。而插入操做相反,相似上浮的操做!

相關文章
相關標籤/搜索