1. 程式人生 > >堆排序:思路與實現

堆排序:思路與實現

學習了一下堆排序的思想,分享一下我的理解。

首先介紹一些概念。

堆(heap),最大堆(max heap),最小堆(min heap)

堆是一種特別的樹狀結構,普通的樹結構,沒有對子節點也特別的規定,但堆是一顆完全的樹,除了最底層,上面的每一層都是滿的。

如果一個堆中所有的節點,它有用子節點的話,並且這個節點大於它的子節點,那麼這個堆就是最大堆

如果一個堆中所有的節點,它有用子節點的話,並且這個節點小於它的子節點,那麼這個堆就是最小堆

#堆的資料結構與特徵 堆是一種樹結構,但是我們卻可以使用陣列來表示,因為它是一個完全二叉樹。所以我們給定一個數組,比如:[1, 2, 3, 4, 5, 6, 7, 8],就可以構造一個堆。

構造的方法,在圖紙上,很簡單,按順序,以樹的結構書寫數字就好了。

       1
     /   \
    2     3
   / \   / \
  4   5 6   7
 /  
8

因此,它可以直接用陣列來表示。

用陣列表示的時候,index有一些特徵。 節點index = i,它的左子節點的index = 2 * i + 1,它的右子節點的index = 2 * i + 2。 如果陣列長度為length,那麼,它最後一個擁有子節點的節點index = length / 2 - 1

堆排序的基本流程

1.將給定的陣列按照堆去理解(或者說構造成堆也行) 2.將這個堆進行調整,調整為大根堆或者小根堆,接下來,我們將堆的頂端的元素拿走放進一個新佇列中儲存起來,將最後一個元素放在堆的根節點上。 3.重複2 在每次對堆進行調整後,都可以得到最大值or最小值,每次取走這個值,堆會越變越小,最後也就排好了。

接下來我用一個圖來表示堆排序的過程: 在這裡插入圖片描述

演算法的實現

註釋我寫的非常清楚了,大家自己看下理解一下吧。

enum SortType {
    Increase, Decrease
}

/**
 * 堆排序
 * @param input 輸入資料
 * @param sortType 排序型別 升序還是降序
 * */
public static int[] heapSort(int[] input, SortType sortType) {

    int[] heap = input;
    int heapLength = input.length; // 初始化堆的大小,每次找到一個數後,堆的大小都減少1,不去操作最後一個已經排好的數
for (int i = 0; i < input.length; i++) { heap = maxHeapify(heap, heapLength, sortType); // 進行堆調整,只調整heapLength的區域 swap(heap, 0, heapLength - 1); // 調整完成後,將第一個元素後最後一個元素交換 heapLength--; // 堆的大小減一 } return heap; } /** * 堆調整 * */ public static int[] maxHeapify(int[] heap, int heapLength, SortType sortType) { if (heap == null) { return null; } if (heap.length == 1) { return heap; } // 標記是否改變過,如果改變了,需要再次執行本方法,直到沒有變化為止 boolean isChange = false; for (int i = heapLength / 2 - 1; i >= 0; i--) { // 從最後一個有children的節點開始,向前遍歷 int leftIndex = i * 2 + 1; // left child index int rightIndex = i * 2 + 2; // right child index if (heapLength > leftIndex) { if (sortType == SortType.Increase) { if (heap[i] < heap[leftIndex]) { // 判斷當前節點和左節點的大小,進行交換 swap(heap, i, leftIndex); isChange = true; // 每次交換,需要標記,發生了改變,這個過程需要再來一次 } } else if (sortType == SortType.Decrease) { if (heap[i] > heap[leftIndex]) { swap(heap, i, leftIndex); isChange = true; } } if (heapLength > rightIndex) { // 處理右節點,如果存在的話 if (sortType == SortType.Increase) { if (heap[i] < heap[rightIndex]) { swap(heap, i, rightIndex); isChange = true; } } else if (sortType == SortType.Decrease) { if (heap[i] > heap[rightIndex]) { swap(heap, i, rightIndex); isChange = true; } } } } } if (isChange) { // 如果改變了,需要再來一次 return maxHeapify(heap, heapLength, sortType); } else { // 如果每個節點都OK了,返回 return heap; } } public static void swap(int[] arr, int i, int j) { int v = arr[i]; arr[i] = arr[j]; arr[j] = v; }

最後測試一下效果:

用了三組資料來測試,一組最差的情況,一組最好的情況,另外一組隨機的情況

public static void main(String[] args) {

    int loopCount = 100000;

    int[] result = null;

    long t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Decrease);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));

    t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Increase);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));

    int[] arr = new int[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = (int) (Math.random() * 100);
    }

    t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(arr, SortType.Increase);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));
}

執行結果:

可以看到,隨機的情況,耗時最少,最差的情況,耗時也很少,最好的情況,反而耗時最多。大概和調整為最大堆和最小堆的時候有關。當我需要從小到大排列的時候,我反而需要將排好的資料構造成最大堆,然後再取出最大的資料放到陣列最後。

36 // 最差的情況
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
49 // 最好的情況
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
20 // 隨機生成的資料
[4, 5, 11, 21, 22, 44, 66, 79, 93, 99]

如果使用額外的空間來儲存,不關心最大堆還是最小堆,會怎麼樣呢?

總是得到最小堆,然後再用額外的空間來儲存結果。

/**
 * 堆排序
 * @param input 輸入資料
 * @param sortType 排序型別 升序還是降序
 * */
public static int[] heapSort(int[] input, SortType sortType) {

    if (input == null || input.length == 0) {
        return null;
    }

    int[] heap = input;
    int[] result = new int[input.length];
    int heapLength = input.length; // 初始化堆的大小,每次找到一個數後,堆的大小都減少1,不去操作最後一個已經排好的數
    for (int i = 0; i < input.length; i++) {
        heap = maxHeapify(heap, heapLength, SortType.Decrease); // 進行堆調整,只調整heapLength的區域

        // Decrease 排序的話,總是得到最小堆
        if (sortType == SortType.Increase) {
            result[i] = heap[0];
        } else {
            result[input.length - i - 1] = heap[0];
        }

        swap(heap, 0, heapLength - 1); // 調整完成後,將第一個元素後最後一個元素交換
        heapLength--; // 堆的大小減一
    }

    return result;
}

執行結果:

33
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
32
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
23
[12, 13, 18, 21, 38, 72, 73, 86, 91, 97]

最好情況所花的時間確實減少了,但對平均情況來說卻沒什麼影響的,總得來說平均情況是非常穩定的。