算法设计与分析(实验一)

实验目的

掌握选择排序、冒泡排序、合并排序、快速排序、插入排序算法原理

掌握不同排序算法时间效率的经验分析方法,验证理论分析与经验分析的一致性。

二、实验概述

        排序问题要求我们按照升序排列给定列表中的数据项,目前为止,已有多种排序算法提出。本实验要求掌握选择排序、冒泡排序、合并排序、快速排序、插入排序算法原理,并进行代码实现。通过对大量样本的测试结果,统计不同排序算法的时间效率与输入规模的关系,通过经验分析方法,展示不同排序算法的时间复杂度,并与理论分析的基本运算次数做比较,验证理论分析结论的正确性。

三、实验内容(实验步骤与结果

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

(1)冒泡排序

 ①算法原理

第i趟起泡排序从1到n-i+1,依次比较相邻两个记录的关键字,如果发生逆序,则交换。即每一轮排序都会将最大值移动到该序列的最后元素位置(关键字小的记录不断向前移动,关键字大的记录向后移动)。

如图5所示,以num[8]={ 4,5,2,3,6,0,9,1 }为例,演示冒泡排序过程:第一趟中,从第一个元素开始进行与下一位元素关键字的比较,如果该元素大于后一个元素,则交换位置,直到遍历到最后一个元素的前一位,此时最后一个元素的位置上应存放的是最大数9,第二趟排序中,从第一个元素开始与下一位元素进行关键字比较,直到遍历到第6个元素(也就是第8-1-1个元素),在第7个元素的位置上的数字比在此之前的所有元素均要大......第i趟扫描中,第一个元素到第n-i元素进行与后一位元素的比较和条件交换,此时第n-i+1个数比前面的都要大。总共进行n-1趟。排序结束,最后排序结果为:{0,1,2,3,4,5,7,9}。

②伪代码:

BubbleSort(Nums):

  1.   for i=1 to Nums.length      
  2.       j=0 to (Nums.length-i-1)  
  3.       //Compare the keywords of two adjacent records in turn
  4.       if  Nums[j] >  Nums[j+1]   
  5.          swap Nums[j+1] and Nums[j]   

③效率分析(时间复杂度)

        最好情况:初始有序,不交换,只执行n-1次关键字比较。

        最坏情况:执行n-1趟起泡,第i趟做n-i次关键字比较, 执行n-i次记录交换。即总共比较次数为

n(n-1)/2,总共移动次数为3n(n-1)/2。

        因此比较次数与关键字的初始状态无关,选择排序的时间复杂度为O(n^2)。

        以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计该排序算法在20个样本上的平均运行时间。见表2.(注:以输入规模为10000的数据运行时间为基准点)

④效率曲线

理论效率分析的曲线和实测的效率曲线见图7:

解释与分析:

        由于冒泡排序算法时间复杂度为O(n^2),故数量级扩大10倍时,时间消耗应扩大100倍。观察图7、图8及表2,发现随着数据量的增大,拟合效果先是越来越好,再越来越差,分析原因,当数据较小时,资源损耗的影响将变得显著;数据较大时,可能是交换数据时的操作导致时间延长,再经过实验验证得出,使用swap函数可以提高拟合度,且再做对比实验时,整体拟合度比初次实验要更优,说明实验存在偶然性和误差。

        总体上来说所有实验数据符合O(n^2)的时间复杂度。

(2)选择排序

(1)选择排序

算法原理

a.每次在待排序的序列中找到最大的元素,与当前序列末尾元素交换位置

b.将末尾元素前的序列作为待排序序列再次进行操作a

c.重复b直到待排序序列元素个数为1,排序结束。

        在寻找最大值时,设定参考值为数组的第一个数,存放最大数下标的变量初始化为0,故可以直接从第二个数开始比较,最后得出最大值。若当前待排序序列有k个数,则应在数组前k个数中找最大数并与第k个数进行交换。

        如图1所示,以num[8]={ 4,5,2,3,6,0,9,1 }为例,演示选择排序过程:在循环第一轮中,在前8个元素中找到最大元素9,与末尾元素(第8个元素)1进行交换,结果为:{ 4,5,2,3,6,0,1,9 }。第二轮中,在前7个元素中找到最大数6,并与第7个元素进行交换...在第m轮中,在前(8-m)个数中找到最大值,并于第m个元素进行交换...直到待排序序列只有一个数。排序结束,最后排序结果为:{0,1,2,3,4,5,7,9}。

②伪代码:

 SelectionSort(Nums):

  1.   for i=0 to Nums.length  
  2.        max=Nums[0]  
  3.        index=0     
  4.        j=1 to (Nums.length-i)  
  5.        //Current range, place the largest number at the end position  
  6.         if  Nums[j] > maxx   
  7.          maxx = nums[j];  
  8.              index = j;  
  9.         swap Nums[index] and Nums[j]    

③效率分析(时间复杂度)

        设整个待排序记录序列有n个数,则第i趟选择最大关键字记录所需的比较次数总是 n-i次。总的关键字比较次数为 n(n-1)/2,交换次数为O(n)。最好情况:初始有序,不交换;最坏情况:每一趟都要进行交换,总的记录移动次数为3(n-1)。因此比较次数与关键字的初始状态无关,选择排序的时间复杂度为O(n^2)。

        以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计该排序算法在20个样本上的平均运行时间,见表1。(注:以输入规模为10000的数据运行时间为基准点)

④效率曲线

理论效率分析的曲线和实测的效率曲线见图4:

解释与分析:

        由于选择排序算法时间复杂度为O(n^2),故数量级扩大10倍时,时间消耗应扩大100倍。由两图观察可得随着数据量的增大,拟合效果越好,所有实验数据符合O(n^2)的时间复杂度。误差在0~10%左右。

        观察表1和图3、图4可以发现,在输入规模为50000到90000间实际测量值与理论值存在约10%的误差。重复实验时发现误差减小至5%左右,说明实验存在偶然性,有可能会造成较大的误差。而且,经过实验发现,当使用swap函数代替存储交换操作时,会使实际值更加偏向于理论值。

(3)归并排序(合并排序)

算法原理(以二路归并为例)

a.将初始序列不断一分为二,直到每个新序列只有一个元素。

b.将前后相邻的两个有序序列归并(类似于插入排序)为一个有序序列。

c.重复b操作,直到只有一个有序序列,归并排序结束。

        如图9所示,以num[8]={ 4,5,2,3,6,0,9,1 }为例,演示归并排序过程:将序列不断一分为二,直到所有序列只有一个元素{4},{5},{2},{3},{6},{0},{9},{1}。然后将前后两个序列合并,第一次合并结果为:{4,5},{2,3},{0,6},{1,9},第二次合并结果为:{2,3,4,5},{0,1,6,9},第三次合并为{0,1,2,3,4,5,7,9},排序结束。

②伪代码:

 MergeSort(Nums):

  1.   for i=1 to Nums.length      
  2.       j=1 to (Nums.length-i)  
  3.       //Compare the keywords of two adjacent records in turn
  4.       if  Nums[j] >  Nums[j+1]   
  5.          swap Nums[j+1] and Nums[j]    

③效率分析(时间复杂度)

        如果待排序的记录为n个,则需要做log2n趟两路归并排序,每趟两路归并排序的时间复杂度为O(n)。 假设待归并的两个有序表长度分别为a和b,两路归并操作至多只需要a+b次比较,因此两路归并的时间复杂度为O(a+b)。因此二路归并排序的时间复杂度为O(nlog2n)。

        以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计该排序算法在20个样本上的平均运行时间见表4:(注:以输入规模为10000的数据运行时间为基准点)

④效率曲线

理论效率分析的曲线和实测的效率曲线见图11:

解释与分析:

        合并排序算法的时间复杂度为:O(nlogn)。由于实测效率是运行时间,而理论效率是基本操作的执行次数,两者需要进行对应关系调整。调整思路为:以输入规模为10000的数据运行时间为基准点,计算输入规模为其他值的理论运行时间。设输入规模为10000的数据运行时间为k,则其他输入规模(n)计算公式为:t(n)=k*10000log2(10000)/nlog2(n).具体计算结果见表3。

        结合以上表3和图11、图12不难得出:整体时间消耗都大致满足O(nlogn)的时间复杂度。但对于输入规模为100的数据存在极大的偏差。查阅资料并分析得出合并排序需要额外的临时空间辅助,有一定的资源损耗,且当数据量较小时,资源损耗的影响将变得显著。导致实际时间会大于理论时间。当数据量较大且运行时间变长时,拟合效果变好。

(4)快速排序

算法原理

a.在每一个待排序序列中,取序列第一个元素为目标值,赋值给变量target,指针low指向序列第一个元素位置,指针high指向序列最后一个元素位置。

b.从high指向的记录开始,向前找到第一个关键字的值小于target的元素,将其放到low指向的位置,使low加1.

c.从low指向的记录开始,向后找到第一个关键字的值大于target的元素,将其放到high指向的位置,使high减1.

d.重复bc操作,直到low=high,将target的值放在low(high)指向的位置。

e.(此时,low(high)位置两边有两个子序列)对其两个子序列执行abcd操作,直到每个子序列都只有一个元素为止,排序结束。

 

如图13所示,以num[8]={ 4,5,2,3,6,0,9,1 }为例,演示快速排序过程:

第一趟排序中,将首位元素4作为目标值,存入target变量中,high指针指向最后一个元素的位置,low指针指向第一个元素的位置。从high指针指向位置向前找到第一个小于目标值(target=4)的元素:1,将1放入low指针指向位置,并让low+1(指针指向位置向后挪动一个)此时low指针指向5。从low指针指向位置向后找到第一个大于目标值(target=4)的元素:5,将5放入high指针指向位置,并让high-1(指针指向位置向前挪动一个)此时high指针指向9。从high指针指向位置向前找到第一个小于目标值(target=4)的元素:0(high指针也指向0),将0放入low指针指向位置,并让low+1(指针指向位置向后挪动一个)此时low指针指向2。从low指针指向位置向后找到第一个大于目标值(target=4)的元素:6,将6放入high指针指向位置,并让high-1(指针指向位置向前挪动一个)此时high=low,将target的值放入high(low)指针所指向的位置。至此第一趟排序结束。此时序列为{1,0,2,3,4,6,9,5}。目标数左右两边有两个子序列{1,0,2,3}和{6,9,5}。分别进行上述操作,第二趟循环结果为{0,1,2,3}和{5,6,9}。现在再次得到子序列:{0},{2,3},{5},{9}。此时,只需要对{2,3}进行上述排序操作,结果为{2,3}。至此,排序操作结束。最终结果为{0,1,2,3,4,5,7,9}。

②伪代码:

Quicksort (Nums, begin, end )

  1. if (end>begin)
  2. mid = quickSort(Nums, begin, end)
  3. quickSort (Nums, begin, mid-1)//左子序列进行相同排序操作
  4. quicksort (Nums, mid+1, end)//右子序列进行相同排序操作

quickSort(Nums, begin, end):

  1.   target=num[begin]     
  2.   high=end
  3.   low=begin      
  4.  while  high > low :
  5.  whlie  high > low and num[high] >= target
  6.                   //找到第一个比目标值小的数的下标
  7.  High - 1
  8.  if high > low
  9.  num[low] =num [high]
  10.  whlie high > low and num[low] <= target
  11.                  //找到第一个比目标值小的数的下标
  12.  Low + 1
  13.  if (high > low)
  14.  num[high] =num [low]
  15. list[low] = target
  16. return low

③效率分析(时间复杂度)

        快速排序的趟数取决于递归树的高度。

        最好的情况:如果每次划分对一个目标值定位后, 该记录的左子序列与右子序列的长度相同, 则下一步将是对两个长度减半的子序列进行排序, 即经过logn趟划分就结束,算法的时间复杂度为O(nlogn)。

        最坏的情况:每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的两个子序列中有一个为空,另一个长度为原序列的长度-1。则长度为n的序列的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n^2)

        综上快速排序的平均时间复杂度是O(nlogn)。

        以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计该排序算法在20个样本上的平均运行时间见表4:(注:以输入规模为10000的数据运行时间为基准点)

