堆是一種特殊的樹,只要知足下面兩個條件,它就是一個堆:數組
(1)堆是一顆徹底二叉樹;code
(2)堆中某個節點的值老是不大於(或不小於)其父節點的值。排序
其中,咱們把根節點最大的堆叫作大頂堆,根節點最小的堆叫作小頂堆。源碼
滿二叉樹是指全部層都達到最大節點數的二叉樹。好比,下面這顆樹:變量
徹底二叉樹是指除了最後一層其它層都達到最大節點數,且最後一層節點都靠左排列。好比,下面這顆樹:定時任務
可見,其實滿二叉樹是一種特殊的徹底二叉樹。二叉樹
那麼,使用什麼結構存儲徹底二叉樹最節省空間呢?im
咱們能夠看見,徹底二叉樹的節點都是比較緊湊的,且只有最後一層是不滿的,因此使用數組是最節省空間的,好比上面這顆徹底二叉樹咱們能夠這樣存儲。總結
咱們下標爲0的位置不存儲元素,從下標爲1的位置開始存儲元素,每層依次從左往右放到數組裏來存儲。img
爲何下標0的位置不存在元素呢?
這是由於這樣存儲咱們能夠很方便地找到父節點,好比,4的父節點即4/2=2,5的父節點即5/2=2。
堆也是一顆徹底二叉樹,可是它的元素必須知足每一個節點的值都不大於(或不小於)其父節點的值。好比下面這個堆:
前面咱們說過徹底二叉樹適合使用數組來存儲,那上面這個堆應該怎麼存儲呢?
一樣地,咱們下標爲0的位置不存在元素,最後就變成下面這樣。
這時候咱們要找8的父節點就拿8的位置下標5/2=2,也就是5這個節點的位置,這也是爲了咱們後面堆化。
往堆中插入一個元素後,咱們須要繼續知足堆的兩個特性,即:
(1)堆是一顆徹底二叉樹;
(2)堆中某個節點的值老是不大於(或不小於)其父節點的值。
爲了知足條件(1),因此咱們把元素插入到最後一層最後一個節點日後一位的位置,可是插入以後可能再也不知足條件(2)了,因此這時候咱們須要堆化。
好比,上面那個堆咱們須要插入元素2,咱們把它放在9後面,這時不知足條件(2)了,咱們就須要堆化。(這是一個小頂堆)
將徹底二叉樹和數組對照着來看。
在徹底二叉樹中,插入的節點與它的父節點相比,若是比父節點小,就交換它們的位置,再往上和父節點相比,若是比父節點小,再交換位置,直到比父節點大爲止。
在數組中,插入的節點與n/2位置的節點相比,若是比n/2位置的節點小,就交換它們的位置,再往前與n/4位置的節點相比,若是比n/4位置的節點小,再交換位置,直到比n/(2^x)位置的節點大爲止。
這就是插入元素時進行的堆化,也叫自下而上的堆化。
從插入元素的過程,咱們知道每次與n/(2^x)的位置進行比較,因此,插入元素的時間複雜度爲O(log n)。
咱們知道,在小頂堆中堆頂存儲的是最小的元素,這時候咱們把它刪除會怎樣呢?
刪除了堆頂元素後,要使得還知足堆的兩個特性,首先,咱們能夠把最後一個元素移到根節點的位置,這時候就知足條件(1),以後就是使它知足條件(2),就須要堆化了。
將徹底二叉樹和數組對照着來看。
在徹底二叉樹中,把最後一個節點放到堆頂,而後與左右子節點中小的交換位置(由於是小頂堆),依次往下,直到其比左右子節點都小爲止。
在數組中,把最後一個元素移到下標爲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)最後,所有插入完成,即完成了建堆的過程。
咱們知道,徹底二叉樹的高度h=log n,且第h層有1個元素,第(h-1)層有2個元素,第(h-2)層有2^2個元素,...,第1層有2^(h-1)個元素。
其實,建堆的整個過程當中一個節點的比較次數是與它的高度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次。
於是,總和就以下圖:
因此,建堆的時間複雜度就是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時,就平衡一下,這樣全部元素都放完以後,兩個堆頂的元素之一(或之二)就是中位數。
歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。