6.7 KiB
排序算法实验报告
1. 解题思路
本实验旨在深入理解并分析多种经典排序算法的性能。为了达成此目标,实验遵循了以下设计思路:
-
模块化实现:将每种排序算法(插入、冒泡、希尔、归并、快速排序及其变体)分别实现在独立的
.cpp文件中,并通过一个统一的algorithm.h头文件进行声明。这种结构使得代码清晰、易于扩展和维护。 -
量化性能指标:为了客观评估算法性能,除了记录运行时间外,还在算法实现中进行“插桩”,精确统计了两个关键操作:
- 比较次数 (Comparisons):元素之间的比较操作,是决定算法时间复杂度的核心因素之一。
- 移动次数 (Moves):元素的赋值或交换操作,反映了算法的数据搬运成本。
-
自动化测试框架:设计了一个灵活的测试框架 (
main.cpp),能够:- 自动化地对所有已实现的算法进行测试。
- 支持对多种不同的输入规模(例如 100, 1000, ..., 500,000)进行测试。
- 通过多次重复实验并取平均值的方式,消除单次运行的随机误差,保证结果的可靠性。
- 在每次排序后进行正确性校验,确保算法实现无误。
- 以格式化的表格输出结果,便于阅读和后续分析。
-
迭代优化与对比:重点对快速排序进行了深度探索,实现了从基础版本到优化版本(三数取中、三路快排、双基准快排)的演进。通过将这些版本置于同一测试框架下进行性能对比,可以直观地展示不同优化策略带来的效果。
2. 算法复杂度分析与实验数据验证
理论分析
| 算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|---|
| InsertSort | O(n²) | O(n) | O(n²) | O(1) |
| BubbleSort | O(n²) | O(n) | O(n²) | O(1) |
| ShellSort | O(n log n) ~ O(n²) | O(n log n) | O(n²) | O(1) |
| MergeSort | O(n log n) | O(n log n) | O(n log n) | O(n) |
| QuickSort | O(n log n) | O(n log n) | O(n²) | O(log n) |
| QuickSortOpt | O(n log n) | O(n log n) | O(n²) | O(log n) |
| QuickSort3Way | O(n log n) | O(n) | O(n²) | O(log n) |
| DualPivotSort | O(n log n) | O(n log n) | O(n²) | O(log n) |
实验数据验证
通过运行实验程序,可以得到不同算法在不同规模下的平均运行时间、比较次数和移动次数。这些数据可以用来验证上述理论复杂度。
-
O(n²) 算法 (InsertSort, BubbleSort):
- 预期:当输入规模
n增大10倍时,运行时间、比较和移动次数大约会增大100倍。 - 观察:从实验数据中可以看到,当
n从 1000 增加到 10000 时,InsertSort和BubbleSort的运行时间急剧增加,远超线性增长,这与 O(n²) 的特征相符。由于性能问题,测试框架在n >= 50000时自动跳过了这些算法。
- 预期:当输入规模
-
O(n log n) 算法 (MergeSort, QuickSort 变体):
- 预期:当输入规模
n增大时,性能增长平缓。比较次数大致在n * log(n)的量级。 - 观察:
MergeSort和各种QuickSort的运行时间随n的增长远比 O(n²) 算法要慢。例如,从n=10000到n=100000(10倍),它们的运行时间增长远小于100倍,符合n log n的趋势。MergeSort的比较次数非常稳定,接近理论值。
- 预期:当输入规模
-
快速排序变体对比:
QuickSort(基础版) 在随机数据下表现良好,但如果输入数据有序,其性能会退化到 O(n²)。QuickSortOpt(三数取中) 通过改进基准点选择,显著提高了在非完全随机数据下的稳定性,其比较和移动次数通常略优于基础版。QuickSort3Way在处理含大量重复元素的数组时优势最大(本次实验为随机数据,优势不明显),其在最好情况下(所有元素相同)可达 O(n)。DualPivotSort(双基准) 在理论上可以减少比较次数。从实验数据看,在较大规模的数据集上(如n=100000及以上),它通常比单基准的快速排序更快,显示出其优化效果。
3. 程序运行指导
编译
所有相关的 .cpp 源文件需要一起编译。可以使用 g++ 编译器,命令如下:
g++ main.cpp InsertSort.cpp BubbleSort.cpp ShellSort.cpp MergeSort.cpp QuickSort.cpp QuickSortOptimized.cpp QuickSort3Way.cpp DualPivotQuickSort.cpp out.cpp -o sorting_experiment
此命令会生成一个名为 sorting_experiment 的可执行文件。
运行
直接在终端中运行生成的可执行文件:
./sorting_experiment
输出结果说明
程序会输出一个性能分析表格,每一行代表一个算法在一个特定输入规模下的测试结果。
| 列 | 说明 |
|---|---|
| Algorithm | 被测试的排序算法名称。 |
| Size | 输入数组的元素个数(即规模 n)。 |
| Avg Time (s) | 多次重复测试的平均运行时间,单位为秒。 |
| Avg Comparisons | 平均比较次数。 |
| Avg Moves | 平均移动(赋值/交换)次数。 |
| Correct? | 排序结果是否正确,"Yes" 表示正确。 |
4. 性能对比与结论
-
算法类别差异:O(n²) 级别的算法(插入排序、冒泡排序)仅适用于小规模数据。当数据规模超过一万时,其性能急剧下降,无法在实际应用中使用。相比之下,O(n log n) 级别的算法(归并、希尔、快速排序)则表现出卓越的性能和可扩展性。
-
快速排序的优势与优化:在所有 O(n log n) 算法中,快速排序及其变体通常在平均情况下的性能最好,这得益于其更少的常量因子和高效的缓存利用率。
- 基准点选择至关重要:基础的快速排序在特定数据模式下存在性能退化的风险,而“三数取中”等优化策略能有效缓解此问题,增强算法的稳定性。
- 双基准快排的威力:
DualPivotQuickSort在大规模随机数据上展现了最佳性能,验证了其在现代计算环境下的理论优势。
-
归并排序的稳定性:虽然
MergeSort在本次测试中的原始速度略逊于最优的快速排序,但它具有一个重要优点:其性能是稳定的 O(n log n),不受输入数据初始顺序的影响。此外,归并排序是一种稳定的排序算法,而快速排序不是。
最终结论:对于通用场景下的内部排序任务,经过优化的快速排序(特别是双基准快速排序)是性能上的首选。而当需要排序稳定性或对最坏情况有严格要求时,归并排序是更可靠的选择。