④效率曲线

理论效率分析的曲线和实测的效率曲线见图15:

解释与分析

        快速排序算法的时间复杂度为:O(nlogn)。由于实测效率是运行时间,而理论效率是基本操作的执行次数,两者需要进行对应关系调整。调整思路为:以输入规模为10000的数据运行时间为基准点,计算输入规模为其他值的理论运行时间。设输入规模为10000的数据运行时间为k,则其他输入规模(n)计算公式为:t(n)=k*10000log2(10000)/nlog2(n).具体计算结果见表4。

         结合表4及图15、图16不难得出:整体时间消耗都大致满足O(nlogn)的时间复杂度,且数据量越大拟合效果越好。但对于输入规模为100的数据存在极大的偏差,拟合效果较差。查阅资料并分析得出快速排序中开辟申请空间与递归栈都会影响运行性能和运行时间。当数据较小时,这种影响造成的时间损耗更明显。但当数据变多时,开辟空间与递归栈对时间的影响被冲淡,故拟合效果变好。

(5)插入排序

算法原理

特点:当插入第i(i≥1)个元素时, 前面的i-1个元素已经有序。

从第2个元素开始插入,直到所有元素插入完成。插入第i个元素时:用其关键字与num[i-1], num[i-2], …num[0]进行比较:如果小于,则将比较元素r[x]向后移动一位;否则,将待插入的元素放在该比较元素的下一个位置(如果当前num[0]比待插入值大,将num[0]后移一位并将待插入值放在num[0]的位置上)。

