《一文說透數據結構》系列之什麼是堆?看這一篇就夠了

本文將首先介紹什麼是堆,然後介紹了堆的插入和刪除操作,最後給出了堆的代碼實現,並進行了測試。

什麼是堆

堆是一顆完全二叉樹,堆中某個節點的值總是不大於或不小於其父節點的值。根節點最大的堆叫做大根堆,根節點最小的堆叫做小根堆。
首先解釋下什麼是完全二叉樹,設一顆二叉樹的深度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。如下圖所示,左側的二叉樹滿足完全二叉樹的定義,而右側的不滿足。
左側爲完全二叉樹,右側的不是
上圖左側便是一個小根堆,滿足任意一個節點的值總是不小於其父節點的值。

堆的表示

一般的二叉樹表示時需要首先定義節點結構,節點中包含指向父節點的指針,如下所示:

class Node<E>{
		E e;//節點儲存的值
		Node left,right;//左右子節點
		public Node(E e){
			this.e = e;
			this.left = this.right = null;
		}
	}

但是堆並不是像樹一樣存儲,其中沒有使用父指針或者子指針,而是用數組來實現。怎麼用數組來實現呢?先看一張圖,如下:
堆的索引關係
我們從0開始對節點進行編號,尋找其中父子節點之間索引的對應關係。
首先,通過子節點的索引來找父節點的索引,設子節點的索引爲i,則其父節點的索引爲

int parentIndex = (i - 1) / 2;

然後,通過父節點的索引來找子節點的索引,設父節點的索引爲p,則其孩子節點的索引爲

int leftChildIndex  = 2 * p + 1;//左子節點
int rightChildIndex = 2 * p + 2;//右子節點

這樣,通過子節點與父節點之間的索引關係,便相當於建立了父節點和子節點之間的指針,實現了用數組來存儲堆這種數據結構。

堆的插入

對於堆來說,只有插入和刪除兩種操作,先談一下堆的插入操作,此處以小根堆爲例。
堆的插入操作
如上圖1所示,在小根堆中插入元素0,首先將元素放置在二叉樹最後一行的末尾,此時依然是完全二叉樹;然後將該元素與父節點的值比較,若改節點的值小於父節點,則進行交換,如圖3所示;之後再次與父節點進行對比交換,直至該節點的值大於等於父節點的值或者已經是根節點爲止,如圖4 所示。此時堆依然滿足定義。

堆的刪除

對於堆來說,刪除元素是指移除根節點。以小根堆爲例,是指移除根中最小值的節點,也就是根節點。移除很簡單,之後我們要通過操作來使得堆依然滿足定義。
堆的刪除操作
首先刪除堆中索引爲0,也就是根節點,對於小根堆來說也就是最小值。然後將堆中最後一個元素填充至根節點的位置,如圖3所示;之後比較該節點與左右子節點,若該節點大於左右子節點中較小的節點的值,則與該節點進行交換(小根堆中父節點永遠與左右子節點中較小的那個子節點交換),如圖4、5所示;直至滿足該節點的值小於其左右子節點的值或者該節點左右子節點均爲空。此時堆依然滿足定義。

堆的實現

下面給出堆的代碼實現,如下所示,實現了堆的插入和刪除操作,並進行了測試。

package datastructures;

public class Heap {
	private int[] data;//存儲堆的數組
	private int size;//堆中元素的數量
	public Heap(int capacity){
		data = new int[capacity];//初始化數組
		size = 0;//初始化數量
	}

	/**
	 * 插入元素
	 */
	public void insert(int value) throws Exception{
		if(size == data.length)
			throw new Exception("堆已滿");
		else{
			data[size] = value;//將新插入的元素放在堆的末尾
			int i = size;
			size ++;
			while(i > 0){//對堆進行調整,直至滿足條件
				int p = (i - 1) / 2;
				if(data[i] < data[p]){
					int temp = data[i];
					data[i] = data[p];
					data[p] = temp;
					i = p;
				}
				else
					break;
			}
		}
	}
	
	/**
	 * 刪除堆中的元素
	 * @return
	 * @throws Exception
	 */
	public int delMin() throws Exception{
		int res;
		if(size == 0)
			throw new Exception("爲空");
		else{
			res = data[0];//返回索引爲0的元素
			size -- ;
			data[0] = data[size];//將堆中最後一個元素填充至索引爲0的位置
			int i = 0;
			while(2 * i + 1 < size){//對堆進行調整
				int left = 2 * i + 1;
				int right = 2 * i + 2;
				if(right < size && data[right] < data[left] && data[right] < data[i]){
					int temp = data[i];
					data[i] = data[right];
					data[right] = temp;
					i = right;
				}
				else if(data[left] < data[i] && (right >= size || data[right] >= data[left])){
					int temp = data[i];
					data[i] = data[left];
					data[left] = temp;
					i = left;
				}
				else
					break;
			}
		}
		return res;
	}
	
	//測試
	public static void main(String[] args) throws Exception {
		Heap heap = new Heap(10);
		heap.insert(1);
		heap.insert(5);
		heap.insert(4);
		heap.insert(3);
		heap.insert(6);
		heap.insert(2);
		System.out.println(heap.delMin());
		System.out.println(heap.delMin());
		System.out.println(heap.delMin());
		System.out.println(heap.delMin());
		System.out.println(heap.delMin());
		System.out.println(heap.delMin());
	}
	
}

推薦閱讀
爲什麼有紅黑樹?什麼是紅黑樹?看完這篇你就明白了
《深入淺出話數據結構》系列之什麼是B樹、B+樹?爲什麼二叉查找樹不行?
都2020年了,聽說你還不會歸併排序?手把手教你手寫歸併排序算法
爲什麼會有多線程?什麼是線程安全?如何保證線程安全?

覺得文章有用的話,點贊+關注唄,好讓更多的人看到這篇文章,也激勵博主寫出更多的好文章。
更多關於算法、數據結構和計算機基礎知識的內容,歡迎掃碼關注我的原創公衆號「超悅編程」。

超悅編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章