排序算法系列——堆排序

堆排序同直接選擇排序一樣是選擇排序的一種。堆排序是藉助一種數據結構——堆來完成排序,堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
什麼是堆
關於二叉樹這裏就不敘述了。堆(二叉堆)可以視爲一棵完全的二叉樹,完全二叉樹的一個“優秀”的性質是,除了最底層之外,每一層都是滿的,這使得堆可以利用數組來表示(普通的一般的二叉樹通常用鏈表作爲基本容器表示),每一個結點對應數組中的一個元素。
對和數組的關係
從上面可以看出,如果對一個堆從上往下,從左往右進行遍歷的話,正好和數組的順序是一致的。所以對於給定一個節點(下標i)我們可以很容易計算出其子節點以及父節點的下標:

  • Parent(i) = floor(i/2),i 的父節點下標
  • Left(i) = 2i,i 的左子節點下標
  • Right(i) = 2i + 1,i 的右子節點下標

同時堆還有最大堆和最小堆之分。
最大堆:

  • 堆的根節點是最大值
  • 堆中每個節點都大於其子節點,小於其父節點

最小堆:

  • 堆的根節點是最小值
  • 堆中每個節點都小於其子節點,大於其父節點

基本思想
堆排序就是利用堆這種特殊的數據結構來進行排序,比如我們選擇最大堆來進行排序,則我們只需要將待排序的元素構建成一個堆即可,因爲堆中的第一個元素是最大值,所以將堆的第一個元素取出,之後再將剩餘的元素構建成一個新堆,再取出第一個元素,當所有的元素都被取出之後,整個序列就完成了排序。從描述可以看出,這裏是利用堆來從無序部分中獲取最大值,所以堆排序同直接選擇排序的基本思想一樣,都是從無序部分取出一個最大或者最小值放入有序部分,兩者的不同之處在於直接選擇排序是通過對無序部分進行遍歷比較來獲取最大值或者最小值,而堆排序是利用堆這種特殊的數據結構。
實現要點
如果想要對一組數據使用堆排序,首先要做的就是將這組數據構建成一個堆,然後取出堆中的根節點。這裏我們選擇最大值,即根節點是最大值,爲了減少排序的空間複雜度,我們將選出的元素直接與原數組的最後一位進行交換,這樣數組的最後一位就是正確的了,然後我們對剩餘元素重新調整成堆結構,重複此過程直到只剩下一個元素。
這裏主要就是兩個操作過程,一個是建堆,一個是調整堆。
在構建堆的時候一般選擇最後一個元素的父節點作爲起始點進行遞歸構建堆。建堆是一個遞歸的過程,即先構建最下層的小堆,然後逐步往外層構建,最終形成一個完整的堆,因爲當一個節點大於其子節點,那麼如果其父節點大於這個節點,則它的父節點比大於其孫子節點,所以整個堆都是滿足條件的。
在調整堆的時候,由於調整堆是因爲我們拿走了一個元素,所以在調整堆時需要注意整個堆的大小,這個大小會隨着迭代的次數而逐步減小,最終爲1。不同於建構堆,調整堆的時候由於我們將根節點與最後一個節點進行交換,所以我們從上而下進行調整,即將這個交換到根節點的元素逐步沉到其合理的位置。
Java實現

package com.vicky.sort;

import java.util.Arrays;
import java.util.Random;

/**
 * <p>
 * 選擇排序:堆排序
 * 
 * </p>
 * 
 * @author Vicky
 * @date 2015-8-13
 * @param <T>
 */
public class HeapSort {
    /**
     * 排序
     * 
     * @param data
     *            待排序的數組
     */
    public static <T extends Comparable<T>> void sort(T[] data) {
        long start = System.nanoTime();
        if (null == data) {
            throw new NullPointerException("data");
        }
        if (data.length == 1) {
            return;
        }
        buildMaxHeap(data);
        // 末尾與頭交換,交換後調整最大堆
        for (int i = data.length - 1; i > 0; i--) {
            T temp = data[0];
            data[0] = data[i];
            data[i] = temp;
            adjustMaxHeap(data, i, 0);
        }
        System.out.println("use time:" + (System.nanoTime() - start) / 1000000);
    }

    /**
     * 構建最大堆
     * 
     * @param <T>
     * @param data
     * @param index
     *            序列起始點
     */
    public static <T extends Comparable<T>> void buildMaxHeap(T[] data) {
        // 自下而上構建最大堆,即從最後一個元素的父節點開始構建最大堆
        int start = getParentIndex(data.length - 1);
        for (; start >= 0; start--) {
            adjustMaxHeap(data, data.length, start);
        }
    }

    /**
     * 調整最大堆,自下而上
     * 
     * @param <T>
     * @param data
     * @param heapsize
     *            堆的大小,即對data中從0開始到heapsize之間的元素構建最大堆
     * @param index
     *            當前需要構建最大堆的位置
     */
    public static <T extends Comparable<T>> void adjustMaxHeap(T[] data, int heapsize, int index) {
        // 獲取該元素左右子元素
        int left = getLeftChildIndex(index);
        int right = getRightChildIndex(index);
        int max = index;
        // 取三個元素中最大值與父節點進行交換
        if (left < heapsize && data[max].compareTo(data[left]) < 0) {
            max = left;
        }
        if (right < heapsize && data[max].compareTo(data[right]) < 0) {
            max = right;
        }
        if (max != index) {
            swap(data, index, max);
            adjustMaxHeap(data, heapsize, max);
        }
    }

    /**
     * 交換元素
     * 
     * @param <T>
     * @param data
     * @param i
     * @param j
     */
    public static <T extends Comparable<T>> void swap(T[] data, int i, int j) {
        T temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    /**
     * 獲取父節點位置
     * 
     * @param i
     * @return
     */
    public static int getParentIndex(int i) {
        return (i - 1) >> 1;
    }

    /**
     * 獲取左子節點位置
     * 
     * @param current
     * @return
     */
    public static int getLeftChildIndex(int current) {
        return (current << 1) + 1;
    }

    /**
     * 獲取右子節點位置
     * 
     * @param current
     * @return
     */
    public static int getRightChildIndex(int current) {
        return (current << 1) + 2;
    }

    public static void main(String[] args) {
        Random ran = new Random();
        Integer[] data = new Integer[100000];
        for (int i = 0; i < data.length; i++) {
            data[i] = ran.nextInt(100000000);
        }
        HeapSort.sort(data);
        System.out.println(Arrays.toString(data));
    }
}

效率分析

(1)時間複雜度
O(nlogn)
(2)空間複雜度
O(1)
-從空間來看,它只需要一個元素的輔助空間,用於元素的位置交換O(1)。
(3)穩定性
不穩定。
考慮序列{5A,7,13,5B},首先我們構建成一個堆,得到的堆是:{13,7,5A,5B},現在我們取出根節點,然後跟最後一個元素進行交換,再重新調整堆,得到的堆是:{7,5B,5A},再取走根節點,剩餘的堆是:{5A,5B},所以最終得到的排序結果是5B,5A,7,13。顯示堆排序是不穩定的。
參考文章
常見排序算法 - 堆排序 (Heap Sort)
堆排序
堆排序

發佈了55 篇原創文章 · 獲贊 35 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章