本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
前面幾節介紹了Java中的基本容器類,每一個容器類背後都有一種數據結構,ArrayList是動態數組,LinkedList是鏈表,HashMap/HashSet是哈希表,TreeMap/TreeSet是紅黑樹,本節介紹另外一種數據結構 - 堆。java
以前咱們提到過堆,那裏,堆指的是內存中的區域,保存動態分配的對象,與棧相對應。這裏的堆是一種數據結構,與內存區域和分配無關。算法
堆是什麼結構呢?這個咱們待會再細看。咱們先來講明,堆有什麼用?爲何要介紹它?編程
堆能夠很是高效方便的解決不少問題,好比說:api
堆還能夠實現排序,稱之爲堆排序,不過有比它更好的排序算法,因此,咱們就不介紹其在排序中的應用了。數組
Java容器中有一個類PriorityQueue,就表示優先級隊列,它實現了堆,下節咱們會詳細介紹。關於後面兩個問題,它們是如何使用堆高效解決的,咱們會在接下來的幾節中用代碼實現並詳細解釋。微信
說了這麼多好處,堆究竟是什麼呢?數據結構
堆首先是一顆二叉樹,但它是徹底二叉樹。什麼是徹底二叉樹呢?咱們先來看另外一個類似的概念,滿二叉樹。spa
滿二叉樹是指,除了最後一層外,每一個節點都有兩個孩子,而最後一層都是葉子節點,都沒有孩子。好比,下圖兩個二叉樹都是滿二叉樹。3d
滿二叉樹必定是徹底二叉樹,但徹底二叉樹不要求最後一層是滿的,但若是不滿,則要求全部節點必須集中在最左邊,從左到右是連續的,中間不能有空的。好比說,下面幾個二叉樹都是徹底二叉樹:
而下面的這幾個則都不是徹底二叉樹:
在徹底二叉樹中,能夠給每一個節點一個編號,編號從1開始連續遞增,從上到下,從左到右,以下圖所示:
徹底二叉樹有一個重要的特色,給定任意一個節點,能夠根據其編號直接快速計算出其父節點和孩子節點編號,若是編號爲i,則父節點編號即爲i/2,左孩子編號即爲2*i
,右孩子編號即爲2*i+1
。好比,對於5號節點,父節點爲5/2即2,左孩子爲2*5
即10,右孩子爲2*5+1
即11。
這個特色爲何重要呢?它使得邏輯概念上的二叉樹能夠方便的存儲到數組中,數組中的元素索引就對應節點的編號,樹中的父子關係經過其索引關係隱含維持,不須要單獨保持。好比說,上圖中的邏輯二叉樹,保存到數組中,其結構爲:
父子關係是隱含的,好比對於第5個元素13,其父節點就是第2個元素15,左孩子就是第10個元素7,右孩子就是第11個元素4。這種存儲二叉樹的方法與以前介紹的TreeMap是不同的,在TreeMap中,有一個單獨的內部類Entry,Entry有三個引用,分別指向父節點、左孩子、右孩子。
使用數組存儲,優勢是很明顯的,節省空間,訪問效率高。
堆邏輯概念上是一顆徹底二叉樹,而物理存儲上使用數組,除了這兩點,堆還有必定的順序要求。
以前介紹過排序二叉樹,排序二叉樹是徹底有序的,每一個節點都有肯定的前驅和後繼,並且不能有重複元素。
與排序二叉樹不一樣,在堆中,能夠有重複元素,元素間不是徹底有序的,但對於父子節點之間,有必定的順序要求,根據順序分爲兩種堆,一種是最大堆,另外一種是最小堆。
最大堆是指,每一個節點都不大於其父節點。這樣,對每一個父節點,必定不小於其全部孩子節點,而根節點就是全部節點中最大的,對每一個子樹,子樹的根也是子樹全部節點中最大的。
最小堆與最大堆正好相反,每一個節點都不小於其父節點。這樣,對每一個父節點,必定不大於其全部孩子節點,而根節點就是全部節點中最小的,對每一個子樹,子樹的根也是子樹全部節點中最小的。
咱們看下圖示:
總結來講,邏輯概念上,堆是徹底二叉樹,父子節點間有特定順序,分爲最大堆和最小堆,最大堆根是最大的,最小堆根是最小的,堆使用數組進行物理存儲。
這個數據結構爲何就能夠高效的解決以前咱們說的問題呢?在回答以前,咱們須要先看下,如何在堆上進行數據的基本操做,在操做過程當中,如何保持堆的屬性不變。
下面,咱們來看下,如何在堆上進行數據的基本操做。最大堆和最小堆的算法是相似的,咱們以最小堆來講明。先來看如何添加元素。
若是堆爲空,則直接添加一個根就好了。咱們假定已經有一個堆了,要在其中添加元素。基本步驟爲:
咱們來看個例子。下面是初始結構:
添加元素3,第一步後,結構變爲:
3小於父節點8,不知足最小堆的性質,因此與父節點交換,會變爲:
交換後,3仍是小於父節點6,因此繼續交換,會變爲:
交換後,3仍是小於父節點,也是根節點4,繼續交換,變爲:
這時,調整就結束了,樹保持了堆的性質。
從以上過程能夠看出,添加一個元素,須要比較和交換的次數最多爲樹的高度,即log2(N),N爲節點數。
這種自低向上比較、交換,使得樹從新知足堆的性質的過程,咱們稱之爲siftup。
在隊列中,通常是從頭部刪除元素,Java中用堆實現優先級隊列,咱們來看下如何在堆中刪除頭部,其基本步驟爲:
咱們來看個例子。下面是初始結構:
執行第一步,用最後元素替換頭部,會變爲:
如今根節點16大於孩子節點,與更小的孩子節點6進行替換,結構會變爲:
16仍是大於孩子節點,與更小的孩子8進行交換,結構會變爲:
此時,就知足堆的性質了。
那若是須要從中間刪除某個節點呢?與從頭部刪除同樣,都是先用最後一個元素替換待刪元素。不過替換後,有兩種狀況,若是該元素大於某孩子節點,則需向下調整(siftdown),不然,若是小於父節點,則需向上調整(siftup)。
咱們來看個例子,刪除值爲21的節點,第一步以下圖所示:
替換後,6沒有子節點,小於父節點12,執行向上調整siftup過程,最後結果爲:
咱們再來看個例子,刪除值爲9的節點,第一步以下圖所示:
交換後,11大於右孩子10,因此執行siftdown過程,執行結束後爲:
給定一個無序數組,如何使之成爲一個最小堆呢?將普通無序數組變爲堆的過程咱們稱之爲heapify。
基本思路是,從最後一個非葉子節點開始,一直往前直到根,對每一個節點,執行向下調整siftdown。換句話說,是自底向上,先使每一個最小子樹爲堆,而後每對左右子樹和其父節點合併,調整爲更大的堆,由於每一個子樹已經爲堆,因此調整就是對父節點執行siftdown,就這樣一直合併調整直到根。這個算法的僞代碼是:
void heapify() {
for (int i=size/2; i >= 1; i--)
siftdown(i);
}
複製代碼
size表示節點個數, 節點編號從1開始,size/2表示第一個非葉節點的編號。
這個構建的時間效率爲O(N),N爲節點個數,具體就不證實了。
在堆中進行查找沒有特殊的算法,就是從數組的頭找到尾,效率爲O(N)。
在堆中進行遍歷也是相似的,堆就是數組,堆的遍歷就是數組的遍歷,第一個元素是最大值或最小值,但後面的元素沒有特定的順序。
須要說明的是,若是是逐個從頭部刪除元素,堆能夠確保輸出是有序的。
以上就是堆操做的主要算法:
本節介紹了堆這一數據結構的基本概念和算法。
堆是一種比較神奇的數據結構,概念上是樹,存儲爲數組,父子有特殊順序,根是最大值/最小值,構建/添加/刪除效率都很高,能夠高效解決不少問題。
但在Java中,堆究竟是如何實現的呢?本文開頭提到的那些問題,用堆到底如何解決呢?讓咱們在接下來的幾節中繼續探索。
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。