堆排序和其他排序算法的总结

目录

堆的定义和性质

优先级队列     

插入

删除并返回最大关键字的元素

返回最大关键字的元素

修改优先级

创建堆

堆排序和创建堆的时间复杂度分析 

其他排序

链式基数排序

直接插入排序

希尔排序

排序简单总结


没有写过排序算法总结的文章,不算真的程序员,因为排序算法是算法课程里基础的算法,还有如果你没有把基础的排序算法总结成文,那你脑袋里的排序算法可能真的只有选择,插入,冒泡这几个简单可怜的排序算法了,所以你不算真的程序员。真的程序员是博文成堆,码量如流。

*_*,吐槽一下,不要当真。

堆的定义和性质

此番要讲的是二叉堆,他是一颗二叉树,除了最底层之外,该树是从左至右完全充满的;一般通常用数组来装载这个二叉堆的元素;二叉堆有2个表现形式,一个大顶堆,一个是小顶堆,大(小)顶堆是指每个节点的关键字不小于(不大于)孩子的关键字。

为了讲解堆的一些性质和操作,我们先约定成俗一些概念和定义。

         我们用A[]数组来装载二叉堆,A.length表示数组的长度,A.heap-size表示有多少个堆元素存储在A中,且有0<=A.heap-size<=A.length,若给定一个节点的下标,则它的父节点,左右孩子的下标(注:这里的下标是从1开始)是:

 

         节点的高度:该节点到叶节点最长简单路径上边的数目;二叉堆的高度:根节点的高度,记为h。

         堆的一些操作,例如增删改堆中的一个或一些元素,可能会破坏大(小)顶堆的定义,所以从破坏的地方重新恢复大(小)顶堆显得格外重要,下面是从某个元素处开始自顶向下的恢复调整伪码:

伪码是递归调用(当然也可以是循坏结构,具体下面讲优先级队列的时候会有循环结构的实现)且简洁易懂,在这里用下面的图例来加以说明:

创建堆,把一个杂乱无章的数组可以通过如下伪码进而创建一个有章可循的大顶堆:

同样,伪码简洁易懂,在这里用下面的图例来加以说明:

         堆排序出场的时机到了,有了恢复调整堆,创建堆的伪码,进而为下面堆排序的伪码打下了铁打的根基。

同样,伪码简洁易懂,在这里用下面的图例来加以说明:

优先级队列     

堆的应用。堆有很多应用,我知道的一个应用是优先级队列。在一个具有作业调度的共享计算机系统中,最大优先队列维护着将要执行的各个作业和其优先级,当一个作业完成或者中断时,系统调用关于优先队列的相关算法从所有的等待作业的最大优先队列中选出队首作业(即最大优先级的作业)来执行。

         我这里拿openjdk的PriorityQueue源码来讲解堆在优先级队列中使用。

         优先队列上常用的操作有:插入(增加),常看最大关键字的元素,删除一个元素,删除并返回最大关键字的元素,提高某元素的优先级等。

插入

//当然这个也是通过逐个插入元素建堆的方式
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)//若队列里没有元素,则插入队头位置(数组0下标位置)
        queue[0] = e;
    else//往队中插入元素e,先把元素放在数组的末尾
        siftUp(i, e);
    return true;
}
 //选择关键字的比较器
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

@SuppressWarnings("unchecked")
//自底向上逐层父子比较大小,进而使数组符合大顶堆或者小顶堆
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    //默认先把要插入的x放在数组末尾的位置k,并与父节点元素比较大小
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        //k小标位置的节点的父节点元素
        Object e = queue[parent];
        //k小标位置的节点元素与父节点元素比较大小,符合大顶堆或者小顶堆的话,退出往上“比较旅行”的循环
        if (key.compareTo((E) e) >= 0)
            break;
        //不符合大顶堆或者小顶堆的话,k小标位置的节点元素与父节点元素交换
        queue[k] = e;
        //让父节点的小标付给k,让继续下一轮的父子“比较旅行”
        k = parent;
    }
    //经过“比较旅行”,在符合大顶堆或者小顶堆退出循环后,把要插入的x赋给正确的位置上
    queue[k] = key;
}

删除并返回最大关键字的元素

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    //删除的元素是数组中0下标的元素,即队头元素
    E result = (E) queue[0];
    //删除之后把数组末尾元素赋值给队头元素,并自顶而下逐层父子比较大小,
    // 进而使数组符合大顶堆或者小顶堆
    E x = (E) queue[s];//获取数组末尾元素
    queue[s] = null;//并置空数组末尾位置
    if (s != 0)
        siftDown(0, x);//自顶而下逐层父子比较大小,进而使数组符合大顶堆或者小顶堆
    return result;
}
//选择关键字的比较器
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
//自顶(数组中k下标位置开始)而下(至叶节点结束)逐层父子比较大小,进而使数组符合大顶堆或者小顶堆
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1; // assume left child is least
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

返回最大关键字的元素

public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

修改优先级

