排序算法(三)堆排序及有界堆排序Java實現及分析


1.堆排序

基數排序適用於大小有界的東西,除了他之外,還有一種你可能遇到的其它專用排序算法:有界堆排序。如果你在處理非常大的數據集,你想要得到前 10 個或者前k個元素,其中k遠小於n,它是很有用的。

例如,假設你正在監視一 個Web 服務,它每天處理十億次事務。在每一天結束時,你要彙報最大的k個事務(或最慢的,或者其它最 xx 的)。一個選項是存儲所有事務,在一天結束時對它們進行排序,然後選擇最大的k個。需要的時間與nlogn成正比,這非常慢,因爲我們可能無法將十億次交易記錄在單個程序的內存中。我們必須使用“外部”排序算法。

我們首先了解一下堆,這是一個類似於二叉搜索樹(BST)的數據結構。有一些區別:

  • 在 BST 中,每個節點x都有“BST 特性”:x左子樹中的所有節點都小於x,右子樹中的所有節點都大於x
  • 在堆中,每個節點x都有“堆特性”:兩個子樹中的所有節點都大於x
  • 堆就像平衡的 BST;當你添加或刪除元素時,他們會做一些額外的工作來重新使樹平衡。因此,可以使用元素的數組來有效地實現它們。

現在討論的是小根堆。如果子樹中的節點都小於根節點,則爲大根堆。

堆中最小的元素總是在根節點,所以我們可以在常數時間內找到它。在堆中添加和刪除元素需要的時間與樹的高度h成正比。而且由於堆總是平衡的,所以hlog n成正比。

JavaPriorityQueue使用堆實現。PriorityQueue提供Queue接口中指定的方法,包括offerpoll

  • 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 緩存並運行速度更快。
  • 在同時運行多個程序的服務器上,如果可以減少每個程序所需的空間,則可以在同一臺服務器上運行更多程序,從而降低硬件和能源成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章