排序(2)

堆排序
堆排序是一种相当优秀的排序算法,也是大量数据中Top k问题的最优方法,常用于数据量非常大的排序场景。

堆排序把数组看成一颗完全二叉树,利用完全二叉树的父节点与左右孩子节点的下标关系来进行相关排序操作。

我们先回顾一下数组与完全二叉树如何对应起来,对于数组,我们把A的每个元素看成完全二叉树的一个节点,那么,把作为其根节点,剩下的逐层从左至右,从上至下依次排列,对于这棵完全二叉树有这样的关系成立

任意节点

其左孩子为,其右孩子为,其父节点为(i/2向下取整)

如图

关于堆如下定义

大(根)堆:任意一个父节点都大于等于其两个孩子节点

小(根)堆:任意一个父节点都小于等于其两个孩子节点

例如

知道什么是大堆,什么是小堆了,那么怎么用它来排序呢?

拿大堆排升序来说(小堆当然是完全一样的),其根节点一定是整个堆中最大的一个元素。那么在最开始,把整个待排序列通过某种方法调整成一个大堆,然后我们把堆顶与堆尾交换(根据前面的数组与完全二叉时的对应关系可知,就是交换当前待排序列的的第一个与最后一个元素),堆顶被放到堆尾后它就不动了,因为它就是整个堆中最大的,它就应该放这,于是我们把它从堆尾删去,因为不需要通过堆对它进行任何相关调整了。
经过刚才的操作,我们发现我们的大堆不是大堆了,因为堆尾的被放到了堆顶,只要堆中现在不止一个元素,那么我们的堆顶一定是小于其左右孩子的(不必考虑相等这样的非一般情况),因此我们必须把它重新调整为一个堆,不然我们的排序就进行不下去了。因此我们通过某种方法对剩下的元素进行重新调整,把其重建为一个大堆。
调整好后,我们又能进行第一步的类似操作(交换堆顶堆尾、删除堆尾),如此循环,直到在第一步中删去堆尾后发现堆中只有一个元素了就排好序了。
相信上面的三点非常容易理解,很形象的说,每次把堆顶往堆尾放时就像在数组末尾从后向前放数据,网上实在找不到合适的动图,大家自己动手画一画。

理解堆排序的过程后我们要去思考怎么实现它。不难发现,上面的三点中的两个“某种方法”是核心。因为堆排序的过程中我们主要要解决这两个问题

如何建立初始堆
如何在交换堆顶、堆尾,删除堆尾后(即破坏原有堆后)重建堆
如何建立初始堆?
首先要明确这个过程是自底向上的,因为堆是一种递归的结构,一个大堆它的所有子树也应该是大堆,反过来,若是一个完全二叉树的每个子树都满足大堆的定义,它就能被称之为大堆,这就和二叉平衡搜索树、红黑树这些结构是一样的道理。

我们知道,若一棵完全二叉树有n个节点并将其与数组对应起来,那么它的最后一个非叶子节点就是数组的第(向下取整)个元素,因此调整必须从该元素开始,自底向上、从右至左、直到根节点这样进行调整,使得每个子树都成为堆。

如图

这样就得到了初始堆,小堆当然是一样的,只不过就是把小的往上提。

具体应该怎么做呢?看完下面你会明白的

如何在交换堆顶、堆尾,删除堆尾后(即破坏原有堆后)重建堆?
这个方法我们称其为向下调整法,是自顶向下的。

我们成现在的堆顶元素为待调整元素,我们将其从堆顶取出,那么此时堆顶相当于空节点,然后我们选出该空节点的左右孩子中较大的一个(若只有一个孩子那当然后就是那个孩子啦),将其放到刚才的空节点中,那么这个孩子它本来的节点就相当于空了,我们的空节点就是这个孩子原来的那个节点,这是我们就要判断我们的待调整元素是否大于等于这个空节点的左右孩子节点。若是待调整元素大于等于这个空节点的左右孩子节点,那么把我们的待调整元素放入这个位置,向下调整就结束了。否则就继续选出该选出该空节点的左右孩子中较大的一个,重复前面的操作,直到待调整元素大于等于当前空节点的左右孩子或者当前空节点没有左右孩子,将其放入当前空节点中就调整结束了。

当然在写代码的时候当然是不需要真的有这个空节点的。

如下是一个向下调整的过程,结合图看你会觉得相当简单

不难发现,建立初始堆的过程中,自底向上的调整实际上就是对于所有所有非叶子节点调用了向下调整法。

代码

void Adjustdown(int* a,int root,int len)
{
    int parent = root;
    int child = 2*parent+1;
    while(child<len)
    {   
        if(child<len-1 && a[child] < a[child+1])
        child++;
        if(a[parent] < a[child])
        swap(a[parent],a[child]);
        parent = child;                                                                                                                                    
        child = 2*parent + 1;
    }   
                        
}
 
void CreateHeap(int* a,int len)
{
    for(int i = (len-1)/2;i>=0;i--)   //写len/2 - 1似乎更好理解向下取整,但是(len-1)/2考虑了只有两个元素的情况
    {
        Adjustdown(a,i,len);
    }
}
 
void HeapSort(int* a,int len)
{
    CreateHeap(a,len);
    int end = len;
    while(end>0)
    {
        swap(a[0],a[end-1]);
        end--;
        Adjustdown(a,0,end);
    }
}

堆当然不止应用于堆排序,贪心算法、优先级队列都要用到堆,堆的操作还有插入元素,删除堆顶,访问堆顶。

访问堆顶不用说,删除堆顶也讲过了,在扩展一下插入元素

扩展:堆的插入
堆的插入用的方法我们称其为向上调整法,因为我们在往堆里插入元素时,我们把元素插入在堆尾,因此,调整就要从堆尾开始,自底向上的把堆尾这个元素提到其合适的位置,也是非常形象,其过程就是相当于把向下调整法反过来了,思想是一样的,各位不妨结合代码自己理一理。

void Adjustup(int child,int* a)
{
    int parent = (child-1)/2;
    while (child > 0)
    {
        if(a[child] > a[parent])
        {
            swap(v[child],v[parent]);
            child = parent;
            parent = (child-1)/2;
        }
        else
        break;
    }
}

算法分析
堆排序的平均时间复杂度、最坏时间复杂度、最好时间复杂度都是o(N*logN),性能是相当不错的

稳定性:堆排序是不稳定的,例如待排序列{1,1,1,1}排升序后显然整个序列被逆序了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值