零、前言
(emm才發現這篇忘記發了=_=)上一篇我們學習了用堆實現優先隊列解決了經典的TopK問題,那這篇我就帶大家來手寫一個自己的堆吧。
一、二叉堆
我們首先來了解一下二叉堆。
二叉堆是一種特殊的堆,其實質是完全二叉樹(把元素順序排列成樹的形狀)。
二叉堆有兩種:最大堆和最小堆。最大堆是指父節點鍵值總是大於或等於任何一個子節點的鍵值。而最小堆恰恰相反,指的是父節點鍵值總是小於任何一個子節點的鍵值。如“圖1 最大堆”、“圖2 最小堆”所示:
圖1 最大堆
圖2 最小堆
需要注意二叉堆與二叉樹的數據存儲結構不同,二叉樹是鏈式存儲,而二叉堆是線性存儲,知識邏輯結構用樹表示而是。 通常用數組來存儲二叉堆,並且可以得出父節點和子節點索引之間的關係。如下圖所示
我這裏對應一部分功能給出一部分代碼,可能更好理解。最後會給出全部代碼。
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引
private int parent(int index) {
if (index == 0)
throw new IllegalArgumentException("index-0 does not have parent.");
return (index - 1) / 2;
}
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
private int leftChild(int index) {
return index * 2 + 1;
}
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
private int rightChild(int index) {
return index * 2 + 2;
}
二、二叉堆的操作
1、添加節點和上浮節點
在添加節點的時候,每添加的節點都是添加到二叉堆的最後一個位置,以最大堆爲例,如下圖所示:
添加節點10後發現其父節點小於新加的子節點,這不滿足最大堆的性質了,那可怎麼辦?所以有了接下來的上浮節點操作。
上浮節點 即判斷當前節點是否比父節點大,是的話則交換元素,繼續判斷。直到不大於父節點或者到達堆頂。下面將這個過程仔細說明並圖示。
添加新數據後(子節點10),我們讓子節點10與其父節點5作比較,父節點小於子節點,於是將子節點上浮與父節點轉換,如下圖:
接着再做同樣的操作,子節點10與父節點8作比較,同樣需要上浮,如圖;
再同樣做同樣操作,直至父節點大於子節點或者子節點已經上浮到根節點即止。如圖;
這裏先給出這部分的代碼。
// 成員變量 data 注:這裏我使用的自己實現的動態數組!使用java的數組也可以
private Array<E> data;
// 向堆中添加元素
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
// 元素上浮
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(k, parent(k));
k = parent(k);
}
}
2、刪除節點和下沉節點
在刪除節點的時候,每次都是刪除二叉堆的根節點,最大堆即最大值,最小堆即最小值,如圖
在刪除根節點之後,我們不可能讓根節點位置無主吧?於是將二叉堆最後一個數據填充至根節點,如圖;
但是,我們又可以發現,填充上來的根節點比它的子節點小,這也不符合最大堆的性質呀!於是就有了下沉節點操作。
下沉節點 與上浮相反,判斷當前節點是否比子節點小,是的話則較大的子節點交換元素,繼續判斷。直到不小於子節點或者到達堆底。
直接上代碼:
// 移除最大元素
public E extractMax() {
E e = findMax();
// 交換最大值和最後一個
data.swap(0, data.getSize() - 1);
// 移除最後一個即最大值
data.removeLast();
// 下沉操作,維護順序
siftDown(0);
return e;
}
// 元素下沉
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if (data.get(k).compareTo(data.get(j)) >= 0)
break;
data.swap(j, k);
k = j;
}
}
3、構建二叉堆
構建二叉堆,也就是把一個無序的完全二叉樹調整爲二叉堆,本質上就是讓所有非葉子節點依次下沉。
比如此時有一個無序的二叉樹,如圖;
首先從最後一個非葉子結點8開始,讓其與兩個子節點11、6作比較,然後做下沉節點操作,
接着是節點2,做同樣的操作
然後是節點5
最後是節點12,因爲該節點都比其兩個子節點大,所以無需下沉。
最終,一顆無序的二叉樹就構建成了一個最大堆了。如圖
代碼:
// 傳入數組的構造函數
public MaxHeap(E[] arr){
data = new Array<>(arr);
for (int i = parent(data.getSize() - 1); i >= 0; i--) {
siftDown(i);
}
}
4、代碼整合
/**
* 動態數組 Array
* 便於觀看我這隻留了需要的方法
* @param <E>
*/
public class Array<E> {
//成員變量
private E[] data;
private int size;
//構造函數,傳入數組的容量capacity構造Array
public Array(int capacity) {
data = (E[])new Object[capacity]; //不能直接new 自定義類型的對象,間接new Object在轉型
size = 0;
}
public Array(E[] arr){
data = (E[])new Object[arr.length];
for (int i = 0; i < arr.length; i++) {
data[i] = arr[i];
}
size = arr.length;
}
//在所有元素後添加一個元素
public void addLast(E e) {
add(size, e);
}
public E removeLast(){
return remove(size-1);
}
public void swap(int i,int j){
if (i < 0 || i >= size || j < 0 || j >= size) {
throw new IllegalArgumentException("Index is illegal!");
}
E t = data[i];
data[i] = data[j];
data[j] = t;
}
}
/**
* @description: 基於數組實現的最大堆
* @author: Kevin
* @createDate: 2020/2/21
* @version: 1.0
*/
public class MaxHeap<E extends Comparable<E>> {
// 成員變量 data 注:這裏我使用的自己實現的動態數組!使用java的數組也可以
private Array<E> data;
public MaxHeap(int capacity) {
data = new Array<>(capacity);
}
public MaxHeap() {
data = new Array<>();
}
// 傳入數組的構造函數
public MaxHeap(E[] arr){
data = new Array<>(arr);
for (int i = parent(data.getSize() - 1); i >= 0; i--) {
siftDown(i);
}
}
public int getSize() {
return data.getSize();
}
public boolean isEmpty() {
return data.isEmpty();
}
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引
private int parent(int index) {
if (index == 0)
throw new IllegalArgumentException("index-0 does not have parent.");
return (index - 1) / 2;
}
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
private int leftChild(int index) {
return index * 2 + 1;
}
// 返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
private int rightChild(int index) {
return index * 2 + 2;
}
// 向堆中添加元素
public void add(E e) {
data.addLast(e);
siftUp(data.getSize() - 1);
}
// 元素上浮
private void siftUp(int k) {
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(k, parent(k));
k = parent(k);
}
}
// 找到最大元素
public E findMax() {
if (data.isEmpty())
throw new IllegalArgumentException("Can not find from an empty heap!");
return data.get(0);
}
// 移除最大元素
public E extractMax() {
E e = findMax();
// 交換最大值和最後一個
data.swap(0, data.getSize() - 1);
// 移除最後一個即最大值
data.removeLast();
// 下沉操作,維護順序
siftDown(0);
return e;
}
// 元素下沉
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if (data.get(k).compareTo(data.get(j)) >= 0)
break;
data.swap(j, k);
k = j;
}
}
}
三、基於二叉堆實現的優先隊列
至此,我們就可以基於二叉堆實現自己的優先隊列啦,既然java默認是基於最小堆實現的優先隊列,那麼我們就用最大堆來實現吧嘻嘻嘻
其實就是調用二叉堆的方法,上馬
/**
* @description: 基於最大堆實現的優先隊列
* @author: Kevin
* @createDate: 2020/2/24
* @version: 1.0
*/
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private MaxHeap<E> maxHeap;
public PriorityQueue(){
maxHeap = new MaxHeap<>();
}
@Override
public int getSize() {
return maxHeap.getSize();
}
@Override
public boolean isEmpty() {
return maxHeap.isEmpty();
}
@Override
public void enqueue(E e) {
maxHeap.add(e);
}
@Override
public E dequeue() {
return maxHeap.extractMax();
}
@Override
public E getFront() {
return maxHeap.findMax();
}
}
四、總結
二叉堆的核心就在於上浮和下沉操作。
當添加元素是填添加到數組的最後一個位置,並判斷當前節點是否比父節點大,是的話則交換,繼續判斷。直到不大於父節點或者到達堆頂。
當刪除元素是刪除堆頂,此時先將堆頂與最後一個節點先交換,刪除最後一個節點即原堆頂。然後堆可能不滿足順序,從堆頂開始下沉操作。判斷當前節點是否比子節點小,是的話則較大的子節點交換元素,繼續判斷。直到不小於子節點或者到達堆底。
恭喜你又學到了知識,是不是還挺簡單的~
最後感謝閱讀,若有幫助點歌贊啦~
圖片引自:https://www.jianshu.com/p/6d3a12fe2d04