1.堆排序
基數排序適用於大小有界的東西,除了他之外,還有一種你可能遇到的其它專用排序算法:有界堆排序。如果你在處理非常大的數據集,你想要得到前 10 個或者前k
個元素,其中k
遠小於n
,它是很有用的。
例如,假設你正在監視一 個Web 服務,它每天處理十億次事務。在每一天結束時,你要彙報最大的k
個事務(或最慢的,或者其它最 xx 的)。一個選項是存儲所有事務,在一天結束時對它們進行排序,然後選擇最大的k
個。需要的時間與nlogn
成正比,這非常慢,因爲我們可能無法將十億次交易記錄在單個程序的內存中。我們必須使用“外部”排序算法。
我們首先了解一下堆,這是一個類似於二叉搜索樹(BST)的數據結構。有一些區別:
- 在 BST 中,每個節點
x
都有“BST 特性”:x
左子樹中的所有節點都小於x
,右子樹中的所有節點都大於x
。 - 在堆中,每個節點
x
都有“堆特性”:兩個子樹中的所有節點都大於x
。 - 堆就像平衡的 BST;當你添加或刪除元素時,他們會做一些額外的工作來重新使樹平衡。因此,可以使用元素的數組來有效地實現它們。
現在討論的是小根堆。如果子樹中的節點都小於根節點,則爲大根堆。
堆中最小的元素總是在根節點,所以我們可以在常數時間內找到它。在堆中添加和刪除元素需要的時間與樹的高度h
成正比。而且由於堆總是平衡的,所以h
與log n
成正比。
JavaPriorityQueue
使用堆實現。PriorityQueue
提供Queue
接口中指定的方法,包括offer
和poll
:
offer
:將一個元素添加到隊列中,更新堆,使每個節點都具有“堆特性”。需要logn
的時間。poll
:從根節點中刪除隊列中的最小元素,並更新堆。需要logn
的時間。
給定一個PriorityQueue
,你可以像這樣輕鬆地排序的n
個元素的集合 :
- 使用
offer
,將集合的所有元素添加到PriorityQueue
。 - 使用
poll
從隊列中刪除元素並將其添加到List
。
因爲poll
返回隊列中剩餘的最小元素,所以元素按升序添加到List
。這種排序方式稱爲堆排序 。
向隊列中添加n
個元素需要nlogn
的時間。刪除n
個元素也是如此。所以堆排序的運行時間是O(n logn)
。
2.代碼實現:
/**
* @Author Ragty
* @Description 堆排序
* @Date 19:15 2019/6/12
**/
public void heapSort(List<T> list,Comparator<T> comparator) {
PriorityQueue<T> heap = new PriorityQueue<T>(list.size(),comparator);
heap.addAll(list);
list.clear();
while(!heap.isEmpty()) {
list.add(heap.poll());
}
}
測試代碼:
list = new ArrayList<Integer>(Arrays.asList(3, 5, 1, 4, 2));
sorter.heapSort(list, comparator);
System.out.println(list);
3.有界堆排序
有界堆是一個限制爲最多包含k
個元素的堆。如果你有n
個元素,你可以跟蹤這個最大的k
個元素:
最初堆是空的。對於每個元素x
:
- 分支 1:如果堆不滿,請添加
x
到堆中。 - 分支 2:如果堆滿了,請與堆中
x
的最小元素進行比較。如果x
較小,它不能是最大的k
個元素之一,所以你可以丟棄它。 - 分支 3:如果堆滿了,並且
x
大於堆中的最小元素,請從堆中刪除最小的元素並添加x
。
使用頂部爲最小元素的堆,我們可以跟蹤最大的k
個元素。我們來分析這個算法的性能。對於每個元素,我們執行以下操作之一:
- 分支 1:將元素添加到堆是
O(log k)
。 - 分支 2:找到堆中最小的元素是
O(1)
。 - 分支 3:刪除最小元素是
O(log k)
。添加x
也是O(log k)
。
在最壞的情況下,如果元素按升序出現,我們總是執行分支 3。在這種情況下,處理n
個元素的總時間是O(n log k)
,對於n
是線性的。
4.代碼實現:
/**
* @Author Ragty
* @Description 有界堆排序
* @Date 19:49 2019/6/12
**/
public List<T> topK(int k,List<T> list,Comparator<T> comparator) {
PriorityQueue<T> heap = new PriorityQueue<T>(list.size(),comparator);
for (T element : list) {
if (heap.size() < k) {
heap.offer(element);
continue;
}
int cmp = comparator.compare(element,heap.peek());
if (cmp>0) {
heap.poll();
heap.offer(element);
}
}
List<T> res = new LinkedList<T>();
while (!heap.isEmpty()) {
res.add(heap.poll());
}
return res;
}
測試代碼:
list = new ArrayList<Integer>(Arrays.asList(6, 3, 5, 8, 1, 4, 2, 7));
List<Integer> queue = sorter.topK(4, list, comparator);
System.out.println(queue);
5.空間複雜性
到目前爲止,我們已經談到了很多運行時間的分析,但是對於許多算法,我們也關心空間。例如,歸併排序的一個缺點是它會複製數據。在我們的實現中,它分配的空間總量是O(n log n)
。通過優化,可以將空間降至O(n)
。
相比之下,插入排序不會複製數據,因爲它會原地排序元素。它使用臨時變量來一次性比較兩個元素,並使用一些其它局部變量。但它的空間使用不取決於n
。
我們的堆排序實現創建了新PriorityQueue
,來存儲元素,所以空間是O(n)
; 但是如果你能夠原地對列表排序,則可以使用O(1)
的空間執行堆排序 。
剛剛實現的有界堆棧算法的一個好處是,它只需要與k
成正比的空間(我們要保留的元素的數量),而k
通常比n
小得多 。
軟件開發人員往往比空間更加註重運行時間,對於許多應用程序來說,這是適當的。但是對於大型數據集,空間可能同等或更加重要。例如:
- 如果一個數據集不能放入一個程序的內存,那麼運行時間通常會大大增加,或者根本不能運行。如果你選擇一個需要較少空間的算法,並且這樣可以將計算放入內存中,則可能會運行得更快。同樣,使用較少空間的程序,可能會更好地利用 CPU 緩存並運行速度更快。
- 在同時運行多個程序的服務器上,如果可以減少每個程序所需的空間,則可以在同一臺服務器上運行更多程序,從而降低硬件和能源成本。