算法系列(一) 堆排序

堆排序

       现在的目标是每天学点新的东西,同时复习过去学过的算法和数据结构微笑。在我今天找排序的题目时,发现在O(nlogn)的排序方法中,前段时间才使用过快排和归并,堆排最久未用,快忘光了。

       思想只记得个大概,但是具体实现的细节则记不清了,正好复习下。所以,第一篇是这个没有任何特殊原因

       目前在锻炼我的文笔水平,有什么地方写错了,或者矛盾了,还请各位见谅。当然,如果帮小弟指出来,更是万分感谢。大笑

       另外大着胆子放到了首页……这个对我来说,还是需要一点勇气的……委屈

       本来打算也写写优先队列的,结果发现,书中是无序数组的优先队列,那真没什么好说的了……偷笑

       在讲堆排序之前,当然要先把排序前的堆给讲了,一般都用数组来实现堆这个结构,不知道有没有谁尝试过链表……

       抽象的数据结构——堆具体长什么样子,大家都没见过,好吧,计算机里面二进制的一堆电子,肯定没人见过。

       个人猜测,这个堆应该来自于现实中的沙堆一类,就像我们教操作系统的老师说的,计算机很多的思想和解法都来自于现实中得意。比如他最喜欢讲的就是操作系统中的资源竞争和大家排队进卫生间的关系惊讶……连分布式系统中的时间戳都成了南极考察队员们轮流进卫生间的必备常识闭嘴

       大家都知道堆就是上面尖下面大,那么,这么一堆数据结构(树、图、顺序表)中,哪个跟它……还用说吗,树呗,树枝是往上长的,树根不就往下展了?我又凑字数了。

       不过,沙堆大家也看到了,一般的二叉树还是差了点,比如极端的永远只有单边子树的,快长成一条线了。最终看中了完全二叉树来作为原型。为什么呢?因为完全二叉树有个数学性质可以让线性的数组实现二叉树的功能,具体的后面解释。

       存储的结构——数组和目标结构——完全二叉树都确定了,现在可以开始建堆了奋斗

       首先,我们要确定一点的就是,堆是优先队列的一种,所以,有优先级。从堆的长相上看,无疑它的顶部最适合表现优先高了,所以,优先级高的就住在山顶了,然后层层往下。

       上层和下层之间有什么关系呢?用的是二叉树,所以不同子树之间的结点互相不抢家财,也就打不起来。

       于是,每个结点只盯着父结点和那个兄弟结点(如果有的话),于是这三个点中,优先级最高的一定会被换到上面一层,而下面的兄弟俩在老爸在时候,就很老实了敲打

       哎,如果是链表就好了,找上层和兄弟结点很容易啊,怎么是数组呢?

       错了,对于一般的树来说,这确实是个问题,但正如我上面说的,完全二叉树的性质决定了,这个数组可以当二叉树来用。

       具体为什么我就不证明了,总之完全二叉树中,结点i的左儿子结点号为2 * i,右儿子为 2 * i + 1,数组的下标可以直接使用了!唯一的变化就是,从a[1]开始,而不是从0开始。

       在最终建好的堆中,a[i]的优先级一定会比a[2*i], a[2*i +1]的高,而刚开始一切都是乱序的。所以,我们实际上要完成的是a[i],它的左子树的最高的优先级,右子树的最高的优先级,三者进行比较,只不过,左子树和右子树最高的优先级已经移到a[i]两个子结点上了。

       我很高兴的发现,我用动态规划又把堆排序给“祸害”了!也就是说,我发现了“深藏”在堆排序内部那“闷骚”的动态规划思想!我真是脑子里全是动态规划了……

       我只是说着好玩的,也许可以加深大家的印象,其实,动态规划是有规范的证明方法的,我可不想当笑话……

       这里和动态规划相似的就是(话说 算法C语言实现也把动态规划划到了递归与树这一章),从最小的子问题往上计算。

       整个建堆的过程,其实就是,从叶子结点开始比较,叶子结点没有下面……全酱油了(节约时间就连酱油都不打),到最后一个内部结点,最终到最顶端的那一点,也是第一个结点,不断地与它的子结点比较并交换位置:

for (i = N/2; i >= 1; i--)
      HeapAdjust(a, i, N);

        i就是这个非叶子结点了,至于为什么是从N/2开始,证明还是不写了吧……尴尬

       为什么要加上N这个参数?有一点要注意,a[i]交换到a[2*i]a[2*i+1]后,还要继续向下比较。本来该子树已经按规则建好,但是,a[i]这家伙一下来,可能还会继续掉下去,干脆让它一次掉个够!生气于是又掀起了血雨腥风得意

       具体的HeapAdjust()实现:

void HeapAdjust(int a[], int k, int N)
{
    int t = a[k];       //独孤求败找他儿子练手
    int i;
    for (i = 2 * k; i <= N; i *= 2)
    {
        if (i < N && a[i] < a[i+1]) //兄弟之前先排名
            i++;
        if (a[i] <= t)    //可惜还是比不过老爸
            break;	  //老爸打不过,我已经跟孙子比过了,就不用比了
        a[k] = a[i];      //打赢了,儿子就把位置给占了
        k = i;		  //独孤求败的位置就换了
    }
    a[k] = t;             //总算找到了最后的位置。
}

    上面就是完整的建堆过程了大笑。优先级最高的项现在就在顶端,斩首行动就很顺利了,一抓一个准(把最高的和最低的交换个位置,再把堆的大小减一)。这样的话,最顶上那个优先级成最低的了,然后,再调整这个堆,把剩下优先级最高的顶上去。

        所以,也不知道是哪位前辈(个人讨厌大神这类词语)得到了一个很巧妙的时间复杂度为O(nlogn),空间复杂度O(1)的高效排序算法——堆排序吐舌头。完整版如下:

       HeapAdjust()如上。

void HeapSort(int a[], int N)
{
    int i;
    for (i = N / 2; i > 0; i--)
        HeapAdjust(a, i, N);
    for (i = N; i > 1; i--)
    {
        t = a[i];
        a[i] = a[1];
        a[1] = t;
        HeapAdjust(a, 1, i-1);
    }
}

       在算法设计与分析基础中,堆排序被划分到了Transform and conquer 一章,大概就是说不停地调整堆的形状来得到最终的排序。

       而我很无聊地把这个算法和动态规划无耻地联系到了一起,如果对大家理解和学习这个算法以及动态规划有帮助,我自然深感荣幸。如果看不惯我毫无根据地牵红线,也请见谅。

       这是偶完成的第一篇算法的笔记,希望我以后都不会忘记这个算法了……忘了好几次了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值