渐进法分析冒泡/选择排序法时间复杂度

渐进分析

渐进分析是一种数学方法,渐进分析技术能够在数量级上对算法进行精确度量。但是,数学不是万能的,实际上,许多貌似简单的算法很难用数学的精确性和严格性来分析,尤其分析平均情况。算法的实验分析是一种事后计算的方法,通常需要将算法转换为对应的程序并上机运行。
计数法是在算法中的适当位置插入一些计数器,来度量算法中基本语句的执行次数。生成合适的测试样例作为测试的基准,并对输入实例运行算法对应的程序,记录得到的实验数据。最后根据实验得到的数据,结合实验目的,对算法结果进行分析。

设计思路

实验首先需要生成合适的测试样例,为了尽可能追求实验结果的一般性,将生成 3 组不同规模的实验数据。每组数据在规模不同的基础上,需要包含 3 种不同特点的数据。由于做的是排序算法时间复杂度的分析,因此 3 种不同情况分别为最好(正序)、最差(倒叙)和随机情况。
接着需要编写运行不同排序算法的程序,由于数据集的规模不同,使用纯 C 语言的动态内存分配并不能很好地适应不同数据集。因此此处选择使用 STL 库中的 vector 容器来自动管理内存,依次适应不同规模的测试数据。通过添加计数变量来记录基本语句的执行次数,在每个数据集运行完毕后输出,进行实验数据统计。

数据生成

数据生成脚本

由于实验中的实验数据打算从文件中读取,因此生成数据时需要把数据保存在文件中。选择使用 Python 脚本生成实验所需数据集:

import random

filename = 'XXX.txt'
with open(filename, 'w') as file_object:
    for i in range(10000):
        file_object.write(str(random.randint(-10000,10000)) + '\n')
        #file_object.write(str(i) + '\n')
        #file_object.write(str(10000 - i) + '\n')

print("成功生成数据集" + filename)

数据集概况

数据集序号 数据集数据量(个) 数据集特点
1 100 正序自然数等差数列
2 100 (-10000,10000)随机数
3 100 (-10000,10000)随机数
4 100 逆序自然数等差数列
5 1000 正序自然数等差数列
6 1000 (-10000,10000)随机数
7 1000 (-10000,10000)随机数
8 1000 逆序自然数等差数列
9 10000 正序自然数等差数列
10 10000 (-10000,10000)随机数
11 10000 (-10000,10000)随机数
12 10000 逆序自然数等差数列

算法程序

主函数

首先编写试验所需的程序框架,即主函数。设计输入数据集名时,程序接受文件名,然后把文件名交付给 file_Read()文件读取函数进行读取。使用 C++ STL 库的 vector 容器进行存储数据,因此 file_Read()函数的返回值应该是存储文件中所有数据的 vector 容器。由于实验实现2种排序算法,因此程序需要实现2种算法对应的函数。主函数调用排序算法进行排序并回显基本语句数量,进行试验数据记录。最后使用迭代器遍历排序完毕的 vector容器,输出排序结果检验排序是否正确。

int main()
{
    vector<int> dataset;
    vector<int>::iterator it;
    char file_name[10];
    
    cin >> file_name;
    dataset = file_Read(file_name);
    dataset = BubbleSort(dataset);
    //dataset = SelectSort(dataset);
    /*for(it = dataset.begin(); it!= dataset.end(); it++)
    {
        cout << *it << endl;
    }*/
    
    return 0;
}

排序函数

选取冒泡排序法和选择排序法进行分析,分别按照 2 种算法的实现方式编写函数,注意要在基本语句——比较和交换语句处设置计数器。当算法执行完毕时输出基本语句的执行次数,进行记录。

vector<int> BubbleSort(vector<int> dataset)    //冒泡排序 
{
       int temp;
       int compare_count = 0;
       int exchange_count = 0;
       int exchange = dataset.size() - 1;
       int bound;
       
       while(exchange != 0)
       {
       	     bound = exchange;
             exchange = 0;
             for(int i = 0; i < bound; i++)
             {
                   compare_count++;
                   if(dataset[i] > dataset[i + 1])
                   {
                         exchange = i;
                         temp = dataset[i];
                         dataset[i] = dataset[i + 1];
                         dataset[i + 1] = temp;
                         exchange_count += 3;
		   }
	     }
      }
		
      cout << "比较次数为:" << compare_count << endl; 
      cout << "交换次数为:" << exchange_count << endl; 
      
      return dataset;
}

