实验1 排序算法性能分析
一、实验目的
- 掌握选择排序、冒泡排序、插入排序、合并排序、快速排序算法原理
- 掌握不同排序算法时间效率的经验分析方法,验证理论分析与经验分析的一致性。
- 求解TOP K问题,并分析比较不同算法效率。
二、实验概述
排序问题要求我们按照升序排列给定列表中的数据项,目前为止,已有多种排序算法提出。本实验要求掌握选择排序、冒泡排序、插入排序、合并排序、快速排序算法原理,并进行代码实现。通过对大量样本的测试结果,统计不同排序算法的时间效率与输入规模的关系,通过经验分析方法,展示不同排序算法的时间复杂度,并与理论分析的基本运算次数做比较,验证理论分析结论的正确性。
三、实验内容。
1、实现选择排序、冒泡排序、插入排序、合并排序、快速排序算法;
2、以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计不同排序算法在20个样本上的平均运行时间;
3、分别以n=10万, n=20万, n=30万, n=40万, n=50万等等,重复2的实验,画出不同排序算法在20个随机样本的平均运行时间与输入规模n的关系。
(1)问题描述
本实验要求实现五种经典排序算法(选择排序、冒泡排序、插入排序、合并排序、快速排序),并测试其在不同输入规模 n 下的平均运行时间。通过统计不同 n(如 100000、200000、300000、400000、500000)的随机数组的排序时间,分析算法的时间复杂度与实际运行效率之间的关系,并绘制时间-规模关系图。
(2)原理描述
1. 选择排序
算法原理:
选择排序通过不断选择未排序部分的最小元素,将其与未排序部分的起始位置交换。每次迭代确定一个元素的最终位置。
实现细节
1. 外层循环遍历数组,当前索引i
表示已排序部分的末尾。
2. 内层循环从i
开始寻找最小元素的索引min_index
。
3. 将a[i]
与a[min_index]
交换,完成一次迭代。
算法行为
- 无论输入是否有序,选择排序的比较次数固定,但交换次数固定为 O(n)。
时间复杂度分析
- 比较次数:
每轮选择最小元素需要比较剩余所有元素:
- 交换次数:
每轮交换一次,共 (n-1) 次,即 O(n)。 - 结论:
无论最优、最差、平均情况,时间复杂度均为O(n^2)。
2.插入排序
算法原理:
插入排序将数组分为已排序和未排序两部分,逐个将未排序元素插入已排序部分的正确位置。
实现细节
1. 外层循环从第二个元素开始(索引i=1
),当前元素为key
。
2. 内层循环从i-1
向前遍历,将比key
大的元素后移,直到找到key
的插入位置。
3. 将key
插入到正确位置。
算法行为
- 最优情况:输入数组已完全有序(升序)。
- 最差情况:输入数组完全逆序。
- 平均情况:输入数组元素随机分布。
时间复杂度分析
- 比较与移动次数:
- 每次将第 i 个元素插入到已排序子数组时,需要比较 i 次(最坏情况)。
- 平均情况下,假设元素插入的位置是随机的,每个元素需要移动 i/2 次。
- 数学推导:
-
最坏时间复杂度:
比较次数: -
移动次数:同理为O(n^2)。
-
最优时间复杂度:
若输入已有序,每次只需比较 1 次,无需移动:
-
平均时间复杂度:
假设每个元素插入的位置均匀分布,则平均比较次数为:
-
3.冒泡排序
算法原理
冒泡排序通过相邻元素比较和交换,将最大元素逐步“冒泡”到数组末尾。
实现细节
1. 外层循环控制遍历次数,每轮减少内层循环的范围。
2. 内层循环遍历未排序部分,若相邻元素逆序则交换。
3. 若一轮无交换,提前终止(优化)。
算法行为
- 最优情况:输入已有序,提前终止。
- 最差情况:输入逆序,需完成所有遍历。
时间复杂度分析
- 比较与移动次数:
- 每次将第 i 个元素插入到已排序子数组时,需要比较 i 次(最坏情况)。
- 平均情况下,假设元素插入的位置是随机的,每个元素需要移动 i/2 次。
- 数学推导:
-
最坏时间复杂度:
比较次数:
交换次数:同样为 (O(n^2))(每次比较均需交换)。
-
最优时间复杂度:
若输入已有序,每次只需比较 1 次,无需移动:
-
平均时间复杂度:
假设每个元素插入的位置均匀分布,则平均比较次数为:
-
4.合并排序
算法原理
合并排序采用分治法,将数组递归划分为子数组,排序后合并。
实现细节
1.分割:将数组分为两半,递归排序。
2.合并:创建临时数组,按序合并左右子数组。
3. 处理剩余元素:将未遍历完的子数组元素直接复制。
算法行为
- 分治策略:递归分割数组,合并两个有序子数组。
- 关键操作:合并过程需线性时间 O(n)。
时间复杂度分析
-
递归式:
-
求解递归式:
-
结论:
无论最优、最差、平均情况,时间复杂度均为 O(n \log n)。。
5.快速排序
算法原理
快速排序通过基准元素(pivot)将数组划分为两部分,递归排序子数组。
实现细节
1. 分区:选择末尾元素为基准,将小于基准的元素移到左侧。
2. 递归:对基准左右子数组递归排序。
算法行为
- 最优情况:每次分区均匀分割数组。
- 最差情况:每次分区极不均衡(如已有序数组)。
- 平均情况:分区随机化后,期望分割均衡。
时间复杂度分析
-
最坏时间复杂度:
每次分区选择的基准为最大/最小元素,递归式为:
-
最优时间复杂度:
-
每次分区均匀分割,递归式为:
-
平均时间复杂度:
推导过程如下:
假设:存在常数 c>0,使得对所有 m<n,有 T(m)≤c⋅mlogm。
需估计k=0n-1klogk的上界:
当 (k ≥2) 时,(logk≤logn),因此:
代入原式:
**结论:**平均情况,时间复杂度为 O(n \log n)。
算法 | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O(n²) | O(1) | 不稳定 |
插入排序 | O(n²) | O(1) | 稳定 |
冒泡排序 | O(n²) | O(1) | 稳定 |
合并排序 | O(n log n) | O(n) | 稳定 |
快速排序 | O(n log n) | O(log n) | 不稳定 |
(3)算法效率测试结果及其分析
测试数据:
测试图像:
测试平台:cloudstudio
以100000为基准值,插入,选择,冒泡排序的换算通式为t=t0*n/1000002 (t0为n=100000时的耗时)
快速,归并排序的换算通式为t =t0*[(n/100000)+n/100000*(log(n/100000)/logn)]
由此计算可得数据以下
理论数据:
理论图像:
图像分析:
由上述测试数据可以很明显的看到本应相差不大的快速排序和合并排序居然相差了一个数量级,考虑到平台原因,在本地上和快排重新跑了一下,得到新数据以下
可见合并排序的得到的数据正常了些,但是仍旧比快排要慢,由此我分析了原因
原因包括:
快速排序的优势
- 缓存局部性 :
快速排序是原地排序,数据访问更集中,缓存命中率更高,尤其对大规模数据更友好。 - 更少的内存操作 :
无需像合并排序那样频繁申请/释放临时数组,减少内存管理的开销。 - 随机化优化 :
随机选择基准可避免极端分区,确保平均 O(nlogn) 时间。
合并排序的劣势
- 额外空间开销 :
合并排序需要 O(n) 的辅助空间,对内存带宽压力更大。 - 合并过程的开销 :
合并两个子数组需要逐元素比较和复制,实际耗时可能比快速排序的分区操作更高。
若需稳定性或避免最坏情况,合并排序是更安全的选择,但速度可能稍逊。
对于时间复杂度为O(n2)的冒泡,选择和插入排序,在十万级随机数据量下,插入排序 通常优于选择排序和冒泡排序,而冒泡排序表现最差。
关键性能差异
(1) 插入排序的优劣势
- 局部性原理 :插入排序逐个处理元素,内存访问更集中,缓存命中率高。
- 实际常数因子小 :虽然时间复杂度同为 O(n2),但插入排序的循环体更紧凑(如内层循环的移动操作是顺序的)。
- 适应性 :在部分有序数据中表现更好(但随机数据中无优势)。
(2) 选择排序的优劣势
- 固定比较次数 :无论数据是否有序,必须完成所有比较,无法提前终止。
- 交换次数最少 :但交换操作(如 swap)的开销远小于比较操作,因此整体性能仍低于插入排序。
(3) 冒泡排序的优劣势
- 大量交换操作 :每轮遍历可能触发多次交换(每次交换需 3 次赋值),导致实际耗时显著增加。
- 缓存不友好 :频繁的随机内存访问(相邻元素交换)导致缓存效率低下。
(4)经验总结
1.时间复杂度:
O(n²) 算法仅适用于小规模数据(n < 1万),而 O(n log n) 算法可高效处理百万级数据。
2.实际优化因素:
快速排序的基准选择策略显著影响性能,随机化基准可避免最坏情况。
合并排序的额外空间开销可能限制其在内存敏感场景的应用。
3.注意事项:
对基本有序数据,插入排序可作为快速排序的补充。
4.测试优化:
在测试的时候,当数据量逐渐提升时,由于三个时间复杂度为O(n2)算法瓶颈以及每组需要跑20个样本,导致速度极慢,因此我使用了多线程来解决这个问题,多线程有两个方向来优化:(1)为每个算法创建一个线程,五个算法一起跑(2)为20个样本创建20个线程,一个一个算法跑。但是由于cpu瓶颈,20个线程太多,因此作罢,采用第一个解决方法。采用后测试速度明显提高,提升了大概1/3的速度,瓶颈卡在了冒泡。
(5)TOP K问题
问题描述:现在有10亿的数据(每个数据四个字节),请快速挑选出最大的十个数,并在小规模数据上验证算法的正确性
快速选择:
平均时间复杂度 :O(n)
快速选择基于快速排序的分治思想,但每次仅递归处理包含目标元素的子数组。平均情况下,每次划分将问题规模减半,总时间复杂度为线性。
堆选择:
时间复杂度 :O(n log k)
通过维护一个大小为 k 的堆(如找前K大元素时使用最小堆),遍历数组时每个元素最多进行一次堆调整操作(时间复杂度 O(log k)),总共有 n 次操作。
运行平台:windows
Cpu:i5-13500H
据图可总结出下列经验:
- 优先选择快速选择 :如果是一次性求解 TOP K 且数据分布随机,快速选择通常是最佳选择。
- 优先选择堆选择 :如果是动态数据流场景或需要稳定性能,堆选择是最优解。
- 避免使用冒泡选择 :除非数据规模极小,否则冒泡选择几乎没有实用价值。