本文记述了 E.W.Dijkstra 的三向切分快速排序的基本思想和一份参考实现代码,并在说明了算法的性能后用随机数据进行了验证。
◆ 思想
“在有大量重复元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现。这就有很大的改进潜力,将当前实现的线性对数级的性能提高到线性级别。”(引《算法(第4版)》)
三向切分快速排序是将待排序范围切分为小于、等于和大于切分元素的三部分。如下所示,
*-----------------------------------------------------------------*
| < 切分元素 | = 切分元素 | > 切分元素 |
*-----------------------------------------------------------------*
Dijkstra 的解法思想是,维护一个指针 lt ,使得从待排序范围开始位置到 lt-1 位置的所有元素都小于切分元素;维护一个指针 gt ,使得从 gt+1 到待排序范围结束位置的所有元素都大于切分元素;维护一个指针 i ,使得从 lt 到 i-1 中的所有元素都等于切分元素,从 i 到 gt 的所有元素都还未确定与切分元素的大小关系。如下所示,
*-----------------------------------------------------------------*
| < 切分元素 | = 切分元素 | 未确定 | > 切分元素 |
*-----------------------------------------------------------------*
^ ^ ^
lt i gt
使用指针 i 从左到右遍历待排序范围,
- 如果 i 所指的元素小于切分元素,将其与 lt 所指的元素交换,将 lt 和 i 都加 1;
- 如果 i 所指的元素大于切分元素,将其与 gt 所指的元素交换,将 gt 减 1;
- 如果 i 所指的元素等于于切分元素,将 i 加 1。
这样就逐步缩小了未确定的范围,保证了遍历当前范围结束。然后递归处理从待排序范围开始位置到 lt-1 位置的所有元素,以及从 gt+1 到待排序范围结束位置的所有元素。
◆ 实现
排序代码采用《算法(第4版)》的“排序算法类模板”实现。(代码中涉及的基础类,如 Array,请参考算法文章中涉及的若干基础类的主要API)
// quick3.hxx
...
class Quick3
{
...
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;
int lt = lo, i = lo+1, gt = hi;
_T v = a[lo];
while (i <= gt) { // #1
int cmp = a[i].compare_to(v);
if (cmp < 0) __exch__(a, lt++, i++); // #2
else if (cmp > 0) __exch__(a, i, gt--); // #3
else i++; // #4
}
__sort__(a, lo, lt-1); // #5
__sort__(a, gt+1, 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 遍历待排序范围,直到无未确定大小关系的元素为止(#1)。如果 i 所指的元素小于切分元素,将其与 lt 所指的元素交换,将 lt 和 i 都加 1(#2);如果 i 所指的元素大于切分元素,将其与 gt 所指的元素交换,将 gt 减 1(#3);如果 i 所指的元素等于于切分元素,将 i 加 1(#4)。然后递归处理从待排序范围开始位置到 lt-1 位置的所有元素,以及从 gt+1 到待排序范围结束位置的所有元素(#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;
Quick3::sort(a); // #2
double time = timer.elapsed_time();
assert(Quick3::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.011 2.75
2048 0.027 2.45
4096 0.053 1.96
8192 0.123 2.32
16384 0.247 2.01
32768 0.580 2.35
65536 1.138 1.96
131072 2.489 2.19
262144 5.077 2.04
524288 10.794 2.13
1048576 23.094 2.14
2097152 47.949 2.08
4194304 99.554 2.08
8388608 209.597 2.11
16777216 451.508 2.15
可以看出,随着数据规模的成倍增长,排序所花费的时间将是上一次规模的 2.1? 倍,且在不断波动地变小。将数据反映到以 2 为底数的对数坐标系中,可以得到如下图像,
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/18243804 。
写作过程中,笔者参考了《算法(第4版)》的快速排序、熵最优排序、“排序算法类模板”和倍率实验。致作者 Sedgwick,Wayne 及译者谢路云。
标签:__,常见,log,元素,切分,算法,gt,排序 From: https://www.cnblogs.com/green-cnblogs/p/18243804