堆排序图解与代码示例

简单选择排序的缺点

没有把每一趟的比较结果存下来,在后一趟的比较中,有许多比较在前一趟已经做过了。堆排序是简单选择排序的一种改良。是一种数据结构,在讲解堆结构之前,我们先了解下满二叉树和完全二叉树。

满二叉树

在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的二叉树称为满二叉树。
在这里插入图片描述
满二叉树的特点有:

  • 叶子只能出现在最下一层
  • 非叶子结点的度(结点拥有的子树数)一定是2
  • 在同样深度的二叉树中,满二叉树的结点个数最多,叶子个数最多

完全二叉树

对一棵具有n个结点的二叉树按照层序编号,如果编号为i(0 ≤ i < n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。

在这里插入图片描述
上图是一棵满二叉树。

在这里插入图片描述
上图是一棵完全二叉树。

在这里插入图片描述
上图中两个二叉树都不是完全二叉树。很显然,它们有几个结点的编号和满二叉树中相同位置的结点不同。

从这里也可以得出一些完全二叉树的特点:

  • 叶子结点只能出现在最下两层
  • 最下层的叶子一定集中在左部连续位置
  • 倒数第二层若有叶子结点,一定都在右部连续位置
  • 若结点的度为1,则该结点只有左孩子
  • 同样结点数的二叉树,完全二叉树的深度最小

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值称为大顶堆。下图是一个大顶堆:

在这里插入图片描述

每个结点的值都小于或等于其左右孩子结点的值称为小顶堆。下图是一个小顶堆:

在这里插入图片描述
由堆的定义可知,根结点的值一定是所有结点中最大或最小的。

如果按照层序遍历的方式给结点从0开始编号,假设i是编号,k是结点的值,n是结点个数,则有如下规律:

在这里插入图片描述
如果i=0,则结点i是二叉树的根,无双亲;如果i>0, 则其双亲是结点(i-1)/2(向下取整);i的左右孩子结点是2i+1和2i+2。

上文中的大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的关系表达式。

在这里插入图片描述

堆排序算法

思路

堆排序(Heap Sort)就是利用堆(假设使用大顶堆)进行排序的算法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它与堆数组末尾元素交换,此时末尾元素就是最大值,然后将剩余的n-1个元素重新构造成一个堆,这样就会得到n个元素中的第二大的值。如此反复执行,便能得到一个有序序列了。

如图,将最大值90与堆数组末尾元素20互换,此时90就成了整个堆序列的最后一个元素。

在这里插入图片描述
将20经过调整,使得除90以外的结点继续满足大顶堆定义。然后再考虑将30与80互换位置。。。。。。

在这里插入图片描述

堆排序的基本思想就是这样,不过要实现它还需要解决两个问题:

  1. 如何由一个无序序列构成一个堆?
  2. 如何在输出堆顶元素后,调整剩余元素成为一个新的堆?

代码示例


/**
 * 堆排序,时间复杂度O(nlogn)
 */
public class HeapSort {

    /**
     * 从小到大排序
     * @param arr
     */
    public static void sortAsc(int[] arr) {
        int i;
        // (arr.length >>> 1) - 1对应公式中的(n / 2) - 1
        // 从这个结点开始,编号递减,也就是从右往左,从下往上遍历
        for (i = (arr.length >>> 1) - 1; i >= 0; i--) {
            heapAdjustAsc(arr, i, arr.length - 1);
        }
        // 从右往左遍历序列并与0号结点交换位置
        // 对于大顶堆来说,0号结点的值最大,也就是较大值一直往后移
        // 数组左边是未排序序列,右边是已排序序列
        // 未排序序列越来越短,已排序序列越来越长
        for (i = arr.length - 1; i > 0; ) {
            swap(arr, 0, i--);
            heapAdjustAsc(arr, 0, i); // 每次交换完位置都需要调整元素位置以满足大顶堆特性
        }
    }

    /**
     * 从大到小排序
     * @param arr
     */
    public static void sortDesc(int[] arr) {
        int i;
        for (i = (arr.length >>> 1) - 1; i >= 0; i--) {
            heapAdjustDesc(arr, i, arr.length - 1);
        }
        for (i = arr.length - 1; i > 0; ) {
            swap(arr, 0, i--);
            heapAdjustDesc(arr, 0, i);
        }
    }

    /**
     * 使用大顶堆进行从小到大排序
     * @param arr
     * @param s 起始位置
     * @param m 最大边界
     */
    private static void heapAdjustAsc(int[] arr, int s, int m) {
        int d = arr[s];
        // 这个循环是从结点s开始遍历它的子孙,可以看成是从上往下遍历,不过不是遍历所有子孙,而是单线路的
        // 正因为是单线路的,所以它的比较次数比简单选择排序少,排序效率更高
        for (int j = (s << 1) + 1; j <= m; j = (j << 1) + 1) {
            // s是父结点,j是它的左孩子,j + 1是它的右孩子(如果有的话)
            if (j < m && arr[j] < arr[j + 1]) // 选取值最大的那个孩子
                ++j;
            if (d >= arr[j]) // 父结点的值比它的两个孩子的值要大,无须调整
                // 因为外层循环(sortAsc这个方法的for循环)是从下往上调整,所以当父结点的值比它的两个孩子的值大时,
                // 肯定也比它所有子孙的值大,因此能直接break
                break;
            arr[s] = arr[j];
            s = j;
        }
        arr[s] = d;
    }

    /**
     * 使用小顶堆进行从大到小排序
     * @param arr
     * @param s 起始位置
     * @param m 最大边界
     */
    private static void heapAdjustDesc(int[] arr, int s, int m) {
        int d = arr[s];
        for (j = (s << 1) + 1; j <= m; j = (j << 1) + 1) {
            if (j < m && arr[j] > arr[j + 1])
                ++j;
            if (d <= arr[j])
                break;
            arr[s] = arr[j];
            s = j;
        }
        arr[s] = d;
    }

    private static void swap(int[] arr, int i, int j) {
        int d = arr[i];
        arr[i] = arr[j];
        arr[j] = d;
    }

    public static void main(String[] args) {
        testCase5();
    }

    private static void testCase1() {
        int[] arr = {50, 10, 90, 30, 70, 40, 80, 60, 20};
        sortAsc(arr);
        print(arr);
    }

    private static void testCase2() {
        int[] arr = {50};
        sortAsc(arr);
        print(arr);
    }

    private static void testCase3() {
        int[] arr = {50, 10, 90, 30, 70, 40, 80, 60, 20};
        sortDesc(arr);
        print(arr);
    }

    private static void testCase4() {
        int[] arr = {50};
        sortDesc(arr);
        print(arr);
    }

    private static void testCase5() {
        int[] arr = {7, 5, 9, 9, 2, 2, 1};
        sortDesc(arr);
        print(arr);
    }

    private static void print(int[] arr) {
        for (int e : arr) {
            System.out.print(e + " ");
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值