八大排序算法【中】 -- 希尔、堆排序

希尔排序(Shell Sort)

上一章提到过的冒泡排序、简单选择排序和直接插入排序时间复杂度都是O(n^2);希尔排序是第一批突破这个时间复杂度的算法之一。

直接插入排序在某些情况下效率是很高的,比如数据基本有序的情苦下,为什么?如果序列是:{9,1,5,8,7,3,4,6,2};这个序列如果使用直接插入排序的话,由于序列中最大的记录9位于序列的第一位,因此后面的记录都需要插入到9之前(假设为从小到大排序),这样就会造成比较大的时间开销。但是如果序列是{2,1,3,6,4,7,5,8,9}这样基本有序的,时间性能就会好很多。注意基本有序这个概念,不要求序列一定是从小到大排好的,但是需要小的关键字在前面,大的在后面,不大不小的在中间。还有一种情况直接插入排序效率也是比较高的,就是记录数比较少的时候,直接插入排序优势也比较明显。其实记录数比较少的时候排序算法之间的差距也并不明显。

希尔排序是科学家希尔在1959年提出的,采用跳跃分割的策略将待排序的序列分割为若干个子序列,然后对这些子序列进行排序,为什么要采取跳跃分割而不是直接将每几位作为一个子序列的呢?这样是为了使得排序后的序列是基本有序而不是局部有序。

代码如下:

void ShellSort(SqList * L)
{
    int i, j;
    int increment = L->length;
    do
    {
        increment = increment/3 + 1;
        for (i = increment+1; i <= L->length; i++)
        {
            if (L->r[i] < L->r[i-increment])
            {
                L->r[0] = L->r[i];
                // 下面这个for循环实质上就是一个直接插入排序
                for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
                {
                    L->r[j+increment] = L->r[j];
                }
                L->r[j+increment] = L->r[0];
            }
        }
    }while(increment > 1);
}

因为希尔排序的关键是相隔某个“增量”(代码中的increment)的记录组成一个子序列,实现跳跃式移动,所以“增量”的选择就很重要。究竟选择什么样的增量最好,目前还是一个数学难题。但是大量的研究表明当增量序列
dlta[k] = 2^(t-k+1) -1(0 <= k <= t <= [log2(n+1)]) 时,可以取得不错的效果。但是“增量”最后的结果一定要为 1 。时间复杂度为 O(n^(3/2))。另外希尔排序不是一种稳定的排序算法(因为时跳跃式的移动)。

堆排序

堆排序是对简单选择排序的一种改进,简单选择排序的主要优势就是交换数据的次数少(最多n - 1次),但是比较的次数为n(n-1)/2。比较次数中有很多是重复比较的,就是说某两个数之间的比较次数不止一次,最明显的就是最后两个数的比较。
如果可以每次做到在选择到最小记录的同时,根据比较结果对其他记录做出调整,那么效率就很高了。堆排序就是这么做的。
在讲堆排序之前先说什么是“堆”,堆实际上是一棵完全二叉树,这颗完全二叉树每个结点的值都大于或者等于其左右孩子节点的值,称为大顶堆(下图左),或者每个结点的值都小于或者等于其左右孩子结点的值,称为小顶堆(下图右)。
这里写图片描述
先介绍一个完全二叉树的性质,代码里面用得到。
完全二叉树性质:

  1. 如果i = 1;则结点 i 是完全二叉树的根,无双亲;如果 i > 1,则其双亲结点 [i/2]。
  2. 如果 2i > n ,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子是结点 2i。
  3. 如果 2i + 1 > n;则结点 i 无左右孩子;否则其右孩子是结点 2i + 1。

    堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
    基本可分为三步:

    • 构建大顶堆
    • 将根节点与末尾元素进行交换
    • 向下调整
      代码:
void HeapAdjust(SqList * L, int s, int m)
{
    int temp, j;
    temp = L->r[s];
    for (j = 2*s; j <= m; j *= 2)   // 这里的j = 2 * s和 j*=2都是根据前面的那条性质来写的
        // 因为 s 结点不可能是 叶子结点,所以必然存在孩子结点(至少存在左孩子,因为是完全二叉树)
        // 所以 j = 2 * s , j 就是表示 s 的左孩子。
    {
        if (j < m && L->r[j] < L->r[j+1])
            j++;    // 到这里, j 表示双亲结点、左右孩子结点中最大结点的下标。
        if (temp >= L->r[j])
            break;
        L->r[s] = L->r[j];  // 到这里,L->r[j]为 s 的左或者右孩子结点中值较大的结点, 将这个值赋给双亲结点
                            // 即 现在的双亲结点是原来双亲结点、左右孩子结点中最大的,符合最大堆的定义
        s = j;
    }
    L->r[s] = temp;
}
// 堆排序
void HeapSort(SqList * L)
{
    int i;
    for (i = L->length/2; i > 0; i--)   // i 为什么是从L->length/2开始,根据完全二叉树的结点顺序可知,
                                        // i 所表示的结点都不是叶子结点。
    {
        HeapAdjust(L, i, L->length);
    }
    for (i = L->length; i > 1; i--)
    {
        swap(L, 1, i);                  // 交换
        HeapAdjust(L,1,i-1);            // 重新调整为大顶堆。
    }
}

堆排序的时间复杂度为: O(nlogn);

参考资料

大话数据结构
堆排序算法解析

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值