普林斯顿公开课 算法4-3:堆排

堆排的灵感源自于堆的数据结构。它是一种原地排序算法,不需要额外的临时数组。

基本思想

堆排的基本思想是:

  1. 先建立一个最大堆
  2. 将最大的元素移动到数组末尾,减小堆的大小,调整最大堆使其符合最大堆的性质
  3. 重复第二步,直到最大堆耗尽为止

第一个步骤建立最大堆的代码非常简单,只要对每个节点执行sink操作即可。


1

2

for(int k
= N/
2;
k >= 
1;
k--)

    sink(a,
k, N);

第二个步骤也很简单,代码如下:


1

2

3

4

while(N
1)
{

    exch(a, 1,
N--);

    sink(a, 1,
N);

}

动画

下图是堆排算法的动画:

http://www.sorting-algorithms.com/animation/50/random-initial-order/heap-sort.gif

复杂度

堆排在构建堆的过程中需要2N次比较和交换。

在排序的过程中需要NlgN次比较和交换。

堆排是一种最坏情况下复杂度为NlogN的原地排序算法,这两种特性是归并排序和快排都做不到的。

堆排不是一种稳定的排序算法。不能很好的利用CPU缓存,因此性能一般。

下图展示了排序算法与算法特性之间的关系

代码

public class HeapSort {
    public static void sort(Comparable[] a) {
        // 需要假想数组a是从1开始编号的
        int N = a.length;

        // 建立最大堆
        for(int i=N/2;i>=1;i--){ // 注意,从最底层开始建立,而不是从最顶层开始建立
            sink(a, i, N);
        }

        // 依次取出最大元素,放在末尾
        while(N > 1) {
            exch(a, 1, N);
            N--;
            sink(a, 1, N);
        }
    }

    public static void sink(Comparable[] a, int k, int N) {
        // 循环直到没有子节点为止
        while(k*2 <= N) {
            int parent = k;
            int child1 = k*2;
            int child2 = child1+1;

            // 如果父亲比任意一个子节点要小
            if(less(a,parent,child1,N) || less(a,parent,child2,N)){
                // 选出较大的子节点
                int greater;
                if(less(a,child1,child2,N)) {
                    greater = child2;
                } else {
                    greater = child1;
                }

                // 将较大的节点与父节点交换
                exch(a, greater, parent);

                // 为下次循环做准备
                k = greater;
            } else {
                break; // 注意:不要忘记跳出循环
            }
        }
    }

    public static void exch(Comparable[] a, int x, int y) {
        // 将以1为起点的数组转换成以0为起点的数组
        x-=1;y-=1;
        SortUtil.exch(a, x,y);
    }

    public static boolean less(Comparable[] a, int x, int y, int N) {
        // 将以1为起点的数组转换成以0为起点的数组
        x-=1;y-=1;

        // 将null值视为无穷小
        if(x>=N) return true;
        if(y>=N) return false;
        return SortUtil.less(a[x],a[y]);
    }
}

普林斯顿公开课 算法4-3:堆排

时间: 2024-08-10 23:30:02

普林斯顿公开课 算法4-3:堆排的相关文章

普林斯顿公开课 算法3-4:快排的应用

排序的应用 排序算法有着广泛的应用. 典型的应用有 对名称进行排序 排序MP3音乐文件 显示Google的搜索结果 按标题顺序列出RSS订阅 排序之后下列问题就变得非常简单了 找出中位数 数据库中的二分查找 找出统计数据中的异常值 在邮箱中找出重复的邮件 不是特别典型的应用有 数据压缩 计算机图形 计算生物 负载平衡 编程语言中的排序算法 java中对于基本类型使用快排,对于引用类型使用归并排序.因为归并排序很稳定,而且保证复杂度为NlgN Java.C/C++中的快排都有以下特性: 对于小的子

普林斯顿公开课 算法3-3:三路快排