vector<int> SelectSort(vector<int> dataset)    //选择排序 
{
	int idx;
	int temp;
	int compare_count = 0;
	int exchange_count = 0;
	
	for (int i = 0; i < dataset.size(); i++)
	{
		idx = i;
		for (int j = i + 1; j < dataset.size(); j++)
		{
			compare_count++;
			if (dataset[idx] < dataset[j])
			{
				idx = j;
			}
		}
		temp = dataset[i];
		dataset[i] = dataset[idx];
		dataset[idx] = temp;
		exchange_count += 3;
	}
	
	cout << "比较次数为:" << compare_count << endl; 
	cout << "交换次数为:" << exchange_count << endl; 
	return dataset;
}

记录实验数据

依次输入 12 个数据集,分别运行冒泡排序法和选择排序法,所获取的实验数据如下:

数据集序号 冒泡排序比较次数 冒泡排序交换次数 选择排序比较次数 选择排序交换次数
1 99 0 4950 300
2 4895 7257 4950 300
3 4745 7668 4950 300
4 4950 14850 4950 300
5 999 0 499500 3000
6 495821 771342 499500 3000
7 496056 756987 499500 3000
8 499500 1498500 499500 3000
9 9999 0 49995000 30000
10 49931801 75580422 49995000 30000
11 49925382 74716506 49995000 30000
12 49995000 149985000 49995000 30000

实验数据分析

首先对比较次数进行分析,3种规模的数据条形图如下。可以明显地看到,当数据已经基本有序时,冒泡排序算法能够在很低的次数就完成排序。当数据完全失序或者处于较为随机的状态时,冒泡排序算法的比较次数略小于选择排序,但是差别并不大。这个趋势会随着数据的规模增大而变得更加明显。



接下来分析交换次数,3 种规模的数据条形图如下。可以明显地看出虽然在基本有序的情况下,冒泡排序的交换次数为 0。但是在其他情况下冒泡排序算法的交换次数远大于选择排序,尤其是在完全失序的情况下,冒泡排序算法的交换次数甚至是随机情况下的 2 倍。而选择排序的交换次数是固定的,是数据集数据量的 3 倍。



最后分析算法基本语句的总执行次数,3 种规模的数据条形图如下。在数据基本有序的情况下,冒泡排序的基本语句执行次数远小于选择排序。但是在其他情况,冒泡排序的基本语句执行次数会是选择排序的 2 倍以上,当完全失序时甚至能达到 3 倍以上。


时间复杂度

首先我们看冒泡排序。在最好情况下,也就是初始序列是一趟排序时,只需要进行一趟排序。排序过程中进行 n-1 次关键字比较,且不移动记录。在初始序列为逆序的最坏情况下,需要进行 n-1 趟排序,总的比较次数 num1 为:

总的移动次数 num2 为:

所以在平均情况下,冒泡排序的关键字比较次数和记录移动次数分别约为 n^2/4 和 3n^2/4,时间复杂度为 O(n^2) 。从上文的统计数据来看,冒泡排序的基本语句执行次数远大于n的一次方阶,远小于n的三次方阶,与平方阶的数量级更为接近。其中最坏情况时间复杂度为 O(n^2),最好情况时间复杂度为
O(1)。
接下来看看选择排序,选择排序所需要进行移动的次数较少。最好情况,也就是数据集时正序序列时不需要移动。在逆序的最坏情况下,算法需要移动 3(n-1)次。无论记录的初始排列如何,所需进行的关键字间比较次数相同,num 值都是:

因此选择排序算法的时间复杂度也是 O(n^2)。从上文的统计数据来看,选择排序的基本语句执行次数远大于 n 的一次方阶,远小于 n 的三次方阶,与平方阶的数量级更为接近。其中最坏情况和最好情况的时间复杂度都是 O(n^2)。

参考资料

《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社
《算法设计与分析(第二版)》——王红梅,胡明 编著,清华大学出版社
算法:排序
c++输入文件流ifstream用法详解
C++ stringstream介绍,使用方法与例子

posted @ 2020-10-06 16:35  乌漆WhiteMoon  阅读(1125)  评论(0编辑  收藏  举报