排序算法(五) —— 堆排序

堆排序(Heap Sort),是指将整个数组转化成“堆”这种数据结构,利用堆的性质,去简化排序的过程。堆排序,可以看做是“变治法”的一种实现。

From Wikipedia:https://en.wikipedia.org/wiki/Heapsort

1. 堆

堆是一种特殊的二叉树结构,但是堆总是需要满足以下两条性质:

  • 堆是一棵完全二叉树。即树的每一层都是满的,除了最后一层最右边的元素有可能出现缺位。
  • 父母优势。即堆中的所有子节点,总是不大于或不小于其父节点的值。

根据子节点与父节点的关系,可以将堆分为最大堆和最小堆两种情况。

2. 堆的数组表现形式

数组 { 9, 7, 3, 5, 1, 2, 6, 0, 8, 4 } ,可以转化为以下二叉树结构。(目前不具备堆的性质)

 

3. 堆的性质(仅罗列在堆排序中用到的性质)

3.1 获取堆中某个节点的左节点、右节点、父节点

    public static int left(int i) {
        return (i << 1) + 1;
    }

    public static int right(int i) {
        return (i << 1) + 2;
    }

    public static int parent(int i) {
        return (i - 1) >>> 1;
    }

3.2 保持最大堆/最小堆的性质

当二叉树中的某个子节点,其左子树和右子树都具备堆的性质,那么可以通过一系列的交换,将以该子节点为根节点的树,转化为堆的结构。

    public static void maxHeapify(int[] array, int rootIndex, int lastIndex) {
        int l = left(rootIndex);
        int r = right(rootIndex);
        int largestIndex = rootIndex;
        if (l <= lastIndex && array[l] > array[largestIndex]) {
            largestIndex = l;
        }
        if (r <= lastIndex && array[r] > array[largestIndex]) {
            largestIndex = r;
        }
        if (rootIndex != largestIndex) {
            ArrayHelper.swap(array, rootIndex, largestIndex);
            maxHeapify(array, largestIndex, lastIndex);
        }
    }

3.3 构造最大堆/最小堆

将一个不具备堆性质的数组,转化为堆的结构,一般都是自底向上开始的。

自二叉树的第一个非叶节点开始,自底向上,依次保持二叉树的堆的性质,直至根节点。

    public static void buildMaxHeap(int[] array) {
        int lastNonLeaf = (array.length - 2) >>> 1;
        for (int i = lastNonLeaf; i > -1; --i) {
            maxHeapify(array, i);
        }
    }

4. 堆排序

在了解了堆的各种性质之后,堆排序就显得非常简单了。

  • 第一步:将原数组转化为堆的形式。(升序排列使用最大堆,降序排列使用最小堆,此处假定进行升序排列)
  • 第二步:将数组最后一位与第一位交换,因为是最大堆的关系,第一位数字就是最大值,落在了最后一位上。
  • 第三步:剔除最后一位数字,将数组剩余部分,看作一颗新的二叉树。其中根节点的左子树和右子树都满足最大堆的性质,保持这棵新二叉树的最大堆性质。
  • 第四步:循环第二步,第三步,直至根节点,整个数组即自然有序。
    public void sort(int[] array) {
        HeapHelper.buildMaxHeap(array);
        for (int i = array.length - 1; i > 0; --i) {
            ArrayHelper.swap(array, i, 0);
            HeapHelper.maxHeapify(array, 0, i - 1);
        }
    }

5. 堆排序的演练过程

以下演示堆排序在数组 { 9, 7, 3, 5, 1, 2, 6, 0, 8, 4 } 上的应用:

  • 构建最大堆:{ 9, 8, 6, 7, 4, 2, 3, 0, 5, 1 } 

 

  • 交换第一位和最后一位:{ 1, 8, 6, 7, 4, 2, 3, 0, 5, 9 } 
  • 保持二叉树 { 1, 8, 6, 7, 4, 2, 3, 0, 5 } 的最大堆性质,得到新的数组:{ 8, 7, 6, 5, 4, 2, 3, 0, 1, 9 } 
  • 交换第一位和倒数第二位:{ 1, 7, 6, 5, 4, 2, 3, 0, 8, 9 } 
  • 保持二叉树 { 1, 7, 6, 5, 4, 2, 3, 0 } 的最大堆性质,得到新的数组:{ 7, 5, 6, 1, 4, 2, 3, 0, 8, 9 } 
  • 交换第一位和倒数第三位:{ 0, 5, 6, 1, 4, 2, 3, 7, 8, 9 } 
  • 保持二叉树 { 0, 5, 6, 1, 4, 2, 3 } 的最大堆性质,得到新的数组: { 6, 5, 3, 1, 4, 2, 0, 7, 8, 9 }
  • 交换第一位和倒数第四位:{ 0, 5, 3, 1, 4, 2, 6, 7, 8, 9 } 
  • 保持二叉树 { 0, 5, 3, 1, 4, 2 } 的最大堆性质,得到新的数组: { 5, 4, 3, 1, 0, 2, 6, 7, 8, 9 } 
  • 交换第一位和倒数第五位:{ 2, 4, 3, 1, 0, 5, 6, 7, 8, 9 } 
  • 保持二叉树 { 2, 4, 3, 1, 0 } 的最大堆性质,得到新的数组:{ 4, 2, 3, 1, 0, 5, 6, 7, 8, 9 } 
  • 交换第一位和倒数第六位:{ 0, 2, 3, 1, 4, 5, 6, 7, 8, 9 } 
  • 保持二叉树 { 0, 2, 3, 1 } 的最大堆性质,得到新的数组:{ 3, 2, 0, 1, 4, 5, 6, 7, 8, 9 } 
  • 交换第一位和倒数第七位:{ 1, 2, 0, 3, 4, 5, 6, 7, 8, 9 } 
  • 保持二叉树 { 1, 2, 0 } 的最大堆性质,得到新的数组:{ 2, 1, 0, 3, 4, 5, 6, 7, 8, 9 } 
  • 交换第一位和倒数第八位:{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } 
  • 保持二叉树 { 0, 1 } 的最大堆性质,得到新的数组:{ 1, 0, 2, 3, 4, 5, 6, 7, 8, 9 } 
  • 交换第一位和倒数第九位:{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } 

至此,整个堆排序过程结束。

6. 堆排序的时间复杂度

堆排序由两部分组成:

  • 构建最大堆
  • 循环保持最大堆的性质

6.1 构建最大堆的时间复杂度 T = O(n)

构建最大堆的过程,是从第一个非叶节点开始的,即数组的1/2处。

考虑最坏情况:

  • 二叉树的倒数第二行,总会与最后一行发生一次比较/交换。
  • 二叉树的倒数第三行,总会与倒数第二行发生一次比较/交换,与最后一行发生一次比较/交换。
  • ...
  • 二叉树的第一行,总会与它下面的每一行都发生一次比较/交换,即交换次数为二叉树高度 d=log2n。

将这些比较/交换次数累加:

T = (sum_{k = 1}^{lg(n)}{{1 over {2^k}} 	imes k} ) * n = O(n)

6.2 循环保持最大堆的性质的时间复杂度 T = O(n*log2n)

这一步的时间计算就相对比较明显了,在n次循环的内部,进行时间复杂度为 O(log2n) 的 maxHeapify()。

所以总时间复杂度 T = O(n) + O(n*log2n) = O(n*log2n)

Source Code: https://github.com/Gerrard-Feng/algorithm-learning.git

原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/9017120.html