/**
 * 修改某一个元素,如果修改元素的关键字(即优先级),则要调整堆中(数组中)元素使之成为大顶堆或者小顶堆
 *若提高优先级,则要自底向上比较父子元素(调用siftUp),若降低优先级,则要自顶向下比较父子元素(调用siftDown)
 * @param k 待修改的元素的下标位置
 * @param x 待修改的元素
 */
public void modify(int k, E x){
    //To-Do请读者自己实现
}

创建堆

private void heapify() {
    //超过(size >>> 1) - 1下标位置上的元素都没有孩子节点,所以从该//位置开始自底向上,比较父子大小,进而使数组成为大顶堆或者小顶堆
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

堆排序和创建堆的时间复杂度分析 

大家看到了伪码和代码,捋起来真的是酷夏中冰到极点,爽!可能对先人的知识的应用本是如此,然而,可以这样讲,问一问堆为什么要这样设计?可能还是会让芸芸众生晕晕乎乎一生,可能最好的方法就是去看原作者的论文手稿。然而, too naïvetoo simple,论文是有那么好看的吗?复杂度的分析就可能触碰到你脆弱敏感易碎有坏点的神经网络。

         蛋话少讲,言归堪忧的复杂度。下面关注一下创建堆和堆排序的时间复杂度。

         上面的优先级队列中的heapify其实就是创建堆伪码BUILD-MAX-HEAP的一个实现,而siftDown是伪码MAX-HEAPFY的一个实现,接着来看一看创建堆的复杂度,MAX-HEAPFY是自顶(数组中k下标位置开始)而下(至叶节点结束)逐层父子比较大小,进而使数组符合大顶堆或者小顶堆,逐层比较这里就是时间消耗的工作量,最坏情况下堆有多少层就有多少工作量,层数决定了工作量,也就是堆的高度(lgn)决定了最坏情况下的工作量,用时间复杂度来衡量一下有:O(lgn);而BUILD-MAX-HEAPn/2MAX-HEAPFY的工作量,可得n/2 O(lgn)= O(nlgn)

         由上面的分析可得创建堆的时间复杂度是:O(nlgn)。很好,分析的到处都是满满的逻辑,然而,算法导论的作者会告诉你,创建堆的时间复杂度是线性的,即:O(n),是不是千斤压顶无法呼吸。其实他俩都是上界,只不过O(n)这个上界是最坏情况下渐进紧确的上界,其实一个渐进紧确的上界如果你不了解它背后定义,它足可以让你在它出现的地方迷晕一生。

         在分析创建堆的复杂度之前,我们先引出一个不等式。根据二叉堆的定义和性质,知道堆最后一层的层级是堆的高度h,且每层如果充满的话,其节点数为2^i,设i为层级,从0h。则堆的节点数最多的情况是满二叉树的情况,最少的情况是最后一层只有一个节点,故,堆的节点数n满足以下不等式:

         2^0+2^1+……+2^(h-1)+1<=n<=2^0+2^1+……+2^(h-1)+ 2^h,推得下式:

         2^h <=n<=2^(h+1)-1<2^(h+1),推得下式:

         h<=lgn<h+1

         MAX-HEAPFY中的工作内容是从当前节点往下调整到叶子节点,最多的调整次数(最坏的情况下)是堆的高度h减去当前节点所在的层数ih-i),若当前节点是叶子节点,则调整次数为0。而BUILD-MAX-HEAP中是自底(底是n/2下取整)向上(上是根节点的下标0)逐节点比较父子大小并调整,为什么不从n开始逐节点调整,原因是从n/2下取整下标开始的节点都是叶子节点,他们的调整次数是0。我们考虑满二叉堆的情况(调整节点最多的情况,即最坏的情况),至于非满的二叉堆的工作量一定是不大于满二叉堆的工作量,满二叉堆最后一层都是叶子节点,倒数第二层的节点的调整次数是1,根节点的调整次数是h,由此,把从n/2下取整下标节点开始往根节点逐个节点的调整次数相加于以下:

         其实创建堆的方式还有通过逐个元素插入的方法创建堆,我们可以假设每次插入元素都要调整堆高h=lgi次,i是当前堆大小,这也是最坏情况下的时间复杂度,读者可以自己推证一个结果:Ω(nlgn)

         堆排序的分析和创建堆的分析一样,考虑满二叉树的情况,最后一层的任意一个节点被换至根节点后的比较调整次数是h,节点数是2^h;倒数第二层的任意一个节点被换至根节点后的比较调整次数是h-1,节点数是2^(h-1);以i表示层数,则堆排序有以下的调整次数:

故得堆排序的时间复杂度为O(nlgn)

打完收工,分析完毕,阿弥陀佛!

其他排序

至于其他排序可以在网上搜索得到,也很基础,暂略。不过略提以下3个排序的应用场景以及其他。

链式基数排序

这个排序的算法在这里,应用场景: 斗地主这种删除性操作;

直接插入排序

应用场景:类似3-7个的小数据量的排序;是希尔排序的前身

希尔排序

采取步长的插入排序

使用场景:麻将在玩的过程中的重复排序(因为数据源本身即是有序);已知的最好步长序列由Marcin Ciura设计(141023571323017011750,…) 这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

排序简单总结

实际选择算法是根据算法的数学原理来选择的,复杂度只是选择的辅助参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值