如图17所示,以num[8]={ 4,5,2,3,6,0,9,1 }为例,演示插入排序过程:

首先,插入第二个元素5(当序列中只有一个元素时,默认其为有序的,故从第二个元素开始插入):5比4大,故将5向后移动一位。其次,插入第三个元素2:2比5小,则将5向后移动一位,2比4小,将4向后移动一位。接着依次对后面的元素进行插入,直到所有元素插入成功。至此,排序操作结束。最终结果为{0,1,2,3,4,5,7,9}。

②伪代码:

InsertionSort(nums)

  1.   for i=2 to n //下标从1开始
  2.        for j=i-1 to 1
  3.            if j=0 and num[0]>num[i]
  4.                 num[1]=num[0]
  5.                 num[0]=num[i]
  6.            if num[j]>num[i]
  7.                 num[j+1]=num[j]
  8.            else
  9.                 num[j+1]=num[i]

③效率分析(时间复杂度)

        最好情况:初始有序, 每趟只需与前面有序记录序列的最后一个记录比较1次, 总的关键字比较次数为 n-1。

          最坏情况:第i趟时第i个记录必须与前面i个记录都做关键字比较, 并且每做1次比较就要做1次数据移动。总关键字比较次数为n(n-1)/2,记录移动次数为(n+4)(n-1)/2

        关键字比较次数和记录移动次数与记录关键字的初始排列有关。在平均情况下的关键字比较次数和记录移动次数约为 n*n/4。即插入排序的时间复杂度为O(n^2).

        以待排序数组的大小n为输入规模,固定n,随机产生20组测试样本,统计该排序算法在20个样本上的平均运行时间见表5:(注:以输入规模为10000的数据运行时间为基准点)

