首页 > 编程语言 >常见的排序算法——快速排序(四)

常见的排序算法——快速排序(四)

时间:2024-06-18 10:10:37浏览次数:14  
标签:__ 常见 log int 元素 切分 算法 排序

本文记述了 J.Bently 和 D.Mcllroy 的快速三向切分快速排序的基本思想和一份参考实现代码,并在说明了算法的性能后用随机数据进行了验证。

◆ 思想

对比快速排序快速排序(二)快速排序(三)可以发现,对于随机数据而言,E.W.Dijkstra 的三向切分快速排序的性能要慢于标准快速排序以及改进版本,“因为在数组中重复元素不多的普通情况下它比标准的二分法使用了很多次交换。 J.Bently 和 D.Mcllroy 找到一个聪明的方法解决了这个问题,使得三向切分快速排序比归并排序和其它排序方法在包括重复元素很多的实际应用中更快。...... J.Bently 和 D.Mcllroy 用将重复元素放置于子数组两端的方式实现一个信息量最优的排序算法。”(引《算法(第4版)》

三向切分快速排序是将待排序范围排序为小于、等于和大于切分元素的三部分。如下所示,

*-----------------------------------------------------------------*
|     < 切分元素       |       = 切分元素      |       > 切分元素     |
*-----------------------------------------------------------------*

J.Bently 和 D.Mcllroy 的解法思想是,维护两个指针 p 和 q ,使得从待排序范围开始位置(lo)到 p-1 、q+1 到待排序范围结束位置(hi)的所有元素都等于切分元素;维护另外两个指针 i 和 j ,使得从 p 到 i-1 的所有元素都小于切分元素,从 j+1 到 q 的所有元素都大于切分元素。从 i 到 j 的所有元素都还未确定与切分元素的大小关系。如下所示,

*----------------------------------------------------------------------------------*
|    = 切分元素   |    < 切分元素   |     未确定    |    > 切分元素    |    = 切分元素   |
*----------------------------------------------------------------------------------*
 ^                ^               ^            ^                   ^              ^
 lo               p               i            j                   q             hi

使用指针 i 从左到右、指针 j 从右到左遍历待排序范围,

  • 如果 i 所指的元素大于切分元素,停止向右扫描;
  • 如果 i 所指的元素等于切分元素,将其与 p 所指的元素交换,将 p 和 i 都加 1。
  • 如果 i 所指的元素小于切分元素,将 i 加 1;
  • 如果 j 所指的元素小于切分元素,停止向左扫描;
  • 如果 j 所指的元素等于切分元素,将其与 q 所指的元素交换,将 q 和 j 都减 1。
  • 如果 j 所指的元素大于切分元素,将 j 减 1;

这样就逐步缩小了未确定的范围,保证了遍历当前范围结束。然后将与切分元素相等的所有元素都放在中间,小于切分元素的放在当前范围左端,大于切分元素的放在当前范围右端。最后递归处理从待排序范围开始位置(lo)到 j 的所有元素,以及从 i 到待排序范围结束位置(hi)的所有元素。

◆ 实现

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

// quick4.hxx

...

class Quick4
{

    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    sort(Array<_T> & a)
    {
        Std_Random::shuffle(a);
        __sort__(a, 0, a.size()-1);
    }
    
    ...
    
    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    __sort__(Array<_T> & a, int lo, int hi)
    {
        if (hi <= lo) return;
        _T v = a[lo];
        int i = lo+1, j = hi, p = lo+1, q = hi;         // #1
        while (true) {
            while (i <= j) {
                int cmp = a[i].compare_to(v);               // #2
                if (cmp  > 0) break;
                if (cmp == 0) __exch__(a, p++, i);
                ++i;
            }
            while (i < j) {
                int cmp = a[j].compare_to(v);               // #3
                if (cmp  < 0) break;
                if (cmp == 0) __exch__(a, q--, j);
                --j;
            }
            if (i >= j) break;                              // #4
            __exch__(a, i++, j--);
        }
        j = i-1;
        for (int k = lo; k < p; ++k) __exch__(a, k, j--);       // #5
        for (int k = hi; k > q; --k) __exch__(a, k, i++);
        __sort__(a, lo, j);                                     // #6
        __sort__(a, i, hi);
    }

    ...

    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)
    {
        _T t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    ...

使用指针 i 从左到右、指针 j 从右到左遍历待排序范围(#1)。比较 i 所指的元素与切分元素的大小关系(#2),大于则停止扫描;等于则将其与 p 所指的元素交换,将 p 和 i 都加 1;小于则将 i 加 1。比较 j 所指的元素与切分元素的大小关系(#3),小于则停止向左扫描;等于则将其与 q 所指的元素交换,将 q 和 j 都减 1;大于则将 j 减 1。当扫描指针相遇后,当前待排序范围的排序过程结束(#4)。然后将与切分元素相等的所有元素都放在中间,小于切分元素的放在当前范围左端,大于切分元素的放在当前范围右端(#5)。最后递归处理从待排序范围开始位置(lo)到 j 的所有元素,以及从 i 到待排序范围结束位置(hi)的所有元素(#6)。

◆ 性能

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

◆ 验证

测试代码采用《算法(第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;
    Quick4::sort(a);                     // #2
    double time = timer.elapsed_time();
    assert(Quick4::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.006   2.00
      2048     0.014   2.33
      4096     0.029   2.07
      8192     0.061   2.10
     16384     0.127   2.08
     32768     0.271   2.13
     65536     0.564   2.08
    131072     1.196   2.12
    262144     2.487   2.08
    524288     5.128   2.06
   1048576    11.086   2.16
   2097152    22.259   2.01
   4194304    46.541   2.09
   8388608    95.625   2.05
  16777216   200.314   2.09

可以看出,随着数据规模的成倍增长,排序所花费的时间将是上一次规模的 2.0? 倍,且在不断波动地变小。将数据反映到以 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/18252468

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

标签:__,常见,log,int,元素,切分,算法,排序
From: https://www.cnblogs.com/green-cnblogs/p/18252468

相关文章

  • Redis常见数据类型及其常用命令详解
    文章目录一、Redis概述二、Redis常用命令1.通用命令1.1KEYS:查看符合模板的所有key1.2DEL:删除一个指定的key1.3EXISTS:判断key是否存在1.4EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除1.5TTL:查看一个key的剩余有效期1.6COPY:复制Redis数据库......
  • 机器学习算法 —— K近邻(KNN分类)
    ......
  • PHP面试宝典之Laravel篇(15个常见问题轻松搞定Laravel面试)
    引言在PHP的众多框架中,Laravel以其优雅的语法、丰富的功能以及强大的社区支持而脱颖而出。对于PHP开发者而言,掌握Laravel已成为迈向高级开发的必经之路。本文将深入探讨Laravel框架的核心概念和高级特性,为即将面临Laravel相关工作面试的开发者提供一个全面的复习材料。本文最......
  • 决策树算法介绍:原理与案例实现
    一、引言在机器学习领域,决策树是一种常用且直观的分类和回归方法。它通过一系列简单的决策规则,将数据集分割成更小的子集,最终形成一个树状结构。本文将详细介绍决策树算法的原理,并通过具体案例实现来帮助读者更好地理解和应用这一算法。二、决策树原理1.决策树的基本概念......
  • LeetCode 算法: 环形链表 c++
    原题链接......
  • 常见的授权渗透环境
    有一些经过授权的渗透测试环境可供学习和实践使用。以下是一些常见的环境:OWASPMutillidae:这是一个免费的、开源的Web应用程序,它包含了许多故意设计的脆弱性,用于安全测试和渗透测试实践。你可以在本地安装它,并尝试发现和利用其中的安全漏洞。OWASPWebGoat:这是另一个OWASP项目,也......
  • Adam优化算法
    Adam优化算法Adam(AdaptiveMomentEstimation)是一种用于训练深度学习模型的优化算法,由DiederikP.Kingma和JimmyBa在2014年提出。Adam结合了动量和自适应学习率的方法,具有高效、稳定和适应性强的特点,被广泛应用于各种深度学习任务中。Adam优化算法的基本思想Adam的核心思......
  • 多叉树的DFS深度优先遍历,回溯法的基础算法之一
    一、前言多叉树一般用于解决回溯问题。想必大家都学过二叉树,以及二叉树的深度优先遍历和广度优先遍历,我们思考:能不能将二叉树的DFS转化为多叉树的DFS?二、多叉树的结构多叉树的本质,就是一棵普通的树,比如下图:如果忽略将来的变化,那么,这棵树可以认为是一个未满的4叉树。......
  • (算法)找到字符串中所有字母异位词——<滑动窗⼝+哈希表>
    1.题⽬链接:438.找到字符串中所有字⺟异位词2.题⽬描述:3.解法(滑动窗⼝+哈希表): 算法思路:◦因为字符串p的异位词的⻓度⼀定与字符串p的⻓度相同,所以我们可以在字符串s中构造⼀个⻓度为与字符串p的⻓度相同的滑动窗⼝,并在滑动中维护窗⼝中每种字⺟的数量; ◦当窗......
  • 进行一个字符串算法的总结
    本文参考字符串基础byAlex_Wei。Manacher算法这玩意是用来求回文子串的。虽然一个字符串的子串数量是\(O(n^2)\)级别的,但是回文串有更好的描述方式。注意到若一个子串\([l,r]\)是以\(mid\)为回文中心的回文串,那么将左端点和右端点朝着\(mid\)方向挪动若干单位也......