【數據結構】堆Heap - 一篇就夠了

一篇就能搞懂堆


目錄

一篇就能搞懂堆

1 概念

1.1    初印象

1.2    前提

1.3    認識堆

1.4    堆樹對比

2 堆研究

2.1    存儲

2.2    父子大小

2.3    下標同層關係

2.4    堆節點數

2.5    葉子節點數

2.6    操作邏輯

增加

刪除根節點

刪除任意節點

遍歷

2.7    堆排序 

3 基本操作    

3.1    HeapTest    

3.2    Heap    

4 附錄    4.1    父子關係推導   

​4.2    參考1:


1 概念

1.1    初印象

大根堆

小根堆

堆排序

堆(JVM)

1kw 個數,快速找出前10個

隊列 優先級高的先執行

上面的概念 和 解決的場景 有所聽過吧(聽過就行了,看完這篇文章你就能瞭解原理了),對的,堆就可以解決上述問題。

1.2    前提

二進制計算:第N爲之前的2^1+2^2+…+2^(N-1)值之和爲 2^N-1 (體會下:二進制11再加1就是100)

完全二叉樹

鏈表,數組

1.3    認識堆

  1. 堆 必須是完全二叉樹
  2. 堆 分爲 大根堆 & 小根堆
  3. 大根堆:父節點值比子節點值大
  4. 小根堆:父節點值比子節點值小
  5. 兄弟節點之間值 無所謂
  6. 對擺放順序 是一層一層,從左到右
  7. 一般使用數組實現,鏈表實現的二叉樹,分層操作複雜且很浪費空間
  8. 堆 只能 確保只有一個最大數或者最小數,排隊第二的不清楚

1.4    堆樹對比

  1. 堆 同二叉搜索樹 一樣是一種 滿足某種特性二叉樹 樹結構
  2. 特性:堆只不過對是 父節點大於左右子樹;二叉搜索樹 是左子樹<根<右子樹。
  3. 存儲:樹可以用鏈表實現,也可以用數組。二叉查找樹更側重搜索效率,常使用鏈表實現。堆則更注重最大值的獲取,是完全二叉樹,按層操作二叉樹,如果使用鏈表將使得操作很複雜,使用數組存儲對,關係可以使用數組下標計算得出,不用外餘存儲子節點應用,更節約空間。
  4. 平衡:二叉搜索樹 只有在平衡的時候 搜索複雜度纔是O(log n),一般的二叉查找樹在不好的情況下就是一個鏈表,複雜度爲O(n)。堆是完全二叉樹,當然平衡,搜索複雜度當然就滿足O(log n)。
  5. 搜索:平衡二叉查找樹就是爲搜索而生的;堆中搜索則交慢,更側重最大值的獲取上。


2 堆研究


2.1    存儲

要存儲樹,需要存儲兩個維度信息:1. 節點本書數據;2. 節點直接的關係。

使用鏈表存儲好處是,數據直接存儲,關係使用引用。

如果使用數組,如果每一個元素當做樹的一個節點,數據直接存儲即可,節點直接關係怎麼存儲呢?

數組還有啥信息可用呢? 下標,即:元素位置。

如果我告訴你 輩分最高在前,同輩中歲數大的在前,每家最多兩個兒子。那麼我給你位置編號,你能知道這是誰家的兒子嗎?

存儲:

數據:節點數據放在數組中,i爲數組下標。

關係:

父à子:2i+1 & 2i+2 (i爲某節點的小標)

子à父:(i-1)/2  (i是子節點下標,/是整除意思)

舉例:

數組:[ 10, 7, 2, 5, 1 ]

根據上述公式計算得:

Node  ArrayIndex(i)  ParentIndex   LeftChild  RightChild

   10           0           0           1           2

    7           1           0           3           4

    2           2           0        5(×)        6(×)

    5           3           1        7(×)        8(×)

    1           4           1        9(×)       10(×)

 

2.2    父子大小

父節點-->子節點: (i 爲父節點下標)

大根堆:父節點 > 子節點

Array[i] > Array[2i+1]

Array[i] > Array[2i+2]

 

小根堆:父節點 < 子節點

Array[i] < Array[2i+1]

Array[i] < Array[2i+2]

 

子節點-->父節點 : (i 爲子節點下標)

大根堆:子節點 < 父節點

Array[i] < Array[(i-1)/2]

 

小根堆:子節點 > 父節點

Array[i] > Array[(i-1)/2] 