④效率曲线

理论效率分析的曲线和实测的效率曲线见图19:

解释与分析

        由于插入排序算法时间复杂度为O(n^2)。结合表5及图19、图20不难得出:数量级扩大10倍时,时间消耗应扩大100倍随着数据量的增大,拟合效果越好,所有实验数据符合O(n^2)的时间复杂度。但对于输入规模为100的数据存在极大的偏差,拟合效果较差。查阅资料并分析得出插入排序中进行了对需插入元素的保存操作,当数据较小时,较差的拟合效果是因为每次对插入前数值的保存占用的时间比例较大。当数据较小时,数据交换造成的时间损耗更明显。但当数据变多时,这种影响被冲淡,故拟合效果比较好。

2.不同排序算法在20个随机样本的平均运行时间与输入规模n的关系

可视化数据:

        对于数量级很小时,五种排序方式都时间很短且差距不大。但当数据量较大时,由表6、表7及图21、图22可以看出:快速排序算法有明显的优势。由图22可以明显看出:效率上, 快速排序>合并排序>插入排序>选择排序>冒泡排序。

        插入排序和选择排序和冒泡排序算法则需要消耗较多的时间完成交换排序操作。

        在时间复杂度同为O(n^2)的排序算法中,插入排序对于较少数据量级时,表现出了很优秀的性能,甚至比快速排序还要快。查阅资料发现:这是因为与快速排序相比:①插入排序没有额外内存的申请和释放开销。②插入排序没有递归栈的开销。对于冒泡排序而言,只要顺序相反,就需要对两个数的顺序进行调换,故中间产生了很多次不必要调换操作,从而大大延长了运行所需时间。

        选择排序和插入排序的效率相近,但是相比之下,插入排序更快。其主要原因可能为选择排序需要找到当前最大值才能进行排序,故每趟排序都需要与子序列中的全部元素进行比较;而插入排序无需比较子序列全部元素,只需要找到当前序列第一个比自己大或小的元素,将自身插入到其前一个位置即可。

        综合本次实验的分析,当所需排序的数据量较小时,可以选择快速排序和插入排序算法。但当数据量较大时,则应选择快速排序算法或者归并排序算法进行排序操作。其他算法适用于数据不太多的情况。

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