很多时候排序是为了对数据进行归类,比如对城市进行排序,对员工的职业进行排序.这种排序的特点就是重复的值特别多. 如果使用普通的快排对这些数据进行排序,会造成N^2复杂度,但是归并排序和三路快排就没有这样的问题. 三路快排 三路快排的基本思想就是,在对数据进行分区的时候分成左中右三个部分,中间都是相同的值,左侧小于中间,右侧大于中间. 性能 三路快排的复杂度比普通快排小,主要取决于数据中重复数据的数量.重复数据越多,三路快排的复杂度就越接近于N. 代码 public class Quick3 {

普林斯顿公开课 算法4-2:二叉堆

二叉树 介绍二叉堆之前首先介绍二叉树.二叉树有一个根节点,节点下又有两个子节点.完全二叉树是指一个二叉树树除了最底层,其他层都是完全平衡的. 完全二叉树最基本的性质就是它的高度是 floor(lgN). 二叉堆 二叉堆是完全二叉树的一种,每个节点对应一个数值,而且这个数值都大于等于它子节点的数值. 下图是一个二叉堆. 二叉堆的储存 由于二叉堆是完全二叉树,所以它可以用一个数组进行储存.所以不需要创建节点对象,再建立节点之间的连接.这样节省了很多开销. 用数组a[]表示一个二叉堆有以下特性: a[

普林斯顿公开课 算法4-1:优先级队列API和基本实现

优先级队列是容器的一种,可以向优先级队列中添加或取出数据,取出数据时只能取出最大的数或最小的数.而其他的一些容器比如队列和栈,取出的顺序跟插入的顺序是有关的. 优先级队列的接口如下: public class MaxPQ<Key extends Comparable<Key>> { MaxPQ(); void insert(Key x); Key popMax(); boolean isEmpty(); } 优先级队列的应用 事件驱动的模拟 数字运算 数据压缩 图的查找 数论 人工

普林斯顿公开课 算法1-10:并查集-优化的快速合并方法

应用 渗透问题 游戏中会用到. 动态连接 最近共同祖先 等价有限状态机 物理学Hoshen-Kopelman算法:就是对网格中的像素进行分块 Hinley-Milner多态类型推断 Kruskai最小生成树 Fortran等价语句编译 形态学开闭属性 Matlab中关于图像处理的bwlabel函数 渗透问题 一个N×N的矩阵,判断顶部和底部是否连通就是渗透问题. 下图中左侧的矩阵能渗透,右侧矩阵不能渗透. 渗透问题在电学.流体力学.社会交际中都有应用. 在游戏中可能需要生成一张地图,但是作为地图

普林斯顿公开课 算法1-11:并查集的应用

应用 渗透问题 游戏中会用到. 动态连接 最近共同祖先 等价有限状态机 物理学Hoshen-Kopelman算法:就是对网格中的像素进行分块 Hinley-Milner多态类型推断 Kruskai最小生成树 Fortran等价语句编译 形态学开闭属性 Matlab中关于图像处理的bwlabel函数 渗透问题 一个N×N的矩阵,判断顶部和底部是否连通就是渗透问题. 下图中左侧的矩阵能渗透,右侧矩阵不能渗透. 渗透问题在电学.流体力学.社会交际中都有应用. 在游戏中可能需要生成一张地图,但是作为地图

普林斯顿公开课 算法2-1:排序概述

目标 对所有类型的数据进行排序. 问题 排序函数如何知道比较的是哪种类型的数据呢? 回调函数 这时候就需要引入回调函数的概念了.回调函数就是将可执行的代码作为参数进行传递. 实现回调的方法 在Java中可以通过接口来实现,在C语言中可以通过函数指针来实现,C++中可以通过class-type functor,也就是重载操作符operator ()的类,在C#中可以使用Delegate委托,在Python/Perl/ML/javascript中可以直接传递函数. JDK中提供了Comparable

普林斯顿公开课 算法1-1:算法分析

为什么要分析算法 分析算法能够预測算法的性能,比較算法之间的优劣,保证算法的正确性,理解算法的理论基础. 成功算法的样例 离散傅立叶变换,假设使用暴力方法,那么算法的复杂度是是N^2,假设使用FFT高速傅立叶变换能够实现O(N logN)复杂度 N-body模拟:使用Barnes-hut算法能够将复杂度减少到N logN 顺便发一张N-body模拟的炫图 Barnes-Hut算法示意图 算法分析的步骤 观察问题的特征和想到得到的结果 依据观察结果提出如果 使用如果来预測可能发生的情况 检測预測结

普林斯顿公开课 算法1-2:观察

这章通过一个简单的例子,详细说明算法分析的步骤. 算法 问题 给定N个不同的整数,从中任意取出三个整数.请问有几种情况,使得取出的3个整数之和为0? 解法 可以使用暴力算法,代码如下: 1 2 3 4 5 6 7 8 9 for(int i=0;i<N;i++){     for(int j=0;j<N;j++){         for(int k=0;k<N;k++){             if(a[i]+b[i]+a[k]==0){                 count+