【深大算法设计与分析】实验一 排序算法性能分析 实验报告 附代码

一、实验目的与要求

  1. 掌握选择排序、冒泡排序、插入排序、合并排序、快速排序算法原理
  2. 掌握不同排序算法时间效率的经验分析方法,验证理论分析与经验分析的一致性。
  3. 求解TOP K问题,并分析比较不同算法效率。

      

二、实验内容与方法

        1. 编写代码,实现选择排序、冒泡排序、插入排序、合并排序和快速排序。

        2. 分别固定待排序数组的大小n=10万、n=20万、n=30万、n=40万、n=50万,在每一个固定的n下使用不同的数据集重复实验二十次,统计并比较各个排序算法的运行效率。

        3. 绘制理论运行效率曲线和实测运行效率曲线,如果两条曲线不同,分析原因。

        4. 思考如何快速挑选出十亿个数中最大的十个数,并在小规模数据上验证算法的准确性。

三、实验步骤与过程

        1. 解题思路简述:

        本次实验代码部分较为简单,主要可以分为两个部分:

        ①相关准备工作:如生成随机数的函数、判断排序是否正确完成的函数以及用于计时的函数

        ②编写五个排序函数,实现选择排序、冒泡排序、插入排序、合并排序和快速排序,实现的方法详见下个部分。

        2. 算法实现的核心伪代码:(数组名为a,长度为len)

        选择排序:

for i from 0 to len-1

       for j from i+1 to len

              find min a[j]//找到最小的a[j],记录这个j

       swap a[i],a[j]

        冒泡排序:

for i from 0 to len

       for j from 0 to i

              if a[j]<a[j-1]

              swap a[j],a[j-1]

        插入排序:

for i from 1 to len

       temp=a[i]

       j=i-1

       while a[j]>temp

              a[j]=a[j-1]

              j-=1

       a[j+1]=temp

        合并(归并)排序:

        函数定义:Mergesort(int a[],int begin,int end),其中参数分别为:待排序数组,起点索引,终点索引;Merge(int a[],int begin,int end),表示将beginmidmid+1end这两段已经排好序的数组进行合并

if begin >end return

mid = (begin+end)/2

Mergesort(a,begin,mid)

Mergesort(a,mid+1,end)

Merge(a,begin,end)

        快速排序:

函数定义:Quicksort(int a[],int begin,int end),各参数定义与Mergesort相同。

if begin >end return

key=a[begin]

p1=begin,p2=end

while p2 > p1

       while a[p2] >= key & p1 < p2)

              p2-=1    

              swap a[p1],a[p2]

       while a[p1] < key & p1 < p2

                     p1+=1    

       swap a[p1],a[p2]

Quicksort(a, begin, p1 - 1)

Quicksort(a, p1 + 1, end)

        3. 算法测试结果及效率分析:

        这五个算法分别在不同大小数据集下的运行效率测试结果如图所示:

图:算法的运行效率测试结果

        其中,时间的单位为毫秒,sample size表示数组的长度。

        绘制成图表如图所示:

图:各排序运行时间对比

        4. 在此基础上,进行理论效率与实际效率对比分析:

        以数据集大小10万为基准,计算各个排序的理论运行效率,结果如图:

图:算法的运行的理论效率

        对结果进行分析:选择排序和冒泡排序的理论运行效率与实际接近,但是插入排序、归并排序与快速排序,理论运行时间均比实际运行时间要长,猜测可能是由于在10万大小的数据集进行测试的时候,运行时间更容易受到一些常数项计算或者复杂度为O(n)的运算的影响。例如内存的访问、验证排序是否正确完场的函数、时间的记录等的影响,就可能导致10万大小的数据集的测试时间较长,而在更大数据集下,由于运算本身的时间变长,这些因素的影响会被削弱。理论与实际的对比(归并排序与快速排序)如图所示:

图:理论与实际运行效率的对比

        五个排序的代码:

#include <iostream>
#include <time.h>
#include <stdlib.h>
using namespace std;

const int N = 500010;//允许的最大数据量
int a[N], tmp[N];

void check(int a[], int len) {// 遍历一次数组,用于检查排序是否正确完成
	for (int i = 1; i < len; i++) {
		if (a[i] < a[i - 1]) {
			cout << "error" << endl;
			return;
		}
	}
	cout << "complete" << endl;
}

void SelectSort(int a[], int len) {// 选择排序

	// a[0]~a[i-1]已经完成(到达最终状态),循环负责找出后续数组中最小的数,放到a[i]处
	for (int i = 0; i < len - 1; i++) {
		int min = i;// 记录最小值编号
		for (int j = i + 1; j < len; j++) {
			if (a[j] < a[min]) min = j;// 更新最小值
		}
		swap(a[i], a[min]);// 将最小值放入a[i]
	}
	cout << "\nSelectSort" << ' ';
	check(a, len);
}

