Java 數據結構 - 堆和堆排序:爲何快排比堆排序性能好

Java 數據結構 - 堆和堆排序:爲何快排比堆排序性能好

[toc]html

數據結構與算法目錄(http://www.javashuo.com/article/p-qvigrlkr-da.html)java

關於二叉樹這種數據結構在實現軟件工程中的應用,前面咱們已經介紹了紅黑樹,下面咱們再介紹另外一種常見的二叉樹 - 堆。算法

  • 紅黑樹:基於平衡二叉查找樹的動態數據結構,用於快速插入和查找數據,其時間複雜度都是 O(logn)。
  • 堆:按照結點大於等於(或小於等於)子結點,又分爲大頂堆和小頂堆。和紅黑樹不一樣,椎只是部分有序,即 "左結點 < 父結點 && 右結點 < 父結點",而有序二叉樹要求 "左結點 < 父結點 < 右結點"。椎插入和刪除元素的時間複雜度都是 O(logn)。堆常見的應用有優先級隊列和堆排序。

<center><img width=600 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309114535069-1627370722.png'/></center>api

1. 什麼是堆

1.1 堆的定義

堆的嚴格定義以下,只要知足這兩點,它就是一個堆:數組

  1. 堆是一個徹底二叉樹;
  2. 堆中每個節點的值都必須大於等於(或小於等於)其子樹中每一個節點的值。

<b>說明:</b> 和紅黑樹不一樣,椎並是一個部分有序隊列,尤爲要注意如下兩點。數據結構

  1. 堆是徹底二叉樹,所以堆這種數據都是用<b>數組</b>進行存儲。
  2. 對於每一個節點的值都大於等於子樹中每一個節點值的堆,叫做<b>大頂堆</b>。反之則是<b>小頂堆</b>。

1.2 堆的常見操做

堆的常見操做兩個,分別是插入元素和刪除堆頂元素:性能

  • 堆化(heapify):往堆中插入元素叫作堆化。堆化分爲從下往上和從上往下兩種堆化方法。
  • 刪除堆頂元素:咱們知道,堆頂元素是最小或最大元素。刪除堆頂元素後,須要經過從上往下的堆化,使其從新知足堆的定義。

(1)堆化spa

咱們往一個小頂堆中添加新的結點,分析從下往上是如何進行堆化。固然,你也可使用從下往上的堆化。code

<center><img width=200 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309122549671-1097358818.png'/></center>htm

如上圖所示,插入結點時從下往上是堆化爲兩步:

  1. 將數組最後位置添加一個新的結點,也就是 arr[size] = value。
  2. 從這個新結點和父結點依次向上比較並交換,直接從新符合堆的定義。其時間複雜度爲樹的高度,也就是 O(logn)。
private int[] arr;
private int size;

// arr[0] 不存儲任何元素,固然你也能夠將堆總體向前移動一位
public void add(int value) {
    if (size >= capcity) return;
    ++size;
    arr[size] = value;
    int i = size;
    while (i > 0 && arr[i / 2] > arr[i]) {
        swap(arr, i, i / 2);
        i = i / 2;
    }
}

(2)刪除堆頂元素

與插入結點時相反,刪除元素時須要從上至下堆化

<center><img width=700 src='https://img2020.cnblogs.com/blog/1322310/202003/1322310-20200309122942037-853130731.png'/></center>

須要注意的是直接和子結點比較並交換位置,可能會出現數組空洞,不符合徹底二叉樹的定義,如右圖所示出現的數據空洞。解決方案如左所示,先將數組最後一位的結點交換到堆頂,而後再從上至下比較交換。

  1. 將數組最後結點賦值給椎頂結點,也就是 arr[1] = arr[size]。
  2. 從這個新結點和子結點依次向下比較並交換,直接從新符合堆的定義。其時間複雜度爲樹的高度,也就是 O(logn)。
public int poll() {
    if (size <= 0) return -1;
    int value = arr[1];
    int i = 1;
    arr[i] = arr[size];
    arr[size] = 0;
    size--;

    while (true) {
        int minPos = i;
        if (size >= 2 * i && arr[minPos] > arr[2 * i]) minPos = 2 * i;
        if (size >= 2 * i + 1 && arr[minPos] > arr[2 * i + 1]) minPos = 2 * i + 1;
        if (minPos == i) break;

        swap(arr, i, minPos);
        i = minPos;
    }
    return value;
}

2. 堆排序

若是咱們要堆數據結構實現從小到大的排序,該怎麼實現呢?咱們知道將數組堆化成大頂堆後,堆頂是最大值,而後咱們依次取出堆頂元素,這樣取出的元素就是按從大到小的順序,咱們每次取出元素時依次放到數組最後。這樣當所有取出後,就實現了從小到大的排序。堆排序分爲兩步:

  1. 堆化:將數組原地建成一個堆。
  2. 排序:依次取出堆頂元素與數組最後一個元素交換位置。

2.1 堆化

原地堆化也有兩種思路:

  1. 從下往上進行堆化。和插入排序同樣,將數組分爲兩部爲:已經堆化和未堆化。依次遍歷未堆化部爲,將其插入到已經堆化部分。
  2. 從上往下進行堆化。遍歷全部的葉子結點,將其與堆頂結點交換後從上往下進行堆化。
// 從下往上堆化
private static void heavify(Integer[] arr) {
    for (int i = 1; i < arr.length; i++) {
        shiftUp(i, arr);
    }
}
// 大頂堆:從下往上堆化
private static void shiftUp(int i, Integer[] arr) {
    while (i > 0 && arr[i] > arr[(i - 1) / 2]) {
        swap(arr, i, (i - 1) / 2);
        i = (i - 1) / 2;
    }
}

// 從上往下堆化,對於徹底二叉樹而言,號子結點起始位置: "arr.length / 2 + 1"
public void heavify(int[] arr) {
    for (int i = arr.length / 2; i >= 1; --i) {
        // 隨機數據的插入和刪除,堆的長度會發生變化,須要第二個參數來控制向下堆化的最大位置
        shiftDown(i, arr.length, arr[i]);
    }
}
// 大頂堆:從上往下堆化
private static void shiftDown(int i, int size, Integer[] arr) {
    while (true) {
        int maxPos = i;
        if (size > 2 * i + 1 && arr[maxPos] < arr[2 * i + 1]) {
            maxPos = 2 * i + 1;
        }
        if (size > 2 * i + 2 && arr[maxPos] < arr[2 * i + 2]) {
            maxPos = 2 * i + 2;
        }
        if (maxPos == i) {
            break;
        }
        swap(arr, i, maxPos);
        i = maxPos;
    }
}

<b>說明:</b> 最核心的方法是向上堆化 shiftUp 和 向下堆化 shiftDown 這兩個方法,能夠參考 PriorityQueue 小頂堆的實現。

2.2 排序

排序一樣是將數據分爲已經排序和未排序部分。其中未排序部分是一個大頂堆,依次從大頂堆中取出最大元素,插入已經排序部分。

public void sort(Integer[] arr) {
    heavify(arr);
    // 從大頂堆中取出最大元素,依次插入已經排序部分
    doSort(arr);
}

private void doSort(Integer[] arr) {
    int n = arr.length;
    for (int i = 1; i < n; i++) {
        swap(arr, 0, n - i);
        shiftDown(0, n - i, arr);
    }
}

2.3 三大指標

(1)時間複雜度

  • 堆化時間複雜度:O(n)

    層數   元素個數  時間複雜度  總時間複雜度
              ○             1      1          h-1     h-1    
          ○       ○         2      2          h-2     2(h-2)
        ○   ○   ○   ○       3      n/8        2       n/2^3 * 2
       ○ ○ ○ ○ ○ ○ ○ ○      4      n/4        1       n/2^2 * 1
              ...           h      n/2        0       0
    
    S = n/2 + 2n/4 + 3n/16 + 4n/32 + 2(h-2) + (h-1) = O(n)
  • 排序時間複雜度:O(nlogn)

若是數據本來就是有序的,堆排序在堆化過程會打亂原先的順序,再進行排序,所以,即使是徹底有序數組的時間複雜度是 O(nlogn)。但對徹底逆序的數組,其時間複雜度也是 O(nlogn)。總的來講,堆排序的時間複雜度很是穩定。

(2)空間複雜度

堆排序是原地排序。

(3)穩定性

在堆化的過程當中,會出現非相鄰元素交換,所以堆排序是非穩定排序。

2.4 堆排序 vs 快速排序

在實際開發中,爲何快速排序要比堆排序性能好?

  1. 堆排序對 CPU 不友好。快速排序來講局部順序訪問,而堆排序則是逐層訪問。 好比,堆排序會依次訪問數組下標是 1,2,4,8 的元素,而不是像快速排序那樣,局部順序訪問。
  2. 堆排序算法的數據交換次數要多於快速排序。好比,有序數組建堆後,數據反而變得更無序。

3. 優先級隊列

如 Java 中的優先級隊列 PriorityQueue 和 PriorityBlockingQueue 就是使用<b>小頂堆</b>實現的。

  • 合併有序小文件
  • 高性能定時器
  • Top N
  • 利用堆求中位數

天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索