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

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

◆ 思想

非递归的快速排序,使用一个栈保存待排序子范围的范围边界。在循环中弹出栈顶的范围边界。使用切分函数找到此范围中一个切分位置,保证其左侧子范围内的所有元素都不大于切分位置的元素,右侧子范围内的所有元素都不小于切分位置的元素。先将待排序子范围内元素多的范围边界压入栈中,后压入另一个。如此往复,直到栈被处理空为止,排序结束。

其本质是将标准快速排序中函数调用所产生的压栈过程,转换为了针对每个待排序子范围边界的压栈过程。

◆ 实现

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

// quick5.hxx

...

class Quick5
{

    ...
    
    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)
    {
        using ___Range___ = std::pair<int, int>;
        Stack<___Range___> stk;                        // #1
        stk.push(___Range___(lo, hi));
        do {
            ___Range___ curr  = stk.pop();             // #2
            lo = std::get<0>(curr);
            hi = std::get<1>(curr);
            int j = __partition__(a, lo, hi);          // #3
            ___Range___ rngl(lo, j-1);
            ___Range___ rngr(j+1, hi);
            int szl = j - lo;
            int szr = hi - j;
            if (szl > szr) {                           // #4
                if (szl > 1) stk.push(rngl);
                if (szr > 1) stk.push(rngr);
            } else {
                if (szr > 1) stk.push(rngr);
                if (szl > 1) stk.push(rngl);
            }
        } while (!stk.is_empty());                     // #5
    }

    ...

    template
    <
        class _T,
        class = typename std::enable_if<std::is_base_of<Comparable<_T>, _T>::value>::type
    >
    static
    void
    __partition__(Array<_T> & a, int lo, int hi)
    {
        int i = lo, j = hi+1;
        _T v = a[lo];
        while (true) {
            while (__less__(a[++i], v)) if (i == hi) break;
            while (__less__(v, a[--j])) if (j == lo) break;
            if (i >= j) break;
            __exch__(a, i, j);
        }
        __exch__(a, lo, j);
        return 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)
    {
        _T t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    
    ...

使用一个栈保存待排序子范围的范围边界(#1)。在循环中弹出栈顶的范围边界(#2)。使用切分函数找到此范围中一个切分位置,保证其左侧子范围内的所有元素都不大于切分位置的元素,右侧子范围内的所有元素都不小于切分位置的元素(#3)。先将待排序子范围内元素多的范围边界压入栈中,后压入另一个(#4)。如此往复,直到栈被处理空为止(#5),排序结束。

◆ 性能

时间复杂度 空间复杂度 是否稳定
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;
    Quick5::sort(a);                     // #2
    double time = timer.elapsed_time();
    assert(Quick5::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.007   2.33
      2048     0.016   2.29
      4096     0.034   2.12
      8192     0.072   2.12
     16384     0.148   2.06
     32768     0.316   2.14
     65536     0.654   2.07
    131072     1.380   2.11
    262144     2.862   2.07
    524288     5.868   2.05
   1048576    12.688   2.16
   2097152    25.396   2.00
   4194304    53.122   2.09
   8388608   108.878   2.05
  16777216   229.839   2.11

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

查看性能对比,了解此算法与其它排序算法的相似性和差异性。

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

posted @ 2024-06-24 15:44  green-cnblogs  阅读(6)  评论(0编辑  收藏  举报