void BubbleSort(int a[], int len) {// 冒泡排序
	for (int i = 0; i < len; i++)
		for (int j = 1; j < len - i; j++)
			if (a[j] < a[j - 1]) swap(a[j], a[j - 1]);// 逆序,则交换		
	cout << "\nBubbleSort" << ' ';
	check(a, len);
}

void InsertSort(int a[], int len) {// 插入排序

	// a[0]~a[i-1]保持递增有序,循环目的是找到a[i]应当插入的位置,并插入
	for (int i = 1; i < len; i++) {
		int end = a[i];
		for (int j = 0; j < i; j++) {
			if (end < a[j]) {// 找到了a[i]应当插入的位置,开始插入
				for (int k = i; k > j; k--) {
					a[k] = a[k - 1];// 先腾出位置
				}
				a[j] = end;// 插入
			}
		}
	}
	cout << "\nInsertSort" << ' ';
	check(a, len);
}

void QuickSort(int a[], int begin, int end) {// 快速排序
	if (begin >= end)return;
	int key = a[begin];// 选取第一个数作为key
	int p1 = begin, p2 = end;

	// 循环:最终目的是使p1==p2,a[p1]==key,p1左边的数全部小于key,右边的数全部大于等于key
	while (p2 > p1) {
		while (a[p2] >= key && p1 < p2) {
			p2--;
		}
		swap(a[p1], a[p2]);
		while (a[p1] < key && p1 < p2) {
			p1++;
		}
		swap(a[p1], a[p2]);
	}
	QuickSort(a, begin, p1 - 1);
	QuickSort(a, p1 + 1, end);
}

void QuickSort(int a[], int len) {// 快速排序(调用接口)
	QuickSort(a, 0, len - 1);
	cout << "\nQuickSort" << ' ';
	check(a, len);
}

void MergeSort(int a[], int begin, int end) {// 归并排序
	if (begin >= end) return;

	// 划分数组,递归调用
	int mid = (begin + end) / 2;
	MergeSort(a, begin, mid);
	MergeSort(a, mid + 1, end);

	// 进行归并(先将归并后的数据存到临时数组,再转到a)
	int k = 0, i = begin, j = mid + 1;
	while (i <= mid && j <= end)
		if (a[i] <= a[j]) tmp[k++] = a[i++];
		else tmp[k++] = a[j++];
	while (i <= mid) tmp[k++] = a[i++];
	while (j <= end) tmp[k++] = a[j++];
	for (i = begin, j = 0; i <= end; i++, j++) a[i] = tmp[j];
}

void MergeSort(int a[], int len) {// 归并排序(调用接口)
	MergeSort(a, 0, len - 1);
	cout << "\nMergeSort" << ' ';
	check(a, len);
}

void init(int a[], int len, int seed) {// 初始化数组
	srand(seed);
	for (int i = 0; i < len; i++) {
		a[i] = rand();
	}
}

int main() {
	int max_len = 500000;// 最大数组长度,要求小于N
	int sample_quantity = 20;// 对于每一个数据长度,进行sample_quantity次重复实验

	for (int len = max_len / 5; len <= max_len; len += max_len / 5) {
		double Select_t = 0, Bubble_t = 0, Insert_t = 0, Quick_t = 0, Merge_t = 0;
		for (int seed = 0; seed <= sample_quantity; seed++) {
			cout << "Sample size:" << len << "  Sample number:" << seed << endl;

			double t0, t1;// 用于计时

			// 选择排序
			init(a, len, seed);
			t0 = clock();
			SelectSort(a, len);
			t1 = clock();
			cout << "Using time:" << t1 - t0 << endl;
			Select_t += (t1 - t0) / sample_quantity;

			// 冒泡排序
			init(a, len, seed);
			t0 = clock();
			BubbleSort(a, len);
			t1 = clock();
			cout << "Using time:" << t1 - t0 << endl;
			Bubble_t += (t1 - t0) / sample_quantity;

			// 插入排序
			init(a, len, seed);
			t0 = clock();
			InsertSort(a, len);
			t1 = clock();
			cout << "Using time:" << t1 - t0 << endl;
			Insert_t += (t1 - t0) / sample_quantity;

			// 快速排序
			init(a, len, seed);
			t0 = clock();
			QuickSort(a, len);
			t1 = clock();
			cout << "Using time:" << t1 - t0 << endl;
			Quick_t += (t1 - t0) / sample_quantity;

			// 归并排序
			init(a, len, seed);
			t0 = clock();
			MergeSort(a, len);
			t1 = clock();
			cout << "Using time:" << t1 - t0 << endl;
			Merge_t += (t1 - t0) / sample_quantity;

			cout << endl << endl;
		}
		cout << "Sample size:" << len << endl;
		cout << "average select sort time: " << Select_t << endl;
		cout << "average bubble sort time: " << Bubble_t << endl;
		cout << "average insert sort time: " << Insert_t << endl;
		cout << "average quick sort time: " << Quick_t << endl;
		cout << "average merge sort time: " << Merge_t << endl;
		cout << endl << endl << endl;
	}
}

       5. 求解TOP K问题:

       对于大数据集下的TOP K问题,使用快速排序是一个简单快捷的方法,但是除此之外,可以使用堆排序:

