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

常见的排序算法——堆排序(五)

时间:2024-07-22 16:21:31浏览次数:15  
标签:__ ... log int 堆排序 算法 排序 pstns

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

◆ 思想

堆排序算法按照层次操作堆中的元素,即物理位置 k 的节点与位置 2k 或 2k+1 的节点交换。然而用前序表示的堆,其父子节点的位置关系不能简单地计算出来。因此,当算法模型(逻辑上)用的位置与内存存储(物理上)用的位置不一致时,需要建立从逻辑上到物理上的映射关系。笔者用一个额外的数组存放物理位置,而数组的索引被用作逻辑位置。

在构建堆之前生成该映射关系,然后按照既有的排序算法排序。排序得到的是按照层次操作后的结果。因此,在排序结束后,再借助映射关系,将层次关系转变回前序关系,进而得到正确的排序结果。

◆ 实现

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

// heap5.hxx

...

class Heap5
{

    ...
    
    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();
        Array<int> pstns(N+1);
        int p = 0;
        __position__(1, &p, pstns);                 // #1
        for (int k = N/2; k >= 1; --k)              // #3
            __sink__(a, k, N, pstns);
        while (N > 1) {
            __exch__(a, 1, N, pstns);
            --N;
            __sink__(a, 1, N, pstns);
        }
        Array<_T> t(a.size());
        for (int i = 0; i < a.size(); ++i) t[i] = a[i];
        for (int i = 0; i < a.size(); ++i) a[i] = t[pstns[i+1]-1];      // #5
    }
    
    ...

    /*
     * pstns[]
     *  array index is the position in level of binary tree. 0 is NOT used.
     *  array value is the position in preorder of binary tree. 0 is NOT used.
     *
     */
    static
    void
    __position__(int k, int * p, Array<int> & pstns)
    {
        if (k >= pstns.size()) return;
        pstns[k] = ++*p;                            // #2
        __position__(k*2,   p, pstns);
        __position__(k*2+1, p, pstns);
    }

    ...

    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, Array<int> & pstns)
    {
        while (2*k <= n) {
            int j = 2*k;
            if (j < n && __less__(a[pstns[j]-1], a[pstns[j+1]-1])) ++j;     // #4
            if (!__less__(a[pstns[k]-1], a[pstns[j]-1])) break;
            __exch__(a, k, j, pstns);
            k = j;
        }
    }

    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    bool
    __less__(_T const& v, _T const& w)
    {
        return v.compare_to(w) < 0;
    }

    ...
    
    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, Array<int> & pstns)
    {
        _T t = a[pstns[i]-1];                       // #4
        a[pstns[i]-1] = a[pstns[j]-1];
        a[pstns[j]-1] = t;
    }

    ...

