拜託,面試別再問我堆(排序)了!

何爲堆?

堆是一種特殊的樹,只要知足下面兩個條件,它就是一個堆:數組

(1)堆是一顆徹底二叉樹;code

(2)堆中某個節點的值老是不大於(或不小於)其父節點的值。排序

其中,咱們把根節點最大的堆叫作大頂堆,根節點最小的堆叫作小頂堆。源碼

堆詳解

滿二叉樹

滿二叉樹是指全部層都達到最大節點數的二叉樹。好比,下面這顆樹:變量

heap1

徹底二叉樹

徹底二叉樹是指除了最後一層其它層都達到最大節點數,且最後一層節點都靠左排列。好比,下面這顆樹:定時任務

heap2

可見,其實滿二叉樹是一種特殊的徹底二叉樹。二叉樹

那麼,使用什麼結構存儲徹底二叉樹最節省空間呢?im

咱們能夠看見,徹底二叉樹的節點都是比較緊湊的,且只有最後一層是不滿的,因此使用數組是最節省空間的,好比上面這顆徹底二叉樹咱們能夠這樣存儲。總結

heap00

咱們下標爲0的位置不存儲元素,從下標爲1的位置開始存儲元素,每層依次從左往右放到數組裏來存儲。img

爲何下標0的位置不存在元素呢?

這是由於這樣存儲咱們能夠很方便地找到父節點,好比,4的父節點即4/2=2,5的父節點即5/2=2。

堆也是一顆徹底二叉樹,可是它的元素必須知足每一個節點的值都不大於(或不小於)其父節點的值。好比下面這個堆:

heap3

前面咱們說過徹底二叉樹適合使用數組來存儲,那上面這個堆應該怎麼存儲呢?

一樣地,咱們下標爲0的位置不存在元素,最後就變成下面這樣。

heap01

這時候咱們要找8的父節點就拿8的位置下標5/2=2,也就是5這個節點的位置,這也是爲了咱們後面堆化。

插入元素

往堆中插入一個元素後,咱們須要繼續知足堆的兩個特性,即:

(1)堆是一顆徹底二叉樹;

(2)堆中某個節點的值老是不大於(或不小於)其父節點的值。

爲了知足條件(1),因此咱們把元素插入到最後一層最後一個節點日後一位的位置,可是插入以後可能再也不知足條件(2)了,因此這時候咱們須要堆化。

好比,上面那個堆咱們須要插入元素2,咱們把它放在9後面,這時不知足條件(2)了,咱們就須要堆化。(這是一個小頂堆)

heap4

將徹底二叉樹和數組對照着來看。

在徹底二叉樹中,插入的節點與它的父節點相比,若是比父節點小,就交換它們的位置,再往上和父節點相比,若是比父節點小,再交換位置,直到比父節點大爲止。

在數組中,插入的節點與n/2位置的節點相比,若是比n/2位置的節點小,就交換它們的位置,再往前與n/4位置的節點相比,若是比n/4位置的節點小,再交換位置,直到比n/(2^x)位置的節點大爲止。

這就是插入元素時進行的堆化,也叫自下而上的堆化。

從插入元素的過程,咱們知道每次與n/(2^x)的位置進行比較,因此,插入元素的時間複雜度爲O(log n)。

刪除堆頂元素

咱們知道,在小頂堆中堆頂存儲的是最小的元素,這時候咱們把它刪除會怎樣呢?

刪除了堆頂元素後,要使得還知足堆的兩個特性,首先,咱們能夠把最後一個元素移到根節點的位置,這時候就知足條件(1),以後就是使它知足條件(2),就須要堆化了。

heap5

將徹底二叉樹和數組對照着來看。

在徹底二叉樹中,把最後一個節點放到堆頂,而後與左右子節點中小的交換位置(由於是小頂堆),依次往下,直到其比左右子節點都小爲止。

在數組中,把最後一個元素移到下標爲1的位置,而後與下標爲2和3的位置對比,發現8比2大,且2是2和3中間最小的,因此與2交換位置;而後再下標爲4和5的位置對比,發現8比5大,且5是5和7中最小的,因此與5交換位置,沒有左右子節點了,堆化結束。

這就是刪除元素時進行的堆化,也叫自上而下的堆化。

從刪除元素的過程,咱們知道把最後一個元素拿到根節點後,每次與2n和(2n+1)位置的元素比較,取其小者,因此,刪除元素的時間複雜度也爲O(log n)。

建堆

假定給定一組亂序的數組,咱們該怎麼建堆呢?

以下圖所示,咱們模擬依次往堆中添加元素。

(1)插入6這個元素,只有一個,不須要比較;

(2)插入8這個元素,比6大,不須要交換;

(3)插入3這個元素,比下標3/2=1的位置上的元素6小,交換位置;

(4)插入2這個元素,比下標4/2=2的位置上的元素8小,交換位置,比下標2/2=1的位置上的元素3小,交換位置;

(5)...

(10)最後,所有插入完成,即完成了建堆的過程。

heap6

咱們知道,徹底二叉樹的高度h=log n,且第h層有1個元素,第(h-1)層有2個元素,第(h-2)層有2^2個元素,...,第1層有2^(h-1)個元素。

heap7

其實,建堆的整個過程當中一個節點的比較次數是與它的高度k成正比的,好比,上圖中的1這個元素,它也是從最後一層依次比較了3次(高度h=4),纔到達瞭如今的位置。

因此,咱們能夠得出第h層的元素有1個,它最多須要比較(h-1)次;第(h-1)層有2個元素,它們最多比較(h-2)次;第(h-2)層有2^2個元素,它們最多比較(h-3)次;...;第1層有2^(h-1)個元素,它們最多比較0次。

於是,總和就以下圖:

heap8

因此,建堆的時間複雜度就是O(n)。

堆排序

咱們知道,對於小頂堆,堆頂存儲的元素就是最小的。

那麼,咱們刪除堆頂元素,堆化,第二小的跑堆頂了,再刪除,再堆化,...,這些刪除的元素是否是正好有序的?

固然是的,因此堆排序的過程就很簡單了。

咱們直接把堆頂的元素與第n個元素交換位置,再把前(n-1)個元素堆化,再把堆頂元素與第(n-1)個元素交換位置,再把前(n-2)個元素堆化,..,,進行下去,最後,數組中的元素就整個變成倒序的了,也就排序完了。

咱們知道刪除一個元素的時間複雜度是O(log n),那麼刪除n個元素正好是:

log n + log(n-1) + log(n-2) + log 1

這個公式約等於nlog n,因此堆排序的時間複雜度爲O(nlog n)。

並且,這樣排序不須要佔用額外的空間,只須要交換元素的須要一個臨時變量,因此堆排序的空間複雜度爲O(1)​。​

總結

(1)堆是一顆徹底二叉樹;

(2)小(大)頂堆中的每個節點都不小於(不大於)它的父節點;

(3)堆的插入、刪除元素的時間複雜度都是O(log n);

(4)建堆的時間複雜度是O(n);

(5)堆排序的時間複雜度是O(nlog n);

(6)堆排序的空間複雜度是O(1)​;​

彩蛋

堆都有哪些應用呢?

其實,堆除了堆排序之外,還有不少其它的用途,好比求中位數,99%位數,定時任務等。

好比,求中位數的大體思路,是分別創建一個大頂堆和一個小頂堆,而後往這兩個堆中放元素,當其中一個堆的元素個數比另一個多2時,就平衡一下,這樣全部元素都放完以後,兩個堆頂的元素之一(或之二)就是中位數。


歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。

qrcode

相關文章
相關標籤/搜索