创建一个大小为k的大根堆,先将其填满(可以用假设的最小值,也可以用数组的前十个数)然后开始遍历数组,如果遇到了一个数比堆的根小,就将其放入根的位置,重新调整堆的结构,直到遍历完成。

       这样一来只需要一遍循环就可以完成对最大k个值的寻找,一般情况下,堆的根不会反复更改,所以对于堆的处理时间几乎可以忽略。即使是最差情况:遇到递增数组,每次更改大小为k的堆的结构也不是非常耗时,故这种方法可以很好地完成TOP K问题的求解。运行效果如图(rand函数生成的随机数最大为32767,所以此处最大的十个数全都为32767):

图:一千万数据集下的测试结果

        TOP K问题代码:

#include <iostream>
#include <time.h>
#include <queue>
using namespace std;

const int N = 1000010;//允许的最大数据量
int a[N];

void QuickSort(int a[], int begin, int end) {// 快速排序
	if (begin >= end)return;
	int key = a[begin];// 选取第一个数作为key
	int p1 = begin, p2 = end;

	// 循环:最终目的是使p1==p2,a[p1]==key,p1左边的数全部小于key,右边的数全部大于等于key
	while (p2 > p1) {
		while (a[p2] >= key && p1 < p2) {
			p2--;
		}
		swap(a[p1], a[p2]);
		while (a[p1] < key && p1 < p2) {
			p1++;
		}
		swap(a[p1], a[p2]);
	}
	QuickSort(a, begin, p1 - 1);
	QuickSort(a, p1 + 1, end);
}

void QuickSort(int a[], int len) {// 快速排序(调用接口)
	QuickSort(a, 0, len - 1);
}

void init(int a[], int len, int seed) {// 初始化数组
	srand(seed);
	for (int i = 0; i < len; i++) {
		a[i] = rand();
	}
}
void print(int a[], int len) {
	cout << "最大的十个数:";
	for (int i = len - 1; i > len - 11; i--) {
		cout << a[i] << ' ';
	}
	cout << endl;
}

int main() {
	int len = 1000000;

	init(a, len, 1);

	//快速排序方法
	cout << "快速排序方法:" << endl;
	int t0 = clock();
	QuickSort(a, len);
	int t1 = clock();
	print(a, len);
	cout << "耗时:" << t1 - t0 << endl;

	init(a, len, 1);

	//堆排序方法
	cout << endl << "堆排序方法:" << endl;
	priority_queue <int, vector<int>, less<int> > q;
	t0 = clock();
	int num = 0;
	for (int i = 0; i < len - 1; i++) {
		if (num < 10) {
			q.push(a[i]);
			continue;
		}
		if (a[i] > q.top()) {
			q.pop();
			q.push(a[i]);
		}
	}
	t1 = clock();
	cout << "最大的十个数:";
	for (int i = 0; i < 10; i++) {
		cout << q.top() << ' ';
		q.pop();
	}
	cout << endl;
	cout << "耗时:" << t1 - t0 << endl;
}

四、实验结论与体会

       实验结论:本次实验通过使用c++代码进行编程,实现了选择排序、冒泡排序、插入排序、合并排序和快速排序,并分别在待排序数组的大小n=10万、n=20万、n=30万、n=40万、n=50万的情况下对排序函数进行了测试、统计并比较了各个算法在不同数据集下的实际运行时间与理论运行时间。除此之外,通过快速排序方法和堆排序方法解决了TOP K问题,并得出了“堆排序可以以较高的效率解决TOP K问题”的结论。本次实验圆满完成了实验任务。

        实验体会:本次实验总体上较为简单。对于排序算法,要多注意可能存在的边界问题,例如:归并排序中mid应当取(begin+end)/2还是(begin+end-1)/2的问题、快速排序中key的位置问题等。实际实验中出现了算法框架已经编写正确,但是由于这些边界问题,导致排序完全错误的情况,因此,应当在编写边界变量是,多加思考,避免错误。

尾注:

本实验是此课程的第一次实验,质量一般,尚有许多不完善不成熟之处,仅供参考。

如有疑问欢迎讨论,如有好的建议与意见欢迎提出,如有发现错误则恳请指正!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值