首页 > 编程语言 >常见的排序算法——堆排序

常见的排序算法——堆排序

时间:2024-07-08 21:21:10浏览次数:21  
标签:__ log int 堆排序 算法 time 排序

本文记述了堆排序的基本思想和一份参考实现代码,并在说明了算法的性能后用随机数据进行了验证。

◆ 思想

J.W.J Williams 提出了堆排序的算法,该算法利用了二叉堆有序的性质,将排序的过程分为先构建堆再排序的两个阶段。

先构建堆。从当前待排序范围一半的位置开始向第一个位置扫描,用下沉操作处理每个位置的元素。每操作一个位置后,以该位置为根节点的子堆就是堆有序的。当扫描完范围的第一个位置后,整个待排序范围就达到了堆有序的状态,即完成了堆的构建。

再排序。将当前堆中的最大元素放到堆底的最后位置,即交换待排序范围的第一个位置的元素和最后一个位置的元素。然后将待排序范围的第一个位置至倒数第二个位置视为新的堆,对新的堆顶元素做下沉操作后,新堆也恢复到堆有序状态。重复以上操作,直到待排序范围只有一个元素为止,排序结束。

◆ 实现

排序代码采用《算法(第4版)》的“排序算法类模板”实现。(代码中涉及的基础类,如 Array,请参考算法文章中涉及的若干基础类的主要API

// heap.hxx

...

class Heap
{

    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    sort(Array<_T> & a)
    {
        int N = a.size();
        for (int k = N/2; k >= 1; --k)          // #1
            __sink__(a, k, N);
        while (N > 1) {
            __exch__(a, 1, N);              // #2
            --N;
            __sink__(a, 1, N);              // #3
        }
    }
    
    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    __sink__(Array<_T> & a, int k, int n)
    {
        while (2*k <= n) {                      // #4
            int j = 2*k;
            if (j < n && __less__(a, j, j+1)) ++j;    // #5
            if (!__less__(a, k, j)) break;
            __exch__(a, k, j);
            k = j;
        }
    }

    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    bool
    __less__(Array<_T> const& a, int i, int j)
    {
        return a[i-1].compare_to(a[j-1]) < 0;       // #6
    }


    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    __exch__(Array<_T> & a, int i, int j)
    {
        _T t = a[i-1];
        a[i-1] = a[j-1];
        a[j-1] = t;
    }

    ...

从当前待排序范围一半的位置开始向第一个位置扫描,用下沉操作达到堆有序的状态(#1)。将当前堆中的最大元素放到堆底的最后位置(#2)。然后对新的堆顶元素做下沉操作后,使新堆也恢复到堆有序状态(#3)。该算法用的是二叉堆(#4),所以某个节点与其两个子节点相比较后,决定是否继续下沉(#5)。将 '<' 改为 '>',即得到逆序的结果(#6)。

◆ 性能

时间复杂度 空间复杂度 是否稳定
N*log(N) 1

◆ 验证

测试代码采用《算法(第4版)》的倍率实验方案,用随机数据验证其正确性并获取时间复杂度数据。

// test.cpp
    
...

time_trial(int N)
{
    Array<Double> a(N);
    for (int i = 0; i < N; ++i) a[i] = Std_Random::random();    // #1
    Stopwatch timer;
    Heap::sort(a);                     // #2
    double time = timer.elapsed_time();
    assert(Heap::is_sorted(a));            // #3
    return time;
}

...

test(char * argv[])
{
    int T = std::stoi(argv[1]);          // #4
    double prev = time_trial(512);
    Std_Out::printf("%10s%10s%7s\n", "N", "Time", "Ratio");
    for (int i = 0, N = 1024; i < T; ++i, N += N) {            // #5
        double time = time_trial(N);
        Std_Out::printf("%10d%10.3f%7.2f\n", N, time, time/prev);   // #6
        prev = time;
    }
}

...

用 [0,1) 之间的实数初始化待排序数组(#1),打开计时器后执行排序(#2),确保得到正确的排序结果(#3)。整个测试过程要执行 T 次排序(#4)。每次执行排序的数据规模都会翻倍(#5),并以上一次排序的时间为基础计算倍率(#6),

此测试在实验环境一中完成,

$ g++ -std=c++11 test.cpp std_out.cpp std_random.cpp stopwatch.cpp type_wrappers.cpp

$ ./a.out 15

         N      Time  Ratio
      1024     0.009   2.25
      2048     0.022   2.44
      4096     0.048   2.18
      8192     0.106   2.21
     16384     0.230   2.17
     32768     0.500   2.17
     65536     1.086   2.17
    131072     2.349   2.16
    262144     5.064   2.16
    524288    10.883   2.15
   1048576    23.276   2.14
   2097152    49.570   2.13
   4194304   105.310   2.12
   8388608   223.426   2.12
  16777216   474.822   2.13

可以看出,随着数据规模的成倍增长,排序所花费的时间将是上一次规模的 2.1? 倍,且在不断地变小。将数据反映到以 2 为底数的对数坐标系中,可以得到如下图像,

test_data

O(N*log(N)) 代表了线性对数级别复杂度下的理论排序时间,该行中的数据是以 Time 行的第一个数据为基数逐一乘 2 + 2/log(N) 后得到的结果(因为做的是倍率实验,所以乘 (2*N*log(2*N)) / (N*log(N)),化简得到 2 + 2/log(N),即乘 2+2/log(1024),2+2/log(2048),2+2/log(4096),... 2+2/log(16777216);因为是二叉堆,所以 log 的底数为 2)。

◆ 最后

完整的代码请参考 [gitee] cnblogs/18289928

写作过程中,笔者参考了《算法(第4版)》的堆排序、“排序算法类模板”和倍率实验。致作者 Sedgwick,Wayne 及译者谢路云。

标签:__,log,int,堆排序,算法,time,排序
From: https://www.cnblogs.com/green-cnblogs/p/18289928

相关文章

  • 自动驾驶感知项目-基于多线激光雷达的小目标锥桶空间位置检测算法(ROS,C++,滤波)
    一:序言想了解更多自动驾驶项目课程以及获取学习代码的可以参考这个链接无人车采用纯跟踪算法跟随离线路径感知锥桶项目中:滤波处理是进行激光雷达目标检测的常见步骤,对原始点云数据进行预处理达到减少噪声、无效点或者数据量的效果。常用的点云滤波方法包括体素滤波、法......
  • C++数据结构底层实现算法
    1.vector底层数据结构为数组,支持快速随机访问2.list底层数据结构为双向链表,支持快速增删3.deque底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间不能)快速增删,也支持随机访问deque是一个双端队列(double-endedqueue),也是在堆中保存内容的.每个......
  • 路径规划算法(1)
    传统路径规划算法      1、BUG避障算法Bug算法大概是人们能想象到的最简单的避障算法。其基本思想是机器人在路途中,跟踪各障碍物的轮廓,从而绕开它。BUG算法十分简单,就像虫子在黑盒中的移动一样,这种规划没有全局路径规划,只有局部路径规划。根据规则的不同分为BUG0,BU......
  • 【算法篇】KMP算法,一种高效的字符串匹配算法
    我们今天了解一个字符串匹配算法-KMP算法,内容难度相对来说较高,建议先收藏再细品!!!KMP算法的基本概念KMP算法是一种高效的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。该算法的主要使用场景就是在字符串(也叫主......
  • Python排序,你用对了吗?一文教你sorted和sort的正确姿势!
    目录1、sorted基础用法......
  • Studying-代码随想录训练营day31| 56.合并区间、738.单调递增的数字、968.监控二叉树
    第31天,贪心最后一节(ง•_•)ง......
  • KMP算法实例——模式匹配
    题目描述:给定两个字符串,一个是文本串txt,另一个是模式串pat。请使用KMP算法找出模式串在文本串中的所有出现位置。示例输入:文本串 txt:"ABABDABACDABABCABAB"模式串 pat:"ABABCABAB"#include<stdio.h>#include<string.h>//计算模式串的最长公共前后缀数组voidco......
  • ACM/ICPC算法基础训练教程(2)
    关于《ACM/ICPC算法基础训练教程》这本书的一些解释与自我理解1.2枚举法1.2.1基本概念1.2.2例题讲解1.2枚举法1.2.1基本概念在某些问题中,问题的解被限制在一个有限的范围内,此类问题只需要按照题目的限定,逐一判断这些可能的解是否符合题目的要求,这种方法称为枚......
  • 代码随想录算法训练营第五天|LeetCode242.有效的字母异位词 LeetCode 349. 两个数组的
    代码随想录算法训练营Day5代码随想录|LeetCode242.有效的字母异位词LeetCode349.两个数组的交集LeetCode202.快乐数LeetCode1.两数之和文章目录代码随想录算法训练营前言代码随想录原文--哈希表今天的内容真的很有挑战o(╥﹏╥)o,做了很久一、哈希表基础理论1......
  • 代码随想录算法训联营第四天|LeetCode24. 两两交换链表中的节点 LeetCode19.删除链表
    系列文章目录代码随想录算法训练营第四天:代码随想录|LeetCode24.两两交换链表中的节点LeetCode19.删除链表的倒数第N个节点面试题02.07.链表相交LeetC142.环形链表文章目录系列文章目录前言一、LeetCode24.两两交换链表中的节点1、题目链接2、题解二、LeetCod......