堆(Heap)

注意本文說的堆是數據結構中的堆,而不是java內存模型中的堆。java

1、定義

n個元素的序列{k1, k2, …, kn}當且僅當知足如下關係時,稱之爲。若堆頂元素最小,則稱之爲小頂堆或小根堆。若堆頂元素最大,則稱之爲大頂堆或大根堆。以下圖所示。算法

\begin{cases}
\text{$k_i$ $\leq$ $k_{2i}$}\\[1ex]
\text{$k_i$ $\leq$ $k2_{i+1}$}
\end{cases}
~~~~~ 或 ~~~~~
\begin{cases}
\text{$k_i$ $\geq$ $k_{2i}$}\\[1ex]
\text{$k_i$ $\geq$ $k2_{i+1}$}
\end{cases} 
~~~~~
\left(i = 1, 2, ..., \lfloor \frac{n}{2} \rfloor\right)

2、性質

若以一維數組做爲堆的存儲結構,並將該一維數組當作是一個徹底二叉樹,則徹底二叉樹中全部非終端結點的值均不大於(或不小於)其左、右孩子結點的值。數組

堆頂元素(或徹底二叉樹的根)是堆中最小值(或最大值)。數據結構

最後一個非終端結點是第\lfloor \frac{n}{2} \rfloor個元素。ui

3、操做

  • 向上移動

向上移動又有人稱其爲上浮,是將一個元素與其父結點比較大小,不符合堆的條件就交換位置,交換後繼續與新的父結點比較,如此循環,直到符合堆的條件爲止。如上圖所示,若是是小根堆,而該元素卻小於父結點,那麼就須要將其向上移動,移動後再與新的父結點比較,以此類推,直到找到某個位置,它再也不小於父結點,則該知足堆的條件,移動結束。spa

參考代碼:code

private void shiftUp(int k) {
		while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
			swap(k, parent(k));
			k = parent(k);
		}
	}
複製代碼
  • 向下移動

向下移動又有人稱其爲下沉,是將一個元素與其孩子結點進行比較與調整。如上圖所示,若是是小根堆,用該結點與其左右孩子中較小的一個結點比較,若是大於那個孩子,則與其交換,交換後繼續與新的孩子結點比較,以此類堆,直到找到其合適位置爲止。cdn

參考代碼:blog

private void shiftDown(int k) {
		while(left(k) < _heapSize) {
			int j = left(k);
			if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
			if(_heap[k] < _heap[j]) break;
			swap(k,j);
			k = j;
		}
	}
複製代碼
  • 插入

插入也就是向堆中加入新成員的操做。那麼新成員放在哪裏呢?放在最後。那放在最後是否是可能破壞堆的結構啊?沒錯。怎麼辦?將其向上移動。排序

參考代碼:

public void insert(int v) {
		if(_heapSize >= _maxSize) return; //如超出容量,這裏只簡單地返回,實際中請根據需求進行處理
		_heap[_heapSize] = v;
		shiftUp(_heapSize);
		_heapSize++;
	}
複製代碼
  • 刪除

刪除也就是堆頂元素被拿走了。羣龍無首這下怎麼辦?別急咱們要選出新的堆頂。下面我來告訴你怎麼辦,首先把堆中最後一個元素搬到堆頂,而後將其向下移動。對,就這麼簡單。

參考代碼:

public int delMin() throws Exception {
		if(_heapSize == 0) {
			throw new Exception("The heap is already empty!");
		}
		int max = _heap[0];
		_heapSize--;
		swap(0, _heapSize);
		shiftDown(0);
		return max;
	}
複製代碼
  • 建立

我的理解堆的建立其實就是將一個序例經過某些操做,使其知足堆的條件從而轉化爲堆的過程。也就是你給我一個序列,我還你一個堆!

那麼如何去搞呢? 有兩種方式:

1)逐個插入。插入操做會自覺保證插入後該序列仍然是堆。

參考代碼

public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = new int[maxSize];

		//請看關鍵代碼,逐個插入
		for(int i = 0; i < initialNums.length; i++) {
			insert(initialNums[i]);
		}
	}
複製代碼

2)逐個調整。從最後一個非終端結點開始,向前,逐個調整以各個非終端結點爲根的子樹,使每棵子樹都變成堆,等最後一個非終端結點調整完畢,整個序列就變成了堆。

參考代碼

public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = Arrays.copyOf(initialNums, initialNums.length);
		_heapSize = initialNums.length;
		
		//從最後一個非終端結點開始逐棵子樹調整
		for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
			shiftDown(i);
		}
	}
複製代碼

4、完整代碼

該代碼簡單實現了小頂堆的建立、插入、刪除等操做。但願可以輔助讀者理解。爲簡單起見,這裏只接收int類型數據。

點擊查看完整代碼
package just.doit;
import java.lang.Exception;

public class MinHeap {
	private  int _heapSize = 0;
	private  int _maxSize;
	private  int[] _heap = null;

// public MinHeap(int[] initialNums, int maxSize) {
// _maxSize = maxSize;
// _heap = Arrays.copyOf(initialNums, initialNums.length);
// _heapSize = initialNums.length;
// 
// //從最後一個非終端結點開始逐棵子樹調整
// for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
// shiftDown(i);
// }
// }

	public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = new int[maxSize];

