渐进法分析冒泡/选择排序法时间复杂度
渐进分析#
渐进分析是一种数学方法,渐进分析技术能够在数量级上对算法进行精确度量。但是,数学不是万能的,实际上,许多貌似简单的算法很难用数学的精确性和严格性来分析,尤其分析平均情况。算法的实验分析是一种事后计算的方法,通常需要将算法转换为对应的程序并上机运行。
计数法是在算法中的适当位置插入一些计数器,来度量算法中基本语句的执行次数。生成合适的测试样例作为测试的基准,并对输入实例运行算法对应的程序,记录得到的实验数据。最后根据实验得到的数据,结合实验目的,对算法结果进行分析。
设计思路#
实验首先需要生成合适的测试样例,为了尽可能追求实验结果的一般性,将生成 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介绍,使用方法与例子
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)