算法设计思路:

①冒泡排序(进行10次循环)

BubbleSort-top10(Nums[10^9]):

  1.   for i=1 to 10      
  2.       j=0 to (Nums.length(10^9)-i-1)  
  3.       //Compare the keywords of two adjacent records in turn
  4.       if  Nums[j] >  Nums[j+1]   
  5.                     swap Nums[j+1] and Nums[j]  

分析:

时间复杂度:O(n)

测试运行时间为:33639ms(数据体量:10^9)

小规模数据进行验证

假设有 20 个数据,选取其中最大的 5 个,对整个代码进行验证

②选择排序(进行10次循环)

SelectionSort-top10(Nums[10^9]):

  1.   for i=0 to 10  
  2.        max=Nums[0]  
  3.        index=0     
  4.        j=1 to (Nums.length(10^9)-i)  
  5.        //Current range, place the largest number at the end position  
  6.         if  Nums[j] > maxx   
  7.          maxx = nums[j];  
  8.              index = j;  

        swap Nums[index] and Nums[j]  

分析:

时间复杂度:O(n)

测试运行时间为:10682ms(数据体量:10^9)

小规模数据进行验证

假设有 20 个数据,选取其中最大的 5 个,对整个代码进行验证

③快速排序

Quicksort (Nums, begin, end )

  1. if (end>begin)
  2. mid = quickSort(Nums, begin, end)
  3. quickSort (Nums, begin, mid-1)//左子序列进行相同排序操作
  4. quicksort (Nums, mid+1, end)//右子序列进行相同排序操作

quickSort(Nums, begin, end):

  1.   target=num[begin]     
  2.   high=end
  3.   low=begin      
  4.  while  high > low :
  5.  whlie  high > low and num[high] >= target
  6.                   //找到第一个比目标值小的数的下标
  7.  High - 1
  8.  if high > low
  9.  num[low] =num [high]
  10.  whlie high > low and num[low] <= target
  11.                  //找到第一个比目标值小的数的下标
  12.  Low + 1
  13.  if (high > low)
  14.  num[high] =num [low]
  15. list[low] = target
  16. return low

分析:

时间复杂度:O(nlog2(n))

