1. 程式人生 > >八大排序演算法(六)——優先佇列、堆和堆排序

八大排序演算法(六)——優先佇列、堆和堆排序

6.1 API

優先佇列是一種抽象資料型別,它表示了一組值和對這些值的操作。優先佇列最重要的操作就是刪除最大元素和插入元素。

6.2 初級實現

6.2.1 陣列實現(無序)

或許實現優先佇列最簡單方法就是基於下壓棧的程式碼。insert()方法的程式碼和棧的完全一樣。要實現刪除最大元素,可以新增一段類似選擇排序的內迴圈的程式碼,將最大元素和邊界元素交換然後刪除它,和棧的pop()方法的實現一樣。和棧類似,也可以加入調整陣列大小的程式碼來保證資料結構中至少含有四分之一的元素而又永遠不會溢位。

6.2.2 陣列實現(有序)

在insert()方法中新增程式碼,將所有的較大的元素向右邊移動一格以使陣列保持有序。這樣,最大的元素總會在陣列的一邊,優先佇列刪除最大元素的操作和棧的pop操作一樣了。

6.2.3 連結串列表示法

和剛才類似,可以用基於連結串列的下壓棧的程式碼作為基礎,而後可以選擇修改push()來保證所有元素為逆序並用pop()來刪除並返回連結串列的首元素。

實現棧或是佇列與實現優先佇列的最大不同在於對效能的要求。對於棧和佇列,能夠在常數時間內完成所有操作;而對於優先佇列,初級實現中,插入和刪除最大元素這兩個操作之一在最壞情況下需要線性時間來完成。

6.3 堆的定義

資料結構二叉堆可以很好地實現優先佇列的基本操作。在二叉堆的陣列中,每個元素都要保證大於等於另外兩個特點的元素。相應地,這些位置的元素又至少要大於等於陣列中的另外兩個元素,以此類推。

當一個二叉樹的每個節點都大於等於它的兩個子節點時,它被稱為堆有序。

二叉堆是一組能夠用堆有序的完全二叉樹排序的元素,並在陣列中按層級儲存。

在堆中,位置k的節點的父節點的位置為k/2(向下取整),而它的兩個子節點的位置分別為2k和2k+1。這樣再不使用指標的情況下,也可以通過計算陣列的索引在樹中上下移動:從a[k]向上一層就令k=k/2,向下一層則令k等於2k或2k+1。

用陣列實現完全二叉樹的結構是很嚴格的,但它的靈活性已經足以高效地實現優先佇列。用它們將能實現對數級別的插入元素和刪除最大元素的操作。

一顆大小為N的完全二叉樹的高度為logN(向下取整)。

6.4 堆的演算法

堆的操作會進行一些簡單的改動,打破堆的狀態,然後再遍歷堆並按照要求將堆的狀態恢復。這個過程叫做堆的有序化。

堆的有序化過程中會遇到兩種情況。當某個節點的優先順序上升(或在堆底加入一個新元素),我們需要由下至上恢復堆的順序。當某個節點的優先順序下降(如將根節點替換成一個較小的元素),需要由上至下恢復堆的順序。

6.4.1 由下至上堆有序化(上浮)
private void swim(int k) {
		while (k > 1 && a[k / 2] < a[k]) {
			swap(a[k / 2], a[k]);
			k = k / 2;
		}
	}
6.4.2 由上至下有序化(下沉)
private void sink(int k) {
		while (2 * k <= N) {
			int j = 2 * k;
			if (j < N && a[j] < a[j + 1]) {
				j++;
			}
			if (!a[k] < a[j]) {
				break;
			}
			swap(a[k], a[j]);
			k = j;
		}
	}

以上是高效實現優先佇列的基礎。

插入元素。將新元素加到陣列末尾,增加堆的大小並讓這個新元素上浮到合適的位置。

刪除最大元素。從陣列頂端刪除最大的元素並將陣列的最後一個元素放到頂端,減小堆的大小並讓這個元素下沉到合適的位置。

它對優先佇列API的實現能夠保證插入元素和刪除最大元素這兩個操作的用時和佇列的大小僅成對數關係。

優先佇列由一個基於堆的完全二叉樹表示,儲存於陣列pq[1..N]中,pq[0]沒有使用。在insert()中,我們將N加1並將新元素添在陣列最後,然後用swim()恢復堆的秩序。在delMax()中,從pq[1]得到返回的元素,然後將pq[N]移動到pq[1],將N減一併用sink()恢復堆的秩序。同時還將不再使用的pq[N+1]設為null,以便系統回收它所佔的空間。

對於一個含有N個元素的基於堆的優先佇列,插入元素操作只需要不超過logN+1次比較,刪除最大元素的操作需要不超過2logN次比較。

6.5 堆排序

堆排序可以分為兩個階段。在堆的構造過程中,將原始陣列重新安排進一個堆中;然後在下沉排序階段,從堆中按遞減順序取出所有元素並得到排序結果。

6.5.1 堆的構造

用下沉操作由N個元素構造堆只需要少於2N次比較以及少於N次交換。

private void sort(Comparable[] a) {
		int N = a.length;
		for (int k = N / 2; k >= 1; k--) {
			sink(a, k, N);
		}
		while (N > 1) {
			swap(a, 1, N--);
			sink(a, 1, N);
		}
	}
6.5.2 下沉排序

堆排序的主要工作都是在第二階段完成的。將堆中最大的元素刪除,然後放入堆縮小後陣列中空出的位置。這個過程與選擇排序有些類似(按照降序而非升序取出所有元素),但所需比較要少得多,因為堆提供了一種從未排序部分找到最大元素的有效防範。

將N個元素排序,堆排序只需少於2NlogN+2N次比較(以及一半次數的交換)。

6.5.3 先下沉後上浮

大多數在下沉排序期間重新插入堆的元素會被直接加入到堆底。正好可以通過免去檢查元素是否到達正確位置來節省時間。在下沉中總是直接提升較大的子節點到堆底,然後再使元素上浮到正確的位置。這樣幾乎將比較次數減少一半,接近歸併排序所需的比較次數(隨機陣列)。但這種方法需要額外的空間,因此在實際應用中只有當比較操作代價較高時才有用。

堆排序在排序複雜度研究中有重要地位,因為它是所知唯一能夠同時最優地利用空間和時間的方法——在最壞的情況下它也能保證使用~2NlogN次比較和恆定的額外空間。當空間十分緊張時它很流行,因為它只要幾行就能實現較好的效能。但現代系統的很多應用很少使用它,因為它無法利用快取。陣列元素很少和相鄰的其他元素進行比較,因此快取未命中的次數要遠遠高於大多數比較都在相鄰元素之間的演算法。

另一方面,用堆實現的優先佇列在現代應用程式中越來越重要,因為它能在插入操作和刪除最大元素操作混合的動態場景中保證對數級別的執行時間。