【算法设计与分析】排序算法性能分析

目录

实验内容与结果

实现选择排序、冒泡排序、合并排序、快速排序、插入排序算法

选择排序

冒泡排序

合并排序

快速排序

插入排序

以待排序数组的大小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

其中 \sum_{i = 1}^{n}ti表示i固定,j取不同值时,不同的i值下minIndex的更新次数

选择排序的运行时间:

T(n) = c1 * (n-1) + c2 * (n-1) + c3 * n(n-1)/2 + c4 * n(n-1)/2 + c5 * \sum_{i = 1}^{n}ti+ 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

其中\sum_{end = 2}^{n} tend表示end取不同值时minIndex的更新次数

冒泡排序的运行时间:

T(n) = c1* n + c2* n(n-1)/2 + c3* (n-1)n/2 + c4*\sum_{end = 2}^{n} tend

对其中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],在内层循环中每一次循环都要将两元素调换, \sum_{end = 2}^{n} tend= 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

其中\sum_{i=2}^{n}ti表示i取不同值时为A[i]寻找位置进行比较的次数

插入排序的运行时间:

T(n) = c1* n + c2* (n-1) + c3* (n-1) + c4* (n-1) + c5* \sum_{i = 2}^{n}(ti-1)+ c6* \sum_{i = 2}^{n}(ti-1)+ c7 *\sum_{i = 2}^{n}(ti-1)+ c8*\sum_{i = 2}^{n}(ti-1) + 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

\sum_{i = 2}^{n}ti= n(n+1)/2 – 1

\sum_{i = 2}^{n}(ti-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)

27252526264935272527
25262926252425242423

(2) 冒泡排序

固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)

47444445504744455146
45454647474655464545

(3) 合并排序

固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)

1221011011
0111011201

(4) 快速排序

固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)

21121112111
2121112011

(5) 插入排序

固定输入规模n = 10000时,20组测试样本的运行时间如下表(单位:ms)

9965665668
7656567565

分别以n=10000, n=20000, n=30000, n=40000, n=50000等等,重复2的实验,画出不同排序算法在20个随机样本的平均运行时间与输入规模n的关系

问题规模1000020000300004000050000
选择排序27.0564140.45242.25372.8
冒泡排序45.95207.5640.251287.12074.15
合并排序0.71.752.853.554.45
快速排序2.054.9510.8522.829.35
插入排序5.926.5557.5598.6155.45

画出理论效率分析的曲线和实测的效率曲线

(1) 选择排序

以n=10000为基准,根据选择排序的时间复杂度O(n²)算出选择排序的理论运行时间

问题规模n1000020000300004000050000
理论运行时间/ms27.05108.2243.45432.8676.25
实际运行时间/ms27.0564140.45242.25372.8

(2) 冒泡排序

以n=10000为基准,根据冒泡排序的时间复杂度O(n²)算出选择排序的理论运行时间

问题规模n1000020000300004000050000
理论运行时间/ms45.95183.8413.55735.21148.75
实际运行时间/ms45.95207.5640.251287.12074.15

(3) 合并排序

以n=10000为基准,根据合并排序的时间复杂度O(nlog2n)算出选择排序的理论运行时间

问题规模n1000020000300004000050000
理论运行时间/ms0.71.512.353.224.11
实际运行时间/ms0.71.752.853.554.45

(4) 快速排序

以n=10000为基准,根据快速排序的时间复杂度O(nlog2n)算出选择排序的理论运行时间

问题规模n1000020000300004000050000
理论运行时间/ms2.054.416.889.4312.04
实际运行时间/ms2.054.9510.8522.829.35

(5) 插入排序

以n=10000为基准,根据快速排序的时间复杂度O(n²)算出选择排序的理论运行时间

问题规模n1000020000300004000050000
理论运行时间/ms5.923.653.194.4147.5
实际运行时间/ms5.926.5557.5598.6155.45

现在有10亿的数据(每个数据四个字节),请快速挑选出最大的十个数,并在小规模数据上验证算法的正确性。

尝试在10万个数据上进行测试。因为合并排序和快速排序的时间复杂度较小,故使用这两种排序算法,生成20组随机样本进行测试。

合并排序快速排序
平均运行时间/ms7.9591.75

尝试使用计数排序,计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当 O(k)>O(nlogn) 的时候其效率反而不如基于比较的排序,因为基于比较的排序的时间复杂度在理论上的下限是 O(nlogn)

关于计数排序的详细解释可查阅排序——计数排序(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进行实验,故得出结果为快速排序的速度反而比归并排序慢一些。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值