資料結構之二叉堆
什麼是二叉堆?
二叉堆 本質上就是一顆 二叉樹 ,而根據根節點資料的不同又分為:最大堆 和 最小堆。
什麼是最大堆?父節點的值 永遠 大於等於 兩個 孩子節點 的值。
什麼是最小堆?父節點的值 永遠 小於等於 兩個 孩子節點 的值。
二叉堆的特性
自我調整:當插入或者刪除資料時,二叉堆會更改元素的位置,使父節點依然大於(小於)等於孩子節點。
完全二叉樹:二叉堆是一顆完全二叉樹。
二叉堆新增和刪除元素
新增元素:二叉堆的元素插入,總是插入到二叉樹的最後一個位置,拿最小堆舉例。如果插入一個新元素0。
0和父節點5做比較,小於父節點,所以0上浮,也就是說0和父節點5交換位置。
此時依然和父節點做比較,父節點是3,0小於3,那麼0繼續上浮,0和3交換位置。
父節點此時為1,0依然小於父節點1,上浮至根節點位置,比較結束。
刪除元素:二叉堆元素的刪除,總是刪除堆頂的元素,最大堆是堆中的最大值,最小的則反之。如果我們要刪除堆頂元素1。
為了維持二叉堆的平衡,此時把最後一個元素10放到堆頂,現在雖然是完全二叉樹,但是二叉堆的平衡已經遭到破壞,所以讓元素10下沉至合適位置。
讓元素10和兩個孩子節點比較,取其中最小的孩子節點,交換位置,孩子節點3、2中,2最小,所以和2交換位置。
此時10的兩個孩子節點又分別為7、8,所以繼續下沉,和7交換位置。
10已經是葉子節點,下沉結束,此時二叉堆已經恢復平衡。
程式碼實現
二叉堆雖然是一顆完全二叉樹,但是並不是用鏈式結構儲存的,因完全二叉樹的性質,可以儲存在陣列當中。
但是如何從當前元素定位到孩子節點呢?我們可以用索引來計算。
元素1儲存在第一個位置,索引為0,那他的左孩子節點的下標索引為 parent * 2 + 1 = 1,右孩子節點的下標索引為 parent * 2 + 2 = 2;但是如果我們把第一個元素的下標索引定義為 1 ,那麼左孩子的下標索引為 parent * 2 = 2,右孩子節點的下標索引為 parent * 2 + 1 = 3。
搞清楚這些,開始寫程式碼
package com.zjm.binheap;
/**
* 最大堆實現
* @author Zhao JinMing
* @data 2018.9.15
* @version 0.9.0
*/
public class BinaryHeap {
private int elementCount;
private int[] elements;
private static final int DEFAULT_CAPACITY = 16;
private static final int MAXIMUM_CAPACITY = 1 << 30;
public BinaryHeap() {
this(DEFAULT_CAPACITY);
}
public BinaryHeap(int[] data) {
this();
for (int d : data) {
push(d);
}
}
public BinaryHeap(int capacity) {
elements = new int[tableSizeFor(capacity)];
elementCount = 0;
}
/**
* 入隊操作
* @param data 入隊資料
*/
public void push(int data) {
if(elements.length - 1 == elementCount)
resize((elementCount + 1) << 1);
elements[++elementCount] = data;
floatElement(elementCount);
}
/**
* 彈出佇列
* @return 返回佇列最大或者最小值
*/
public int pop() {
if(elements.length < elementCount >> 2)
resize(elementCount >> 2);
int res = elements[1];
swap(1, elementCount);
elements[elementCount--] = 0;
sinkElement();
return res;
}
/**
* 檢視隊首元素
* @return 返回佇列第一個元素
*/
public int peek() {
return elementCount == 0 ? -1 : elements[1];
}
/**
* 元素個數
* @return 返回元素個數
*/
public int size() {
return elementCount;
}
/**
* 檢驗非空
* @return TRUE 佇列為空
*/
public boolean isEmpty() {
return elementCount == 0;
}
/**
* 重新設定elements元素大小
* @param size 新陣列的大小
*/
private void resize(int size) {
if (size > MAXIMUM_CAPACITY) {
//TODO
}
int[] newElements = new int[size];
copyArray(elements, newElements);
elements = newElements;
}
/**
* 元素上浮
* @param index 上浮元素索引位置
*/
private void floatElement(int index) {
while (index > 1 && elements[index >> 1] < elements[index]) {
swap(index >> 1, index);
index >>= 1;
}
}
/**
* 元素下沉
*/
private void sinkElement() {
int index = 1;
while (index << 1 <= elementCount &&
(elements[index] < elements[index << 1] || elements[index] < elements[(index << 1) + 1])) {
int p = index << 1;
int i = elements[p] > elements[p + 1] ? p : p + 1;
swap(index, i);
index = (i & 0x01) == 1 ? (index << 1) + 1 : index << 1;
}
}
/**
* HashMap中的方法,把傳入引數變為下一個 1 << n (2的n次方大於 cap 並且 2的n-1次方小於 cap)
* @param cap 改變的引數
* @return 1 << n
*/
private static int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/**
* 陣列複製
* @param source 被複制的源陣列
* @param target 複製目標陣列
*/
private static void copyArray(int[] source, int[] target) {
System.arraycopy(source, 0, target, 0, source.length);
}
/**
* 交換陣列兩元素位置
* @param indexA 元素A的索引位置
* @param indexB 元素B的索引位置
*/
private void swap(int indexA, int indexB) {
int temp = elements[indexA];
elements[indexA] = elements[indexB];
elements[indexB] = temp;
}
}
這就是一個最大堆,陣列第一個位置並沒有存放元素,而是從下標索引1開始,存放最大值。
執行測試資料
public static void main(String[] args) {
int length = 20;
Random random = new Random();
int[] elements = new int[length];
for (int i = 0; i < length; i++) {
int ele = random.nextInt(80);
elements[i] = ele;
System.out.print(ele + " ");
}
BinaryHeap maxHeap = new BinaryHeap(elements);
System.out.println();
while (!maxHeap.isEmpty()) {
System.out.print(maxHeap.pop() + " ");
}
}
結果輸出
77 74 56 27 13 23 44 38 35 46 43 51 76 67 78 20 77 12 60 62
78 77 77 76 74 67 62 60 56 51 46 44 43 38 35 27 23 20 13 12
一個最大堆完成。