【排序算法】— 手写堆排序
原創(chuàng)公眾號:bigsai,碼字不易,如有幫助,記得三聯(lián)!
前言
在個人的專欄中,其他排序陸陸續(xù)續(xù)都已經(jīng)寫了,而堆排序遲遲沒有寫,在國慶假期的尾聲,把堆排序也寫一寫。
插入類排序—(折半)插入排序、希爾排序
交換類排序—冒泡排序、快速排序手撕圖解
歸并類排序—歸并排序(逆序數(shù)問題)
計數(shù)排序引發(fā)的圍觀風(fēng)波——一種O(n)的排序
兩分鐘搞懂桶排序
對于常見的快排、歸并這些O(nlogn)的排序算法,我想大部分人可能很容易搞懂,但是堆排序大部分人可能比較陌生,或許在Java的comparator接口中可能了解一點。但堆排序在應(yīng)用中比如優(yōu)先隊列此類維護(hù)動態(tài)數(shù)據(jù)效率比較高,有著非常廣泛的應(yīng)用。
而堆排序可以拆分成堆和排序,其中你可能對堆比較陌生,對排序比較熟悉,下面就帶你徹底了解相關(guān)內(nèi)容。
堆
什么是堆?
談起堆,很多人第一聯(lián)想到的是土堆,而在數(shù)據(jù)結(jié)構(gòu)中這種土堆與完全二叉樹更像,而堆就是一類特殊的數(shù)據(jù)結(jié)構(gòu)的統(tǒng)稱。堆通常是一個可以被看做一棵樹(完全)的數(shù)組對象。且總是滿足以下規(guī)則:
- 堆總是一棵完全二叉樹
- 每個節(jié)點總是大于(或小于)它的孩子節(jié)點。
完全二叉樹
我想什么是完全二叉樹大部分人也是知道:最后一層以上都是滿的,最后一層節(jié)點從左到右可以排列(有任何空缺即不滿足完全二叉樹)。
看作樹的數(shù)組對象
我們都知道我們排序的對象一般都是對數(shù)組之類的序列進(jìn)行排序,如果轉(zhuǎn)成抽象數(shù)據(jù)結(jié)構(gòu)再實現(xiàn)可能成本比較大。
我們正常在構(gòu)造一棵二叉樹的時候通常采用鏈?zhǔn)絣eft,right節(jié)點,但其實二叉樹的表示方式用數(shù)組也可以實現(xiàn),只不過普通的二叉樹如果用數(shù)組儲存可能空間利用 效率會很低而很少采用,但我們的堆是一顆完全二叉樹。使用數(shù)組儲存空間使用效率也比較高,所以在形式上我們把這個數(shù)組看成對應(yīng)的完全二叉樹,而操作上可以直接操作數(shù)組也比較方便。
大根堆 VS 小根堆
上面還有一點就是在這個完全二叉樹中所有節(jié)點均大于(或小于)它的孩子節(jié)點,所以這里就分為兩種情況
- 如果所有節(jié)點大于孩子節(jié)點值,那么這個堆叫做大根堆,堆的最大值在根節(jié)點。
- 如果所有節(jié)點小于孩子節(jié)點值,那么這個堆叫做小根堆,堆的最小值在根節(jié)點。
堆排序
通過上面的介紹,我想你對堆應(yīng)該有了一定的認(rèn)識,堆排序肯定是借助堆實現(xiàn)的某種排序,其實堆排序的整體思路也很簡單,就是
- 構(gòu)建堆,取堆頂為最小(最大)。
- 將剩下的元素重新構(gòu)建一個堆,取堆頂,一直到元素取完為止。
建堆
如果給一個無序的序列,首先要給它建成一個堆,我們?nèi)绾螌崿F(xiàn)這個操作呢?以下拿一個小根堆為例進(jìn)行分析。
對于二叉樹(數(shù)組表示),我們從下往上進(jìn)行調(diào)整,從第一個非葉子節(jié)點開始向前調(diào)整,對于調(diào)整的規(guī)則如下:
①對于小根堆,當(dāng)前節(jié)點與左右孩子比較,如果均小于左右孩子節(jié)點,那么它本身就是一個小根堆,它不需要做任何改變,如果左右有孩子節(jié)點比它還小,那么就要和最小的那個進(jìn)行替換。
②但是普通節(jié)點替換可能沒問題,對于某些和子節(jié)點替換有可能改變子樹成堆,所以需要繼續(xù)往下判斷交換(最差判斷到葉子節(jié)點)。
分析構(gòu)造堆的這個過程,每個非葉子節(jié)點都需要判斷比較是否交換,這樣一層就是O(n),而每個節(jié)點可能替換之后影響子節(jié)點成堆需要再往下判斷遍歷,你可能會認(rèn)為它是一個O(nlogn),但其實你看看二叉樹性值,大部分都是在底部的,上面的只有很少個數(shù),如果你用數(shù)學(xué)方法去求得最終的復(fù)雜度它還是一個O(n)級別,這里就不作詳細(xì)介紹了。
一個大根堆建立過程也是一樣的:
堆排序
上面的一個堆建造完畢之后,我們怎么去利用這個堆實現(xiàn)排序呢?答案也是很簡單的,我們知道堆有一個特性就是堆頂是最小(或最大),而我們建造這個如果去除第一個元素,剩余左右孩子依然滿足堆的性質(zhì)。
將最后一個元素放置堆頂,由于第一個元素的存在使得整個不滿足堆的性質(zhì)。分析這個結(jié)構(gòu),和我們前面構(gòu)造堆的過程中構(gòu)造到第一個元素的操作相同:
- 判斷左右孩子,如果需要交換則交換,交換后再次考慮交換子節(jié)點是否需要交換。一直到不需要考慮。
這樣到最后,堆排序即可完成,最終得到的序列即為堆排序序列。
一個大根堆的排序過程如下:
具體實現(xiàn)
有了上述的思想之后,如何具體的實現(xiàn)這個堆排序的代碼呢?
從細(xì)致的流程來看,大概流程是如下的:
給定數(shù)組建堆(creatHeap)
- 從第一個非葉子節(jié)點開始判斷交換下移(shiftDown),使得當(dāng)前節(jié)點和子孩子能夠保持堆的性值
- 如果交換打破子孩子堆結(jié)構(gòu)性質(zhì),那么就要重新下移(shiftDown)被交換的節(jié)點一直到停止。
堆構(gòu)造完成,取第一個堆頂元素為最小(最大),剩下左右孩子依然滿足堆的性值,但是缺個堆頂元素,如果給孩子調(diào)上來,可能會調(diào)動太多并且可能破壞堆結(jié)構(gòu)。
- 所以索性把最后一個元素放到第一位。這樣只需要判斷交換下移(shiftDown),不過需要注意此時整個堆的大小已經(jīng)發(fā)生了變化,我們在邏輯上不會使用被拋棄的位置,所以在設(shè)計函數(shù)的時候需要附帶一個堆大小的參數(shù)。
- 重復(fù)以上操作,一直堆中所有元素都被取得停止。
而堆算法復(fù)雜度的分析上,之前建堆時間復(fù)雜度是O(n)。而每次刪除堆頂然后需要向下交換,每個個數(shù)最壞為logn個。這樣復(fù)雜度就為O(nlogn).總的時間復(fù)雜度為O(n)+O(nlogn)=O(nlogn).
具體實現(xiàn)的代碼如下:
import java.util.Arrays;public class 堆排序 {static void swap(int arr[],int m,int n){int team=arr[m];arr[m]=arr[n];arr[n]=team;}//下移交換 把當(dāng)前節(jié)點有效變換成一個堆(小根)static void shiftDown(int arr[],int index,int len)//0 號位置不用{int leftchild=index*2+1;//左孩子int rightchild=index*2+2;//右孩子if(leftchild>=len)return;else if(rightchild<len&&arr[rightchild]<arr[index]&&arr[rightchild]<arr[leftchild])//右孩子在范圍內(nèi)并且應(yīng)該交換{swap(arr, index, rightchild);//交換節(jié)點值shiftDown(arr, rightchild, len);//可能會對孩子節(jié)點的堆有影響,向下重構(gòu)}else if(arr[leftchild]<arr[index])//交換左孩子{swap(arr, index, leftchild);shiftDown(arr, leftchild, len);}}//將數(shù)組創(chuàng)建成堆static void creatHeap(int arr[]){for(int i=arr.length/2;i>=0;i--){shiftDown(arr, i,arr.length);}}static void heapSort(int arr[]){System.out.println("原始數(shù)組為 :"+Arrays.toString(arr));int val[]=new int[arr.length]; //臨時儲存結(jié)果//step1建堆creatHeap(arr);System.out.println("建堆后的序列為 :"+Arrays.toString(arr));//step2 進(jìn)行n次取值建堆,每次取堆頂元素放到val數(shù)組中,最終結(jié)果即為一個遞增排序的序列for(int i=0;i<arr.length;i++){val[i]=arr[0];//將堆頂放入結(jié)果中arr[0]=arr[arr.length-1-i];//刪除堆頂元素,將末尾元素放到堆頂shiftDown(arr, 0, arr.length-i);//將這個堆調(diào)整為合法的小根堆,注意(邏輯上的)長度有變化}//數(shù)值克隆復(fù)制for(int i=0;i<arr.length;i++){arr[i]=val[i];}System.out.println("堆排序后的序列為:"+Arrays.toString(arr));}public static void main(String[] args) {int arr[]= {14,12,16,8,9,1,14,9,6 };heapSort(arr); }}執(zhí)行結(jié)果:
當(dāng)然,代碼為了成章節(jié)我把它命名為中文,還有些不規(guī)范的地方請注意甄別。
對于堆排序就先介紹到這里了,當(dāng)然堆的強(qiáng)大之處不止這么一點,優(yōu)先隊列同樣也是用到堆但是這里就不詳細(xì)介紹了,我相信優(yōu)秀的你肯定又掌握了一門O(nlogn)級別的排序算法啦。如果寫的有啥不確切地方還請指正。
結(jié)語
原創(chuàng)不易,最后我請你幫兩件事幫忙一下:
star支持一下, 您的肯定是我在csdn平臺創(chuàng)作的源源動力。
微信搜索「bigsai」,關(guān)注我的公眾號,不僅免費(fèi)送你電子書,我還會第一時間在公眾號分享知識技術(shù)。加我還可拉你進(jìn)力扣打卡群一起打卡LeetCode。
記得關(guān)注、咱們下次再見!
總結(jié)
以上是生活随笔為你收集整理的【排序算法】— 手写堆排序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LeetCode 38外观数列39组合总
- 下一篇: 回溯算法 | 追忆那些年曾难倒我们的八皇