zoukankan      html  css  js  c++  java
  • 排序算法(五) —— 堆排序

    堆排序(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

  • 相关阅读:
    程序员常见的坏习惯,你躺枪了吗?
    程序员常见的坏习惯,你躺枪了吗?
    程序员常见的坏习惯,你躺枪了吗?
    ACM2037
    [Golang]字符串拼接方式的性能分析
    如果一个类同时继承的两个类都定义了某一个函数会怎样呢 | Code4Fun
    Python学习笔记(四)函数式编程
    MySql之增删改查 · YbWork's Studio
    季銮西的博客
    ActiveMQ学习总结(一)
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/9017120.html
Copyright © 2011-2022 走看看