排序演算法(七)——堆排序
基本思想
堆排序是一種樹形選擇排序,是對直接選擇排序的改進。
首先,我們來看看什麼是堆(heap):
(1)堆中某個節點的值總是不大於或不小於其父節點的值;
(2)堆總是一棵完全二叉樹(Complete Binary Tree)。
完全二叉樹是由滿二叉樹(Full Binary Tree)而引出來的。除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點的二叉樹稱為滿二叉樹。
如果除最後一層外,每一層上的節點數均達到最大值;在最後一層上只缺少右邊的若干結點,這樣的二叉樹被稱為完全二叉樹。
一棵完全二叉樹,如果某個節點的值總是不小於其父節點的值,則根節點的關鍵字是所有節點關鍵字中最小的,稱為小根堆(小頂堆)
;如果某個節點的值總是不大於其父節點的值,則根節點的關鍵字是所有節點關鍵字中最大的,稱為大根堆(大頂堆)。從根節點開始,按照每層從左到右的順序對堆的節點進行編號:
可以發現,如果某個節點的編號為i,則它的子節點的編號分別為:2i、2i+1。據此,推出堆的數學定義:
具有n個元素的序列(k1,k2,...,kn),當且僅當滿足
時稱之為堆。
需要注意的是,堆只對父子節點做了約束,並沒有對兄弟節點做任何約束,左子節點與右子節點沒有必然的大小關係。
如果用陣列儲存堆中的資料,邏輯結構與儲存結構如下:
初始時把要排序的n個數看作是一棵順序儲存的完全二叉樹,調整它們的儲存順序,使之成為一個堆,將堆頂元素輸出,得到n 個元素中最小(最大)的元素,這時堆的根節點的數最小(或者最大)。然後對前面(n-1)個元素重新調整使之成為堆,輸出堆頂元素,得到n 個元素中次小(或次大)的元素。依次類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。這個過程就稱為堆排序
。寫程式碼之前,我們要解決一個問題:如何將一個不是堆的完全二叉樹調整為堆。
例如我們要將這樣一個無序序列:
49,38,65,97,76,13,27,49
建成堆,將它直接對映成二叉樹,結果如下圖的(a):
(a)是一個完全二叉樹,但不是堆。我們將它調整為小頂堆。
堆有一個性質是:堆的每個子樹也是堆。
調整的核心思想就是讓樹的每棵子樹都成為堆,以某節點與它的左子節點、右子節點為操作單位,將三者中最小的元素置於子樹的根上。
(a)中最後一個元素是49,在樹中的序號為8,對應的陣列下標則為7,它的父節點對應的陣列下標為3(如果一個元素對應的儲存陣列的下標為i,則它的父節點對應的儲存陣列的下標為(i-1)/2),49小於97,所以兩者交換位置。
此時,以第三層元素為根節點的所有子樹都已是堆了,下一步繼續調整以第二層元素為根節點的子樹。
先調整以65為根的子樹,再調整以38為根的子樹(滿足堆的要求,實際上不用調整)。
然後調整以第一層元素為根的子樹,即以49為根,以38為左子節點,以13為右子節點的子樹,交換13與49的位置。
一旦交換位置,就有可能影響本來已經是堆的子樹。13與49交換位置之後,破壞了右子樹,將焦點轉移到49上面來,繼續調整以它為根節點的子樹。如果此次調整又影響了下一層的子樹,繼續調整,直至葉子節點。
以上就是由陣列建堆的過程。
堆建好之後開始排序,堆頂就是最小值,取出放入陣列中的最後一個位置,將堆底(陣列中的最後一個元素)放入堆頂。這一操作會破壞堆,需要將前n-1個元素調整成堆。
然後再取出堆頂,放入陣列的倒數第二個位置,堆底(陣列中的倒數第二個元素)放入堆頂,再將前n-2個元素調整成堆。
按照上面的思路迴圈操作,最終就會將陣列中的元素按降序的順序排列完畢。
如果想要升序排列,利用大頂堆進行類似的操作即可。下面的java實現就是使用大頂堆完成的。
java實現
//堆排序 public void heapSort(){ buildHeap(); System.out.println("建堆:"); printTree(array.length); int lastIndex = array.length-1; while(lastIndex>0){ swap(0,lastIndex); //取出堆頂元素,將堆底放入堆頂。其實就是交換下標為0與lastIndex的資料 if(--lastIndex == 0) break; //只有一個元素時就不用調整堆了,排序結束 adjustHeap(0,lastIndex); //調整堆 System.out.println("調整堆:"); printTree(lastIndex+1); } } /** * 用陣列中的元素建堆 */ private void buildHeap(){ int lastIndex = array.length-1; for(inti= (lastIndex-1)/2;i>=0;i--){ //(lastIndex-1)/2就是最後一個元素的根節點的下標,依次調整每棵子樹 adjustHeap(i,lastIndex); //調整以下標i的元素為根的子樹 } } /** * 調整以下標是rootIndex的元素為根的子樹 *@param rootIndex 根的下標 *@param lastIndex 堆中最後一個元素的下標 */ private void adjustHeap(int rootIndex,intlastIndex){ int biggerIndex = rootIndex; int leftChildIndex = 2*rootIndex+1; int rightChildIndex = 2*rootIndex+2; if(rightChildIndex<=lastIndex){ //存在右子節點,則必存在左子節點 if(array[rootIndex]<array[leftChildIndex] || array[rootIndex]<array[rightChildIndex]){ //子節點中存在比根更大的元素 biggerIndex = array[leftChildIndex]<array[rightChildIndex] ? rightChildIndex :leftChildIndex; } }else if(leftChildIndex<=lastIndex){ //只存在左子節點 if(array[leftChildIndex]>array[rootIndex]){ //左子節點更大 biggerIndex = leftChildIndex; } } if(biggerIndex != rootIndex){ //找到了比根更大的子節點 swap(rootIndex,biggerIndex); //交換位置後可能會破壞子樹,將焦點轉向交換了位置的子節點,調整以它為根的子樹 adjustHeap(biggerIndex,lastIndex); } } /** * 將陣列按照完全二叉樹的形式打印出來 */ private void printTree(int len){ int layers = (int)Math.floor(Math.log((double)len)/Math.log((double)2))+1; //樹的層數 int maxWidth = (int)Math.pow(2,layers)-1; //樹的最大寬度 int endSpacing = maxWidth; int spacing; int numberOfThisLayer; for(int i=1;i<=layers;i++){ //從第一層開始,逐層列印 endSpacing = endSpacing/2; //每層列印之前需要列印的空格數 spacing = 2*endSpacing+1; //元素之間應該列印的空格數 numberOfThisLayer = (int)Math.pow(2, i-1); //該層要列印的元素總數 int j; for(j=0;j<endSpacing;j++){ System.out.print(" "); } int beginIndex = (int)Math.pow(2,i-1)-1; //該層第一個元素對應的陣列下標 for(j=1;j<=numberOfThisLayer;j++){ System.out.print(array[beginIndex++]+""); for(intk=0;k<spacing;k++){ //列印元素之間的空格 System.out.print(" "); } if(beginIndex == len){ //已列印到最後一個元素 break; } } System.out.println(); } System.out.println(); }
用以下程式碼測試:
int [] a = {7,1,9,2,5,10,6,4,3,8}; Sort sort = new Sort(a); System.out.println("未排序時:"); sort.display(); System.out.println(); sort.heapSort(); System.out.println("排序完成:"); sort.display();
列印結果如下:
演算法分析
它的執行時間主要是消耗在初始構建堆和在重建堆時的反覆篩選上。
在構建堆的過程中,因為我們是完全二叉樹從最下層最右邊的非終端結點開始構建,將它與其孩子進行比較和若有必要的互換,對於每個非終端結點來說,其實最多進行兩次比較和互換操作,因此整個構建堆的時間複雜度為O(n)。
在正式排序時,第i次取堆頂記錄重建堆需要用O(logi)的時間(完全二叉樹的某個結點到根結點的距離為log2i+1),並且需要取n-1次堆頂記錄,因此,重建堆的時間複雜度為O(nlogn)。
所以總體來說,堆排序的時間複雜度為O(nlogn)。由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞和平均時間複雜度均為O(nlogn)。這在效能上顯然要遠遠好過於冒泡、簡單選擇、直接插入的O(n2)的時間複雜度了。
空間複雜度上,它只有一個用來交換的暫存單元,也非常的不錯。不過由於記錄的比較與交換是跳躍式進行,因此堆排序是一種不穩定的排序方法。
相關推薦
java排序演算法(七)------堆排序
堆排序 程式碼實現: public static void sort01(int[] arr) { for (int i = 0; i < arr.length; i++) { headAdust01(arr, arr.length - 1
排序演算法(七)——堆排序
基本思想堆排序是一種樹形選擇排序,是對直接選擇排序的改進。首先,我們來看看什麼是堆(heap):(1)堆中某個節點的值總是不大於或不小於其父節點的值;(2)堆總是一棵完全二叉樹(Complete Binary Tree)。 完全二叉樹是由滿二叉樹(Full Binary Tr
排序演算法(九)——堆排序
堆排序(Heap Sort)演算法是基於選擇排序思想的演算法,其利用堆結構和二叉樹的一些性質來完成資料的排序。 堆排序演算法的運作如下: (1)將陣列變為一個大頂堆。 (2)因為大頂堆的根結點為陣列中的最大值,然後每次把根結點放到陣列的最後面,並固定住。 (3)以此類推,將前面的數字
排序演算法(七)——歸併排序
歸併排序(Merge Sort)演算法就是將多個有序資料表合併成一個有序資料表。如果參與合併的只有兩個有序表,則稱為二路合併。對於一個原始的待排序序列,往往可以通過分割的方法來歸結為多路合併排序。 合併排序演算法的運作如下: (1)首先將含有n個結點的待排序資料序列看成9個長度為1的有序子表
排序演算法(三)-- 堆排序
堆排序 堆排序演算法結合了插入排序和歸併排序演算法的優點,和插入排序一樣,堆排序不需要額外申請空間。它是一種原地排序的演算法;和歸併排序一樣,堆排序的執行時間也是O(nlgn)。堆排序利用“堆”這種資
排序演算法(5)堆排序
一:基本思想堆排序是利用堆(一種近似完全二叉樹的結構)這種資料結構設計的一種排序演算法(1)由輸入的無序陣列構造一個最大堆,作為初始的無序區(2)把堆頂元素(最大值)和堆尾元素進行互換(3)把堆的尺寸縮小1,並呼叫heapify(A,0)從新的堆頂元素開始進行堆調整(4)重複
第16周專案1 驗證演算法(6)堆排序
問題: /* * Copyright (c)2015,煙臺大學計算機與控制工程學院 * All rights reserved. * 檔名稱:專案1-6.cbp * 作 者:張芸嘉 *
經典排序演算法(1)——氣泡排序演算法詳解
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!  
八大排序演算法(五)——快速排序
快速排序可能是應用最廣泛的排序演算法。快速排序流行的原因是因為它實現簡單、適用於各種不同的輸入資料且在一般應用中比其他排序演算法都要快的多。快速排序的特點包括它是原地排序(只需要一個很小的輔助棧),且將長度為n的陣列排序所需的時間和nlogn成正比。快速排序的內迴圈比大多數排序演算法都要短小,這
排序演算法(5)--快速排序QuickSort
快速排序 時間複雜度: 平均O(nlogn) 最差的情況就是每一次取到的元素就是陣列中最小/最大的,這種情況其實就是氣泡排序了(每一次都排好一個元素的順序) 這種情況時間複雜度,就是氣泡排序的時間複雜度:T[n] = n * (n-1) = n^2 + n; 綜
排序演算法(一)------氣泡排序
氣泡排序 氣泡排序: 兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序記錄為止 氣泡排序是將比較大的數字沉在最下面,較小的浮在上面 最簡單的氣泡排序 /* * 嚴格意義上說不滿足氣泡排序思想,應該是最簡單的交換排序而已 * 思路:讓每一個關鍵字
java排序演算法(二)------插入排序
插入排序 直接插入排序基本思想: 每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完所有元素為止。 public static void sort(int[] arr) { int i; int t; for (int j
java排序演算法(三)------選擇排序
選擇排序 基本思想:每一趟從待排序的資料元素中選擇最小(或最大)的一個元素作為首元素,直到所有元素排完為止,簡單選擇排序是不穩定排序。 選擇排序的時間複雜度和空間複雜度分別為 O(n2 ) 和 O(1) 程式碼實現: public static void s
java排序演算法(四)------歸併排序
歸併排序: 是利用歸併的思想實現的排序方法,該演算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然後遞迴求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。 合
java排序演算法(九)------基數排序
基數排序 程式碼實現: public class RadixSort { public static void sort(int[] a) { int digit = 0;// 陣列的最大位數 for (int i
java排序演算法(二)----選擇排序
選擇排序(Selection Sort) 選擇排序(Selection-sort)是一種簡單直觀的排序演算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從
java 實現 常見排序演算法(三)快速排序
大家好,我是烤鴨: 今天分享一下基礎排序演算法之快速排序。快速排序是內部排序(基於比較排序)中最好的比較演算法。 1. 快速排序:
java 實現 常見排序演算法(二) 插入排序
大家好,我是烤鴨: 今天分享一下基礎排序演算法之直接插入排序。 1. 直接插入排序: 原理:假設前面的數為有序數列,然後有序數列與無序數列的每個數比較,我們可
Java排序演算法(十)--桶排序
前面的1~8介紹的都是基礎的排序的演算法,現在來介紹一種高效的排序演算法–桶排序。 桶排序的原理是:將陣列分到有限數量的桶子裡。每個桶子再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣
排序演算法(一)氣泡排序,簡單選擇排序,直接插入排序,希爾排序
氣泡排序,簡單選擇排序,直接插入排序是三種複雜度為O(n2)的演算法,希爾排序在特殊增量序列的時候可以獲得複雜度為O(n3/2) 氣泡排序 1、最簡單的排序實現 這裡把每個數和這個數之後的每個數比較,大於就交換位置。 缺點:多出了很多次沒有用的交