在构建堆之前生成该映射关系(#1),递归处理从逻辑上到物理上的映射关系(#2)。按照既有的排序算法构建堆并排序(#3),过程中要借助映射关系把逻辑上的位置转变为物理上的位置(#4)。在排序结束后,再次借助映射关系,将层次关系转变回前序关系(#5),得到正确的排序结果。

◆ 性能

时间复杂度 空间复杂度 是否稳定
N*log(N) 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;
    Heap5::sort(a);                     // #2
    double time = timer.elapsed_time();
    assert(Heap5::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.018   2.25
      2048     0.039   2.17
      4096     0.087   2.23
      8192     0.191   2.20
     16384     0.416   2.18
     32768     0.908   2.18
     65536     1.996   2.20
    131072     4.332   2.17
    262144     9.348   2.16
    524288    20.088   2.15
   1048576    43.033   2.14
   2097152    91.654   2.13
   4194304   194.493   2.12
   8388608   412.155   2.12
  16777216   871.686   2.11

可以看出,随着数据规模的成倍增长,排序所花费的时间将是上一次规模的 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/18315474

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

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

相关文章

  • 代码随想录算法训练营第17天 | 复习二叉搜索树
    2024年7月19日题654.最大二叉树熟练运用递归即可classSolution{publicTreeNodeconstructMaximumBinaryTree(int[]nums){intmaxNum=Integer.MIN_VALUE;intflag=-1;for(inti=0;i<nums.length;i++){if(nums[i]>maxNum){......
  • 「代码随想录算法训练营」第十七天 | 二叉树 part7
    235.二叉搜索树的最近公共祖先题目链接:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/题目难度:中等文章讲解:https://programmercarl.com/0235.二叉搜索树的最近公共祖先.html视频讲解:https://www.bilibili.com/video/BV1Zt4y1F7ww?share_so......
  • 二分查找算法基础
    一.二分查找二分查找,又叫“折中查找”,其通过问题的性质,每次将问题规模缩小一半,对应的时间复杂度为O(logN)。二分查找不仅能用作数组元素的查找,还可用于单调函数的求解。对于二分查找算法,它包含head(头指针),tail(尾指针), mid三个变量,这里的头尾指针其实并不是指针,一般为整型,表......
  • 冒泡排序
    冒泡排序两层循环,外层冒泡轮数,内层依次比较。时间复杂度O(n2),代码量小,效率较低,小数据好用。publicstaticvoidmain(String[]args){int[]ints={1,24,53,42,5};int[]sort=sort(ints);System.out.println(Arrays.toString(sort));}public......
  • 代码随想录算法训练营第十一天| 144. 二叉树的前序遍历 , 94. 二叉树的中序遍历, 145.二
    今天主要学习了二叉树的基本概念,以及多种遍历方法。包含分别使用迭代思想和递归思想的前序遍历,中序遍历,后序遍历以及层次遍历。二叉树的基础知识二叉树二叉树的种类可以分为满二叉树和完全二叉树。满二叉树指的是一个二叉树仅包含度为0和度为2的结点,并且度为0的节点在同一层......
  • 推式子——拓展欧几里德算法exgcd
    试求方程\(ax+by=\gcd(a,b)\)的一组整数解,其中\(a\)和\(b\)是给定的数提前准备好一个式子:辗转相除法\[\gcd(a,b)=\gcd(b,a\bmodb)\]同时我们可以注意到,\(\bmod\)的等价版本:\[a\bmodb=a-b\lfloor\frac{a}{b}\rfloor\]开始推式子首先考虑\(b=0\)的情况,因为......
  • 代码随想录算法训练营第一天leetcode704二分查找27移除元素
    leetcode704,这是leetcode提交四次后通过的结果:classSolution{  publicintsearch(int[]nums,inttarget){    if(nums.length==1&&nums[0]==target)      return 0;    if(nums.length==2)      if(nums[0]==target)......
  • 数据结构-C语言-排序(3)
            代码位置:test-c-2024:对C语言习题代码的练习(gitee.com)一、前言:1.1-排序定义:        排序就是将一组杂乱无章的数据按照一定的规律(升序或降序)组织起来。(注:我们这里的排序采用的都为升序)1.2-排序分类:常见的排序算法:插入排序a. 直接插......
  • C语言-常用算法-23
     题目:分数计算器程序源代码:#include<stdio.h>intgys(intx,inty){returny?gys(y,x%y):x;}intgbs(intx,inty){returnx/gys(x,y)*y;}voidyuefen(intfz,intfm){ints=gys(fz,fm);fz/=s;fm/=s;printf("结果是:%d/%d&quo......
  • C语言-常用算法-22
    题目:分鱼问题A,B,C,D,E五个人在某天合伙去捕鱼,分鱼时,A先将鱼分为五份,把多余的一条鱼扔掉,拿走自己的一份;B第二个醒来,也将鱼分为五份,把多余的一条扔掉,拿走自己的一份;C,D,E依次醒来,也按同样的方式拿鱼,问他们至少捕了多少鱼源代码:#include<stdio.h>intsub(intn){if(n......