2.3    下標同層關係

定義層數:從0開始,根節點爲第0層。

如果一個堆有 n 個節點,那麼它的高度是 h = log2(n)。這是因爲我們總是要將這一層完全填滿以後纔會填充新的一層。上面的例子有 15 個節點,所以它的高度是 floor(log2(15)) = floor(3.91) = 3。


2.4    堆節點數

一個h層的堆,最多有:2^(h+1) - 1 個節點。

每一層的節點個數:2^h

從第一層到第h層節點總數:2^h-1

那麼一個具有第h層的堆節點個數 = 2^h + 2^h-1 = 2^(h+1) - 1


2.5    葉子節點數

一個堆葉子節點總數爲n,那麼葉節點總是位於數組的 n/2 和 n-1 之間。


2.6    操作邏輯

增加

從上到下,從左到右。

每次將增加的節點放在最下層的最右邊空位。

 

添加16

調整對滿足屬性

16不用跟兄弟節點7比較了,父節點10>左子節點7,現在右子節點16>父節點,一定也大於兄弟節點。

 

刪除根節點

刪除10,現在頂部有一個空的節點,怎麼處理?

我們取出數組中的最後一個元素,將它放到樹的頂部,然後再修復堆屬性。

爲了保持最大堆的堆屬性,需要調整。

繼續堆化直到該節點沒有任何子節點或者它比兩個子節點都要大爲止。

刪除任意節點

絕大多數時候你需要刪除的是堆的根節點,因爲這就是堆的設計用途。

但是,刪除任意節點也很有用。

刪除 (7),數組中的刪除是:我們需要將刪除的元素和最後一個元素交換,保證前面的數據都是有用的。

[ 10, 7, 2, 5, 1 ]  (有用下標:[0,4])

刪除後:

[ 10, 1, 2, 5, 7 ]  (有用下標:[0,3])

替換後,

 

底層節點跑到上面去了,那麼就需要調整堆。可能是向下調整了,直到滿足屬性。

遍歷

這就不用說了吧,直接遍歷數組即可。

就是一次刪除和一次增加。


2.7    堆排序 

從大到小:

  1. 大根堆 構建完成後,每次將 根節點輸出來,直到沒有元素。
  2. 原理:大根堆每次輸出的根是堆中 最大的

從小到大:

  1. 小根堆同理


3 基本操作    


3.1    HeapTest    

package com.example.demo;

import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.heap.Heap;

@SpringBootTest(classes = HeapApplication.class)
public class HeapTest {

	private Heap heap;

	@Before
	public void init(){
		int[] datas = new int[] { 7, 10, 2, 5, 1 };
		Heap p = new Heap();
		for (int i = 0; i < datas.length; i++) {
			// 新增數據
			p.add(datas[i]);
		}
		p.printRelationship();

		this.heap = p;
	}
	
	@Test
	public void add() {
		
	}

	@Test
	public void delete() {
		// 刪除任意節點
		heap.delete(2);
		heap.printRelationship();
	}
	
	@Test
	public void remove(){
		// 移除根節點
		System.out.println("移除根節點:" + this.heap.remove());
		heap.printRelationship();
	}
	
	@Test
	public void sort(){
		// 將所有根移除,就是堆排序
		int temp = -1;
		while((temp = heap.remove())!= -1){
			System.out.print(temp + " ");
		}
	}
}


3.2    Heap    

package com.example.demo.heap;

/**
 * 堆
 */
public class Heap {

	// 容量
	private int capcity = 16;
	// 數組的有效長度
	private int size = -1;
	// 堆數組
	private int[] datas = new int[capcity];

	/**
	 * 判斷是否是空堆
	 * @return
	 */
	public boolean empty() {
		return datas == null || datas.length == 0 || this.size == -1;
	}

	/**
	 * 刪除 數據
	 * @param data
	 */
	public void delete(int data) {
		int i = -1;
		if ((i = this.queryIndex(data)) == -1) {
			System.out.println("沒有找到");
			return;
		}

		// 刪除數據
		System.out.println("刪除數據: " + this.datas[i]);

		// 使用最後那個補充
		this.datas[i] = this.datas[this.size--];

		// 向下調整
		this.shiftDown(i);
	}

	// 檢索數據並返回下標
	private int queryIndex(int data) {
		if (this.empty()) {
			return -1;
		}

		for (int i = 0; i <= this.size; i++) {
			if (this.datas[i] == data) {
				return i;
			}
		}

		return -1;
	}