		//逐個插入
		for(int i = 0; i < initialNums.length; i++) {
			insert(initialNums[i]);
		}
	}

	public void insert(int v) {
		if(_heapSize >= _maxSize) return; //如超出容量,這裏只簡單地返回,實際中請根據需求進行處理
		_heap[_heapSize] = v;
		shiftUp(_heapSize);
		_heapSize++;
	}

	public int delMin() throws Exception {
		if(_heapSize == 0) {
			throw new Exception("The heap is already empty!");
		}
		int max = _heap[0];
		_heapSize--;
		swap(0, _heapSize);
		shiftDown(0);
		return max;
	}

	public void printMinHeap() {
		for(int i = 0; i < _heapSize; i++) {
			System.out.print(_heap[i]+" ");
		}
		System.out.println();
	}

	private void shiftUp(int k) {
		while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
			swap(k, parent(k));
			k = parent(k);
		}
	}

	private void shiftDown(int k) {
		while(left(k) < _heapSize) {
			int j = left(k);
			if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
			if(_heap[k] < _heap[j]) break;
			swap(k,j);
			k = j;
		}
	}

	private void swap(int i, int j) {
		int temp = _heap[i];
		_heap[i] = _heap[j];
		_heap[j] = temp;
	}
	//本代碼從0開始存儲,因此left爲2 * k + 1,若從1開始存儲則left爲2 * k
	private int left(int k) {
		return 2 * k + 1;
	}
	//本代碼從0開始存儲,因此right爲2 * k + 2,若從1開始存儲則right爲2 * k + 1
	private int right(int k) {
		return 2 * k + 2;
	}
	//本代碼從0開始存儲,因此parent爲(k - 1) / 2,若從1開始存儲則parent爲k / 2
	private int parent(int k) {
		return (k - 1) / 2;
	}

	public static void main(String[] args) throws Exception {
		int[] a = {10,33,1,4,3,29,5,8};

		MinHeap maxHeap = new MinHeap(a, 20); //使用逐個插入的方式構建堆

		maxHeap.printMinHeap(); // 1 3 5 8 4 29 10 33

		System.out.println(maxHeap.delMin()); // 取出堆頂元素 1

		maxHeap.printMinHeap(); //取出堆頂元素後的新堆 3 4 5 8 33 29 10

		maxHeap.insert(6); // 插入 6

		maxHeap.printMinHeap(); // 插入後的新堆 3 4 5 6 33 29 10 8
	}

}

複製代碼

5、使用場景

堆的使用場景包括但不限於一下三種。

  • 堆排序

有了上面的基礎,堆排序的思路很簡單,給一個序列,先將其構建成堆,堆頂元素確定是最大(或最小值),將堆頂元素放到序列末尾,並把末尾元素補充到堆頂,並對其進行向下調整,調整到n-1位置爲止,這樣前n-1個元素又是一個堆,又能夠取到第二大(或第二小)的值,以此類推,直到堆只剩下一個元素,將獲得一個有序序列。

以下代碼是經過構建小根堆,將int數組從大到小排序:

public static void heapSort(int[] initialNums) {
		int[] heap = buildMinHeap(initialNums);

		for(int i = heap.length - 1; i > 0; i--) {
			swap(heap, 0, i);
			shiftDown(heap, 0, i);
		}
	}
複製代碼
點擊查看完整代碼
package just.doit;

import java.util.Arrays;

public class Sort {
	public static void heapSort(int[] initialNums) {
		int[] heap = buildMinHeap(initialNums);

		for(int i = heap.length - 1; i > 0; i--) {
			swap(heap, 0, i);
			shiftDown(heap, 0, i);
		}
	}

	private static int[] buildMinHeap(int[] initialNums) {
		for(int i = ((initialNums.length - 1) / 2); i >= 0; i--) {
			shiftDown(initialNums, i, initialNums.length);
		}
		return initialNums;
	}

	private static void shiftDown(int[] heap, int k, int heapSize) {
		while(left(k) < heapSize) {
			int j = left(k);
			if(right(k) < heapSize && heap[right(k)] < heap[left(k)]) j++;
			if(heap[k] < heap[j]) break;
			swap(heap,k,j);
			k = j;
		}
	}

	private static void swap(int[] heap, int i, int j) {
		int temp = heap[i];
		heap[i] = heap[j];
		heap[j] = temp;
	}

	private static int left(int k) {
		return 2 * k + 1;
	}

	private static int right(int k) {
		return 2 * k + 2;
	}

	public static void main(String[] args) {
		int[] a = {5,23,7,33,2,1,16,9};
		System.out.println("排序前:" + Arrays.toString(a));
		Sort.heapSort(a);
		System.out.println("堆排序後:" + Arrays.toString(a));
	}

}

輸出:
排序前:[5, 23, 7, 33, 2, 1, 16, 9]
堆排序後:[33, 23, 16, 9, 7, 5, 2, 1]

複製代碼
  • 優先隊列

堆能夠用來實現優先隊列(Priority Queue)。說到隊列,你們馬上會想到先進先出。根據名字來看,優先隊列彷佛不同。沒錯,它根據元素的優先級來決定取出順序。關於優先隊列這裏不過多講述。

  • 海量數據中找TopK

例如給了一百萬個數據,我想找到最大的100個數據。那麼我能夠先拿100個元素建一個小根堆,而後一個一個取剩下的元素與堆頂比較,若是大於堆頂,則把堆頂刪除,再把這個元素放入堆中。若是小於堆頂,則不作處理。最後堆中100個元素則爲最大的100元素。

6、總結

以上則爲做者對堆的一些認識與總結,但願能給讀者一些啓發。若有不妥之處,但願能獲得批評指正!

結尾與君共同賞古詩一首,願君更上一層樓!

登鸛雀樓  
    [唐] 王之渙  
白日依山盡,黃河入海流。  
欲窮千里目,更上一層樓。  
複製代碼

7、參考文獻

《數據結構》 嚴蔚敏 吳偉民 編著 《算法導論》 殷建平 徐雲 等譯 《算法》 謝路雲 譯

相關文章
相關標籤/搜索