空间复杂度:每个数据占4个字节,故10亿数据就需要占用4G内存(快速排序需要将数据全部加载到内存中)

缺点:占用空间大,大量排序操作是无效的。

测试运行时间为:101ms(数据体量:10^6)

对比前两种算法,基础的快速排序并没有展现其面对大数据量的优越性,反而在10^9体量的数据测试中运行时间极长,更改数据体量为10^6后,得出测试运行时间为101ms,按照比例能推测出在10^9体量的数据测试中大概需要1.5*10^5ms。经过分析得知,快速排序使用调用函数的方法(递归),会频繁的使用客户栈,当数据输入规模较大时,容易造成爆栈。所以改进办法为:不使用递归,而是改用迭代的办法。

这种方法不可取。于是提出优化如下:

记录右子序列的个数m,若m>10,则只需要对该右子序列进行下一轮的快速排序操作,直到m<=10时。

若m=10,则此时该子序列就是10亿数据中最大的10个数据

若m<10,则对当前基准值左边序列进行下一轮的快速排序操作,直到该轮排序的基准值的右子序列的个数加m等于10。

Quicksort (Nums, begin, end )

  1. if (end>begin)
  2. mid = quickSort(Nums, begin, end)
  3.      m=end-mid
  4.      while m> 10
  5.          quicksort (Nums, mid+1, end)//右子序列进行相同排序操作
  6.      if  m =10
  7.           return //则此时该子序列(最后10元素)就是数据中最大的10个数据
  8.      else //m<10
  9.      quickSort (Nums, begin, mid-1)
  10.            //对当前基准值左边序列进行下一轮的快速排序操作,直到该轮排序的基准值的右子序列的个数加m等于10。

quickSort(Nums, begin, end):

  1.   target=num[begin]     
  2.   high=end
  3.   low=begin      
  4.  while  high > low :
  5.  whlie  high > low and num[high] >= target
  6.                   //找到第一个比目标值小的数的下标
  7.  High - 1
  8.  if high > low
  9.  num[low] =num [high]
  10.  whlie high > low and num[low] <= target
  11.                  //找到第一个比目标值小的数的下标
  12.  Low + 1
  13.  if (high > low)
  14.  num[high] =num [low]
  15. list[low] = target
  16. return low

③分治法

将数据分散成多份,通过多台机器分布式运算或者多线程并发计算的方式取得每份数据的Top 10,然后汇总结果。该算法可以解决快速排序等思路中对计算机内存要求较大的问题。

④小顶堆

 将十亿数据均匀分成以十万个数据为单位的数据组,将每个子数据组通过小顶堆的方法取出的前10个数组合成一个新的数组,再通过小顶堆计算出最大的10个数,即为十亿数据的TOP10结果。

优点:

该算法只需要遍历一次文件中的数字,不存在多次读写数据的问题。节省了大量空间。

三.实验心得

本次实验主要从时间复杂度分析了五种算法的效率,综合得出一般情况下:快速排序>合并排序>插入排序>选择排序>冒泡排序。当所需排序的数据量较小时,可以选择快速排序和插入排序算法。但当数据量较大时,则应选择快速排序进行排序操作。在实验过程中,我也了解和分析了各个算法的优势和缺点,明白了一些算法背后的联系。在面对不同的数据类型时,应该首先考虑它更适合哪种算法。

    对于一些特殊或者极端的数据,在排序前是否可以进行一些特殊操作,比如将序列全部逆转,值得考虑。

最值得注意的是,在操作排序时还应该考虑其稳定性,根据用户需求判断哪些算法是不适合的,在五种算法中,插入排序算法、冒泡排序算法、归并排序算法是稳定的。

另外,每种算法的代码实现时可以采取相应的优化操作,这些都是在设计排序时所应该考虑的。比如冒泡排序中的冗余比较操作:对于一组已经基本有序的数据,在经过几轮排序后,序列可能已经有序,后续对剩余的元素的比较则为无效操作。针对这一情况,可以在判断出某轮比较中没有进行相邻元素间的交换,则直接退出循环,排序完成。

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值