	/**
	 * 移除根元素
	 * 
	 * @return
	 */
	public int remove() {
		if (this.size == -1) {
			return -1;
		}
		// 獲取根節點
		int root = this.datas[0];
		// 將最後面的節點補在根節點
		this.datas[0] = this.datas[this.size--];
		// 向下調整
		this.shiftDown(0);
		return root;
	}

	/**
	 * 添加數據
	 * 
	 * @param data
	 * @return
	 */
	public int[] add(int data) {
		// 判斷容量
		if (size == capcity) {
			System.out.println("容量不足");
		}

		// 存儲數據
		datas[++size] = data;

		// 第一個沒必要調整屬性
		if (size != 0) {
			this.shiftUp(this.size);
		}

		return this.datas;
	}

	// 向下比較
	private void shiftDown(int pi) {

		// 如果是葉子節點就沒必要在比較了
		if (pi > this.size) {
			return;
		}

		// 獲取兩個子節點Index
		int ci1 = this.getFirstChildIndex(pi);
		int ci2 = ci1 + 1;

		/*
		 * 獲取 需要替換的子節點 
		 *  1. 沒有子節點了,即:ci1和ci2 都在size外了
		 *  2. 有一個子節點, 那也只能是1了 
		 *  3. 有兩個子節點,選出比較大的那個
		 */
		if (ci1 > this.size) {
			/*
			 * && ci2 > this.size 沒必要了吧,ci2 = ci1+1 沒有子節點了,該節點已經在最底層了
			 */
			return;
		} else if (ci1 == this.size) {
			/*
			 * && ci2 一定大於size了
			 */
			if (this.datas[ci1] > this.datas[pi]) {
				this.exchangeData(ci1, pi);
				// 替換後直接返回,沒必要在下午了。
				return;
			}
		} else if (ci2 <= this.size) {
			/*
			 * c1 也一定 < this.size了
			 */
			int mi = this.maxIndex(ci1, ci2);
			if (this.datas[mi] > this.datas[pi]) {
				// 子節點中 最大的 還大於 目標節點,則替換後
				this.exchangeData(mi, pi);
				// 繼續
				this.shiftDown(mi);
			}
		}
	}

	// 向上比較
	private void shiftUp(int ci) {
		// 如果目標節點下標是0,則是根節點,不需要比較了
		if (ci == 0) {
			return;
		}

		int pi = this.getParentIndex(ci);
		// 子節點的 還比 父節點 大,說明 還需要向上走
		if (this.datas[pi] < this.datas[ci]) {
			// 換值
			this.exchangeData(pi, ci);
			// 換完後 pi成爲 目標節點 index,繼續shiftup
			this.shiftUp(pi);
		}
		// 其他的則不作處理
	}

	// 中間變量
	private int temp;

	// 跟換數組的值
	private void exchangeData(int x, int y) {
		this.temp = this.datas[x];
		this.datas[x] = this.datas[y];
		this.datas[y] = this.temp;
	}

	// 獲取最大值下標
	private int maxIndex(int x, int y) {
		return this.datas[x] > this.datas[y] ? x : y;
	}

	/**
	 * 打印推的關係
	 */
	public void printRelationship() {

		if (datas == null || datas.length == 0 || this.size == -1) {
			System.out.println("空堆");
			return;
		}

		System.out.println(String.format(
				"%5s  %10s  %10s  %10s  %10s", "Node",
				"ArrayIndex(i)", 
				"ParentIndex",
				"LeftChild",
				"RightChild"));
		for (int i = 0; i <= this.size; i++) {
			System.out.println(String.format("%5s  %10s  %10s  %10s  %10s", 
					datas[i], i,
					this.checkIndex(this.getParentIndex(i)), 
					this.checkIndex(this.getFirstChildIndex(i)),
					this.checkIndex(this.getFirstChildIndex(i) + 1)));
		}
	}

	// 檢查index
	private String checkIndex(int i) {
		if (i > this.size) {
			return "×";
		}
		return i + "";
	}

	// 獲取 第一個子節點的下標
	private int getFirstChildIndex(int parentIndex) {
		return parentIndex * 2 + 1;
	}

	// 獲取 父節點的 下標
	private int getParentIndex(int childIndex) {
		return (childIndex - 1) / 2;
	}
}



4 附錄    
4.1    父子關係推導   


4.2    參考1:

https://www.jianshu.com/p/6b526aa481b1


大道至簡。

 

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