堆排序時間複雜度:
一、定義
定義只有兩條:
- 堆是一個完全二叉樹;
除了最後一層外,其它節點都是滿的,最後一層節點全部靠左排列,完全二叉樹非常適合使用數組存儲
- 堆中每一個節點的值都必須大於等於(或小於等於)其子樹中每個節點的值。
等價於“堆中每個節點的值都大於等於(或小於等於)其左子節點和右子節點的值”,大於等於的叫大頂堆,小於等於的叫小頂堆。
二、如何實現一個堆
首先確定如何存儲一個堆 和 堆支持哪些操作
存儲堆
使用數組存儲完全二叉樹,不需要存儲左右子節點的指針,只需要通過數組的下標就可以找到左右子節點和父節點。所以用數組存儲堆。
假設堆頂元素存儲在數組下標爲 1 的位置,那麼下標爲的節點的相關節點位置爲:
- 左子節點爲
- 右子節點爲
- 父節點爲
堆的基本操作
主要有插入元素,刪除堆頂元素
插入元素
假設我們將新插入的元素放在堆的最後,插入元素後需要繼續滿足堆的定義,就需要對堆進行調整,這個過程叫堆化。
堆化分爲自上而下和自下而上
自下而上即讓新插入的節點與父節點比對大小,如果不滿足子節點小於等於父節點的大小關係就互換兩個節點,一直重複,直到滿足關係。
刪除堆頂元素
假設堆爲大頂堆,刪除之後就需要將左右子節點較大的元素放入父節點,迭代刪除,但是這樣會出現空洞,所以我們可以先將堆的最後一個元素放入堆頂,然後利用同樣的父子節點比對方法進行互換,這就是自上而下的的堆化方法。
那堆的這兩個基本操作的時間複雜度是多少?堆化是順着節點所在路徑比較交換,時間複雜度與樹的高度成正比,樹的高度不超過,所以時間複雜度爲
三、堆排序
堆排序的步驟爲建堆和排序
1.建堆
將數組原地建成一個堆
兩種思路:
- 藉助堆的插入方法,假設起初堆中只包含一個元素,下標爲1,之後插入下標爲2的元素,再插入下標爲3的元素…直到將包含n個元素的數組組成了堆,這樣就是從前往後處理數組元素,自下而上堆化。
- 另外的思路是從後往前處理數組元素,自上而下堆化
因爲葉子節點往下堆化只能與自己比較,所以我們從第一個非葉子節點(下標爲)開始依次堆化,下標大於的是葉子節點,不需要堆化。建堆過程的時間複雜度是O(n)
2. 排序
建堆之後,數據就是按照大頂堆的特性來組織的,我們將堆頂元素與堆最後一個元素交換,即將最大元素放到堆的最後,類似於刪除了堆頂元素,將剩下n-1個重新建堆,再取堆頂元素,放到n-1個元素的最後,直到堆中只剩下標爲1的一個元素。排序每次維護堆的時間複雜度爲O(logn),進行了n-1次,所以時間複雜度爲O(nlogn)。
堆排序是原地排序,並不是穩定的排序算法,排序過程中堆的每一個節點要與堆頂節點互換。
如果堆在數組中從0開始存儲,那麼相關節點的下標爲:
- 左節點下標爲
- 右節點下標爲
- 父節點下標爲
3. 堆排序的缺點
實際開發中快排要比堆排序好,有以下原因:
- 快排局部順序訪問,而堆排序依次訪問的下標是指數增長的,如1,2,4,8,。。。這樣對CPU緩存不友好
- 對於同樣的順序,堆排序的數據交換次數多於快速排序。快排的數據交換次數不會比逆序度多,而快排第一步的建堆過程會打亂數據的先後順序,導致原有數據有序度降低。
堆的應用
主要有:優先隊列,求Top K 和求中位數
優先隊列
堆和優先隊列非常相似,一個堆就可以看做一個優先隊列
優先隊列的具體應用有合併有序小文件和高性能定時器
求TOP K
針對靜態數據集合,可以維護一個大小爲K的小頂堆,順序遍歷數組,從數組中取出數據與堆頂元素比較,如果比堆頂大,則把堆頂元素刪除,並把這個元素插入堆,如果比堆頂元素小,則不作處理。時間複雜度是O(nlogK)。
針對動態數據集合,我們同樣維護一個大小爲K的小頂堆,當數據被添加到集合時,與堆頂元素比較,決定是非刪除堆頂元素併入堆,
利用堆求中位數
對於靜態數據,先排序再確定中位數,但是對於動態數據集合,每次排序的成本太高。
我們可以維護兩個堆,一個大頂堆,一個小頂堆,大頂堆中存儲前半部分數據,小頂堆中存儲後半部分數據,小頂堆中數據都大於大頂堆的數據,這樣堆頂元素就有一個是中位數,當n是奇數時,大頂堆存儲個數據,這樣大頂堆堆頂就是中位數
數據是動態變化,當添加一個元素a時,當a小於大頂堆堆頂我們就將a插入大頂堆,否則插入小頂堆,這樣就有可能出現兩個堆中數據不符合前面約定,我們將一個堆的堆頂插入另一個堆直到滿足要求即可。此時插入數據的時間複雜度爲O(logn),取出中位數的時間複雜度爲O(1),求其它百分位數據是同樣道理