常见的排序算法——希尔排序(二)

本文记述了希尔排序采用另一个间隔序列的基本思想和参考实现,并在说明了算法的性能后用随机数据进行了验证。

◆ 思想

在前一篇希尔排序文章中,用到了简单的间隔序列 1, 4, 13, 40, ... (h = 3*h + 1)。本文参考了《算法(第4版)》练习题 2.1.29,用到的间隔序列为 1, 5, 19, 41, ... 。此序列通过 9*4^k - 9*2^k + 1 (k=0,1,2...) 和 4^k - 3*2^k + 1 (k=2,3,4...) 综合得到,如下:

k 0 1 2 3 4 5 ...
h1 1 19 109 505 2161 8929 ...
h2 - - 5 41 209 929 ...

因不方便用简单计算将此间隔序列递减至 1,笔者参考了《算法(第4版)》练习题 2.1.11 的做法,将希尔排序中实时计算间隔序列改为预先计算并存储在一个栈中。

按逐一出栈的顺序使用间隔序列。弹出栈顶的间隔 h,将所有间隔为 h 的元素作为独立的待排序范围,可以得到 h 个这样的子范围。针对每个子范围执行插入排序,使得任意间隔为 h 的元素是有序的。然后重复以上的弹栈、对子范围执行插入排序的操作,直到间隔序列栈被弹空为止。如要得到逆序的结果,则仅需改变比较的方向即可。

◆ 实现

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

// shell2.hxx

...

class Shell2
{

    ...

    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();
        int h1, h2, k = 0;
        Stack<int> hs;
        do {                                                         // #1
            h1 = int(9 * std::pow(4, k) - 9 * std::pow(2, k) + 1);
            if (h1 < N) hs.push(h1);                                // #2
            h2 = int(std::pow(4, k+2) - 3 * std::pow(2, k+2) + 1);
            if (h2 < N) hs.push(h2);                                // #2
            ++k;
        } while (h1 < N || h2 < N);
        int h;
        while (!hs.is_empty()) {
            h = hs.pop();                           // #3
            for (int i = h; i < N; ++i) {                   // #4
                _T t = a[i];
                int j;
                for (j = i; j >= h && __less__(t, a[j-h]); j -= h)
                    a[j] = a[j-h];
                a[j] = t;
            }
        }
    }
    
    ...
    
    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;         // #5
    }

    ...

不断地使用公式计算间隔序列(#1)并将其存入栈中(#2)。重复地从栈中取得间隔 h(#3),针对每个子范围执行不需要交换的插入排序(#4)。将 '<' 改为 '>',即得到逆序的结果(#5)。

◆ 性能

时间复杂度 空间复杂度 是否稳定
N^(7/6) ? N*log(N)) ? 1

【注】“透彻理解希尔排序的性能至今仍然是一项挑战。... 算法的性能不仅取决于 h,还取决于 h 之间的数学性质,...”(引《算法(第4版)》)。

◆ 验证

测试代码采用《算法(第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;
    Shell2::sort(a);                     // #2
    double time = timer.elapsed_time();
    assert(Shell2::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.015   2.50
      4096     0.034   2.27
      8192     0.077   2.26
     16384     0.169   2.19
     32768     0.372   2.20
     65536     0.808   2.17
    131072     1.749   2.16
    262144     3.772   2.16
    524288     8.120   2.15
   1048576    17.355   2.14
   2097152    36.966   2.13
   4194304    78.381   2.12
   8388608   168.013   2.14
  16777216   351.518   2.09

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

test_data

O(N^(7/6)) 代表了(7/6)次方级别复杂度下的理论排序时间,该行中的数据是以 Time 行的第一个数据为基数逐一乘 2^(7/6) 后得到的结果(因为做的是倍率实验,所以乘 (2)^(7/6) 即 2.24)。

O(N*log(N)) 代表了线性对数级别复杂度下的理论排序时间,该行中的数据是以 Time 行的第一个数据为基数逐一乘 2 + 1/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/18140082

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

posted @ 2024-04-17 15:15  green-cnblogs  阅读(3)  评论(0编辑  收藏  举报