目录
以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计不同排序算法在20个样本上的平均运行时间
分别以n=10000, n=20000, n=30000, n=40000, n=50000等等,重复2的实验,画出不同排序算法在20个随机样本的平均运行时间与输入规模n的关系
现在有10亿的数据(每个数据四个字节),请快速挑选出最大的十个数,并在小规模数据上验证算法的正确性。
前言
本文章旨在分享学习经历,如有错误请指正。侵删
实验目的
1.掌握选择排序、冒泡排序、合并排序、快速排序、插入排序算法原理
2.掌握不同排序算法时间效率的经验分析方法,验证理论分析与经验分析的一致性。
实验内容与结果
实现选择排序、冒泡排序、合并排序、快速排序、插入排序算法
选择排序
计算过程:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
伪代码:
SELECTION-SORT(A,n) cost times
for i = 1 to n - 1 c1 n-1
minIndex = i c2 n-1
for j = i + 1 to n c3 n(n-1)/2
if A[j] < A[minIndex] c4 n(n-1)/2
minIndex = j c5 ∑ti
exchange A[i] with A[minIndex] c6 n
其中 表示i固定,j取不同值时,不同的i值下minIndex的更新次数
选择排序的运行时间:
T(n) = c1 * (n-1) + c2 * (n-1) + c3 * n(n-1)/2 + c4 * n(n-1)/2 + c5 * + c6 * n
对其中ti的取值进行讨论
-
最好情况:数组已排好序
此时在整个循环测试中,总是有A[j] >= A[minIndex],因此在每一次循环中都不需要更新minIndex的值,所有的ti均为0
运行时间T(n) = c1* (n-1) + c2* (n-1) + c3* (n-1)n/2 + c4* n(n-1)/2 + c6*n
T(n) 可表示为 an² + bn + c,其中a,b,c依赖于语句代价c
-
最坏情况:数组已逆序排好
此时在整个循环测试中,总是有A[j] < A[minIndex],在内层循环中每一次循环都要更新一次minIndex值,ti= 1 + 2 + ··· + n - 1 = n(n-1)/2
运行时间T(n) = c1* (n-1) + c2* (n-1) + c3* (n-1)n/2 + c4* n(n-1)/2 + c5* n(n-1)/2 + c6*n
T(n)可表示为 an² + bn + c,其中a,b,c依赖于语句代价c
-
平均情况:不同的比较次数等概率出现
ti可以理解为随机变量
E(ti) = (1 + 2 + 3 + ··· + i - 1) / (i - 1) = i / 2
参考最坏情况,T(n)可表示为an² + bn + c,则平均情况下的运行时间仍然是n的二次函数
冒泡排序
计算过程:
每一趟都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数的大小关系,重复此步骤,直到最后一个还没归位的数。
伪代码:
BUBBLE-SORT(A.n) cost times
for end = n to 2 c1 n-1
for i = 1 to end - 1 c2 n(n-1)/2
if A[i] > A[i+1] c3 n(n-1)/2
exchange A[i] with A[i+1] c4 ∑tend
其中表示end取不同值时minIndex的更新次数
冒泡排序的运行时间:
T(n) = c1* n + c2* n(n-1)/2 + c3* (n-1)n/2 + c4*
对其中ti的取值进行讨论
-
最好情况:数组已排好序
此时在整个循环测试中,总是有A[i] <= A[i+1],因此不需要将两元素调换,所有的tend均为0
运行时间T(n) = c1* n + c2* n(n-1)/2 + c3* (n-1)n/2
T(n) 可表示为 an² + bn + c,其中a,b,c依赖于语句代价c
-
最坏情况:数组已逆序排好
此时在整个循环测试中,总是有A[i] >= A[i+1],在内层循环中每一次循环都要将两元素调换, = n(n-1)/2
运行时间T(n) = c1* n + c2* n(n-1)/2 + c3* (n-1)n/2- + c4* n(n-1)/2
T(n) 可表示为 an² + bn + c,其中a,b,c依赖于语句代价c
-
平均情况:不同的比较次数等概率出现
tend理解为随机变量
E(tend) = (1 + 2 + 3 + ··· + end - 1) / (end - 1) = end / 2
参考最坏情况,T(n)可表示为an² + bn + c,则平均情况下的运行时间仍然是n的二次函数
合并排序
计算过程:
把一组n个数的序列,折半分为两个序列,然后再将这两个序列再分,一直分下去,直到分为n个长度为1的序列。然后两两按大小归并。如此反复,直到最后形成包含n个数的一个数组。
伪代码:
MERGE-SORT(A,left,right) cost times
mid = left + (right - left) / 2 c1 1
if left < right c2 1
MERGESORT(A,left,mid) c3 n*T(1)
MERGESORT(A,mid+1,right) c4 n*T(1)
MERGE(A,left,mid,right,result) c5 n*(logn)
MERGE(A,left,mid,right,result) cost times
i = left c1 1
j = mid + 1 c2 1
k = 1 c3 1
while i <= mid and j <= right c4 t1
if arr[i] < arr[j] c5 t1-1
result[k++] = A[i++] c6 ti
else c7 t1-1
result[k++] = A[j++] c8 tj
while i <= mid c9 n/2-ti+1
result[k++] = A[i++] c10 n/2-ti
while j <= right c11 n/2-tj+1
result[k++] = A[j++] c12 n/2-tj
for a = 1 to n c13 n+1
A[a+left] = result[a] c14 n
其中ti表示arr的左半边元素插入result数组的插入次数,tj表示将arr的右半边元素插入数组的插入次数
合并排序的运行时间:
T(n) = c1 + c2 + c3* n* T(1) + c4* n * T(1) + c6* n *logn
可以表示成T(n) = a*nlogn + b,其中a,b取决于语句代价c
快速排序
计算过程:
每次排序的时候设置一个基准点,同时从左右两边开始检索,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。
伪代码:
QUICK-SORT(A,left,right) cost times
if left > right c1 1
return c2 1
temp = A[left] c3 1
i = left c4 1
j = right c5 1
while i != j c6 t
while A[j] >= temp and i < j c7 tj
j-- c8 tj-1
while A[i] <= temp and i < j c9 ti
i++ c10 ti-1
if i < j c11 t-1
exchange A[i] and A[j] c12 te
A[left] = A[i] c13 1
A[i] = temp c14 1
QUICK-SORT(A,left,i-1) c15 nlogn
QUICK-SORT(A,i+1,right) c16 nlogn
其中tj表示指针j向左移动的次数,ti表示指针i向右移动的次数,te表示左右两指针指向的值交换的次数
快速排序的运行时间:
T(n) = c1 + c2 + c3 + c4 + c5 + c6 + c7* tj + c8* (tj-1) + c9* ti + c10* (ti-1) + c11* (t-1) + c12* te + c13 + c14 + c15* nlogn + c16*nlogn
可以表示成T(n) = a*nlogn + b,其中a,b取决于语句代价c
插入排序
计算过程:
对于未排序数据,在一排好序的序列中从后向前扫描,找到相应位置并插入。在从后向前扫描的过程中需要反复将以排序的元素逐步向后移位,为新元素提供插入空间。
伪代码:
INSERTION-SORT(A,n) cost times
for i = 2 to n c1 n
insertVal = A[i] c2 n-1
index = i c3 n-1
j = i - 1 c4 n-1
while j > 0 and A[j] > insertVal c5 ∑ti
A[j+1] = A[j] c6 ∑(ti-1)
index = j c7 ∑(ti-1)
j-- c8 ∑(ti-1)
A[index] = insertVal c9 n-1
其中表示i取不同值时为A[i]寻找位置进行比较的次数
插入排序的运行时间:
T(n) = c1* n + c2* (n-1) + c3* (n-1) + c4* (n-1) + c5* + c6* + c7 *+ c8* + c9*(n-1)
对其中ti的取值进行讨论
-
最好情况:数组已排好序
此时在整个循环测试中,总是有A[j] <= insertVal,因此不需要将元素移动,所有的ti均为1
运行时间T(n) = c1* n + c2 * (n-1) + c3* (n-1) + c4* (n-1) + c5 + c9* (n-1)
T(n)可表示为an + b,其中a,b依赖于语句代价c
-
最坏情况:数组已逆序排好
此时在整个循环测试中,总是有A[j] > insertVal,必须将元素A[i]与整个已排序子数组A[1...j]中的每个元素进行比较, ti= j = i - 1
= n(n+1)/2 – 1
= n(n-1)/2
运行时间T(n) = c1* n + c2* (n-1) + c3* (n-1) + c4* (n-1) + c5* [n(n+1)/2 – 1] + c6* n(n-1)/2+ c7* n(n-1)/2 + c8* n(n-1)/2 + c9*(n-1)
T(n) 可表示为 an2 + bn + c,其中a,b,c依赖于语句代价c
-
平均情况:不同的比较次数等概率出现
tj理解为随机变量
E(tj) = (1 + 2 + 3 + ··· + j) / j = (1 + j) / 2
参考最坏情况,T(n)可表示为an2 + bn + c,则平均情况下的运行时间仍然是n的二次函数
以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计不同排序算法在20个样本上的平均运行时间
(1) 选择排序
固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)
27 | 25 | 25 | 26 | 26 | 49 | 35 | 27 | 25 | 27 |
---|---|---|---|---|---|---|---|---|---|
25 | 26 | 29 | 26 | 25 | 24 | 25 | 24 | 24 | 23 |
(2) 冒泡排序
固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)
47 | 44 | 44 | 45 | 50 | 47 | 44 | 45 | 51 | 46 |
---|---|---|---|---|---|---|---|---|---|
45 | 45 | 46 | 47 | 47 | 46 | 55 | 46 | 45 | 45 |
(3) 合并排序
固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)
1 | 2 | 2 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | 1 | 0 | 1 | 1 | 2 | 0 | 1 |
(4) 快速排序
固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)
21 | 1 | 2 | 1 | 1 | 1 | 2 | 1 | 1 | 1 |
---|---|---|---|---|---|---|---|---|---|
2 | 1 | 2 | 1 | 1 | 1 | 2 | 0 | 1 | 1 |
(5) 插入排序
固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)
9 | 9 | 6 | 5 | 6 | 6 | 5 | 6 | 6 | 8 |
---|---|---|---|---|---|---|---|---|---|
7 | 6 | 5 | 6 | 5 | 6 | 7 | 5 | 6 | 5 |
分别以n=10000, n=20000, n=30000, n=40000, n=50000等等,重复2的实验,画出不同排序算法在20个随机样本的平均运行时间与输入规模n的关系
问题规模 | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
选择排序 | 27.05 | 64 | 140.45 | 242.25 | 372.8 |
冒泡排序 | 45.95 | 207.5 | 640.25 | 1287.1 | 2074.15 |
合并排序 | 0.7 | 1.75 | 2.85 | 3.55 | 4.45 |
快速排序 | 2.05 | 4.95 | 10.85 | 22.8 | 29.35 |
插入排序 | 5.9 | 26.55 | 57.55 | 98.6 | 155.45 |
画出理论效率分析的曲线和实测的效率曲线
(1) 选择排序
以n=10000为基准,根据选择排序的时间复杂度O(n²)算出选择排序的理论运行时间
问题规模n | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
理论运行时间/ms | 27.05 | 108.2 | 243.45 | 432.8 | 676.25 |
实际运行时间/ms | 27.05 | 64 | 140.45 | 242.25 | 372.8 |
(2) 冒泡排序
以n=10000为基准,根据冒泡排序的时间复杂度O(n²)算出选择排序的理论运行时间
问题规模n | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
理论运行时间/ms | 45.95 | 183.8 | 413.55 | 735.2 | 1148.75 |
实际运行时间/ms | 45.95 | 207.5 | 640.25 | 1287.1 | 2074.15 |
(3) 合并排序
以n=10000为基准,根据合并排序的时间复杂度O(nlog2n)算出选择排序的理论运行时间
问题规模n | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
理论运行时间/ms | 0.7 | 1.51 | 2.35 | 3.22 | 4.11 |
实际运行时间/ms | 0.7 | 1.75 | 2.85 | 3.55 | 4.45 |
(4) 快速排序
以n=10000为基准,根据快速排序的时间复杂度O(nlog2n)算出选择排序的理论运行时间
问题规模n | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
理论运行时间/ms | 2.05 | 4.41 | 6.88 | 9.43 | 12.04 |
实际运行时间/ms | 2.05 | 4.95 | 10.85 | 22.8 | 29.35 |
(5) 插入排序
以n=10000为基准,根据快速排序的时间复杂度O(n²)算出选择排序的理论运行时间
问题规模n | 10000 | 20000 | 30000 | 40000 | 50000 |
---|---|---|---|---|---|
理论运行时间/ms | 5.9 | 23.6 | 53.1 | 94.4 | 147.5 |
实际运行时间/ms | 5.9 | 26.55 | 57.55 | 98.6 | 155.45 |
现在有10亿的数据(每个数据四个字节),请快速挑选出最大的十个数,并在小规模数据上验证算法的正确性。
尝试在10万个数据上进行测试。因为合并排序和快速排序的时间复杂度较小,故使用这两种排序算法,生成20组随机样本进行测试。
合并排序 | 快速排序 | |
---|---|---|
平均运行时间/ms | 7.95 | 91.75 |
尝试使用计数排序,计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当 的时候其效率反而不如基于比较的排序,因为基于比较的排序的时间复杂度在理论上的下限是 。
关于计数排序的详细解释可查阅排序——计数排序(Count sort)_努力的老周的博客-CSDN博客
伪代码如下:
COUNTING-SORT(A,n,count,result)
max = A[0]
for i = 1 to n
if max < A[i]
max = A[i]
for i = 1 to n
cout[arr[i]++]
index = 0
for i = 1 to max
while count[i] > 0
result[index++] = i
count[i]--
测得程序平均运行时间为0.85ms,比合并排序和快速排序都要快。
实验总结
由本次实验可知,效率较高的排序算法为快速排序和合并排序,两者效率非常接近,其他三种排序算法效率较低,其中选择排序和插入排序效率较为接近。此外,五种排序算法的实际运行效率和理论运行效率曲线基本吻合,可以推测选择排序、冒泡排序、插入排序的时间复杂度为O(n²),合并排序、快速排序的时间复杂度为O(nlogn)
补充:
可以看到在本实验的结果中,快速排序比合并排序的速度要慢,查阅相关资料(原文:快排和归并排序哪个更快归并排序和快速排序哪个效率高AnDiXL的博客-CSDN博客)发现有以下原因:
1.C++模板有很强的inline优化机制,比较操作相对于赋值(移动)操作要快的多(尤其是元素较大时)
2.另一方面,一般情况下,归并排序的比较次数小于快速排序的比较次数,而移动次数一般多于快速排序的移动次数,二者大约都是2~3倍的差距。
因为这样,在C++中快排要比归并排序更快,但其实在Java中恰恰相反,移动(赋值)一般比较快,在本实验中笔者使用的是Java进行实验,故得出结果为快速排序的速度反而比归并排序慢一些。