【数据结构】八大排序代码实现(C++)

1.直接插入排序

排序思想:在一个有序数组中插入一个新的值,使该数组依然有序。

咱们平时玩扑克牌,咱们摸牌的时候就是插入排序的思想。

代码:

void InsertSort(vector<int>& arr) {
	int n = arr.size();
	for (int i = 0; i < n - 1; i++) {
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0) {
			if (tmp < arr[end]) {
				arr[end + 1] = arr[end];
				end--;
			}
			else break;
		}
		arr[end + 1] = tmp;
	}
}

1.1复杂度分析:

时间复杂度:O(n^2)。

不要觉得时间复杂度是n^2就瞧不起插入排序,其实它要比冒泡排序优秀得多。

上面是咱们实现升序得代码,它仅仅是在排降序的数组时,复杂度是n^2。其他情况都要优于n^2。

当排一个升序数组时,时间复杂度降为O(n)。

所以,当插入排序排一个十分接近有序的数组(只有个别数据是无序的,比如调换位置),时间复杂度趋于O(n),此时优于n*logn的排序算法!

1.2稳定性分析:

稳定性分析:

分析代码逻辑,我们发现:在排序前和排序后,数值相等的数据不会改变它们之间的前后顺序,所以插入排序是稳定的!

2.希尔排序

排序思想:在插入排序时说过,在插入排序排一个接近有序的数组时效率是非常高的,要优于n*logn的算法。那如何将一个乱序的数组变得接近有序呢?我们需要将该数组的数据等分成gap组,gap代表同组数据的下标之差。分好组后,用插入排序对每组数据排一遍序,这样数组就接近有序了(这个过程称为预排序),最后对整个数组再来一遍插入排序就OK了!这个排序过程就是希尔排序!

代码:

void ShellSort(vector<int>& arr) {
	int n = arr.size();
	int gap = n;
	while (gap > 1) {
		gap = gap / 3 + 1;//保证最后一次gap=1;
		for (int i = 0; i < n - gap; i++) {
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0) {
				if (tmp < arr[end]) {
					arr[end + gap] = arr[end];
					end-=gap;
				}
				else break;
			}
			arr[end + gap] = tmp;
		}
	}
}

至于gap如何取值,官方给出了比较好的方式就是上述代码采用的方式,但不一定是最好的,最优的方式一定是随着数据量的变化而变化的。

因为gap越大,代表着预排序结束,数组越无序。gap越小,比如gap接近1,就直接接近插入排序了,那么优化的效果就越低。

2.1复杂度分析

时间复杂度:希尔排序的时间复杂度难以计算。比较权威的教材上给出:当n在特定范围内时间复杂度约为O(n^1.3),当n->无穷时,时间复杂度可以降低到n*(logn)^2。也就是说它的效率要比n*logn的排序算法稍低一些。

2.2稳定性分析

在希尔排序的预排序过程中,数值相等的数据可能会改变它们之间的前后顺序,所以希尔排序是不稳定的!

2.3希尔排序&插入排序性能测试

测试代码:

void TestOP() {
	srand(time(nullptr));
	vector<int> arr1(100000);
	vector<int> arr2(100000);
	vector<int> arr3(100000);
	int n = 100000;
	for (int i = 0; i < n; i++) {
		int j = rand();
		arr1[i] = j;
		arr2[i] = j;
		arr3[i] = j;
	}

	int begin1 = clock();
	//BubbleSort(arr1);
	int end1 = clock();

	int begin2 = clock();
	InsertSort(arr2);
	int end2 = clock();

	int begin3 = clock();
	ShellSort(arr3);
	int end3 = clock();

	cout << "ShellSort:" << (end3 - begin3) << endl;
	cout << "InsertSort:" << (end2 - begin2) << endl;
	//cout << "BubbleSort:" << (end1 - begin1) << endl;
}
int main() {
	TestOP();
	return 0;
}

在release版本下:十万个随机数,希尔排序耗时:7毫秒。插入排序耗时:885毫秒。

虽然希尔排序的思路比较麻烦,但它的效率是非常高的!它和插入排序,冒泡排序已经不是一个量级的算法了,它已经可以和n*logn的算法一较高下了!

3.选择排序

排序思路:假如要升序排列。先遍历数组,找到最小的数,和下标为0的数交换,再次遍历数组,找到次小的数,和下标为1的数交换。依次类推。。。

代码:

void SelectSort(vector<int>& arr) {
	int n = arr.size();
	int begin = 0, end = n - 1;
	while (begin < end) {
		int min = begin, max = begin;
		for (int i = begin + 1; i <= end; i++) {
			if (arr[i] < arr[min]) min = i;
			if (arr[i] > arr[max]) max = i;
		}
		swap(arr[begin], arr[min]);
		if (max == begin) max = min;
		swap(arr[end], arr[max]);
		begin++;
		end--;
	}
}

3.1复杂度分析

时间复杂度:O(n^2),且不论排有序还是无序的数组,永远都是O(n^2)。

3.2稳定性分析

分析代码逻辑,我们发现:在排序前和排序后,数值相等的数据不会改变它们之间的前后顺序,所以选择排序是稳定的!

3.3选择排序&冒泡排序性能对比

release版本下:十万个随机数,选择排序耗时:4140毫秒。冒泡排序耗时:11673毫秒。

选择排序效率大概是冒泡排序的三倍。

看来这两中排序算法的效率差距不大,属于同一个量级的算法。

当然在实践中,这两种排序没什么应用场景。较多应用于面试和教学。

4.堆排序

首先需要搞清楚什么是堆,堆的分类有哪些,如何建堆。我之前有一篇文章是专门介绍堆的,大家可以去看一看。

堆分为大堆和小堆。大堆的根结点最大,且每个父节点均大于子节点。小堆的根结点最小,且每个结点均小于子结点。

排序思想:如果要升序排列,则需要建大堆。让根结点与最后一个子结点交换,那么最大的数就排好了,再让交换后的堆向下调整,此时根结点就是次大的数,和倒数第二个子节点交换,那么次小的数就排好了,依此类推。。。

有的同学想问:升序排列可以建小堆嘛?

答:理论上讲是可以的!但这样做排序效率会变得极慢,没有实际运用价值!

因为建小堆的话,数组第一个数就是最小的,而我要升序排列,那么第一个数直接就排好了,不需要调整。我们必须从第二个数开始调整堆,但此时数据之间的关系全都乱套了,无法向上或向下调整恢复堆的特性,只能重新建堆,而建堆的代价是很大的,时间复杂度是O(n*logn)。这样每选出来一个数就要重新建一次堆。等排完整个数组,时间复杂度就达到了O((logn)*n^2)。这比冒泡排序还要慢!

4.1如何建堆

建堆一共有两种方式:向上调整建堆和向下调整建堆。

向上调整建堆的时间复杂度约为:O(n*logn)。

向下调整建堆的时间复杂度约为:O(n)。

向上调整建堆:

代码:

void AdjustUp(vector<int>& arr,int child) {
	int parent = (child - 1) / 2;
	while (child > 0) {
		if (arr[child] > arr[parent]) swap(arr[child], arr[parent]);
		else break;
		child = parent;
		parent = (parent - 1) / 2;
	}
}

向上调整时,从第二层开始往下的层数都是需要调整的,层数越大,数据越多,调整的次数也越多。

向下调整建堆:

代码:

void AdjustDown(vector<int>& arr,int parent,int size) {
	int child = parent * 2 + 1;
	while (child < size) {
		if (child + 1 < size && arr[child + 1] > arr[child]) child++;
		if (arr[parent] < arr[child])	swap(arr[parent], arr[child]);
		else break;
		parent = child;
		child = child * 2 + 1;
	}
}

最后一层不需要调整,从倒数第二次开始往上,都需要调整。且越靠近最后一层,数据越多,调整的次数越少。越往上,数据越少,调整的次数越多。

通多对比两种建堆方式,我们发现向上调整只舍掉了第一层的数据,且只有一个。而向下调整舍掉的是最后一层的数据,如果是完全二次数的话就舍掉了整棵树差不多一半的数据。所以粗略的分析得出:向下调整优于向上调整。

我们也可以通过数学计算得出,每一项是一个等差乘等比,要用到错位相减法。

结论:向上调整时间复杂度:O(n*logn)。向下调整时间复杂度:O(n)。

所以堆排序采用向下调整建堆。

4.2堆排序代码


void AdjustDown(vector<int>& arr,int parent,int size) {
	int child = parent * 2 + 1;
	while (child < size) {
		if (child + 1 < size && arr[child + 1] > arr[child]) child++;
		if (arr[parent] < arr[child])	swap(arr[parent], arr[child]);
		else break;
		parent = child;
		child = child * 2 + 1; 
	}
}

void HeapSort(vector<int>& arr) {
	int n = arr.size();
	for (int i = (n - 2) / 2; i >= 0; i--) {//向下调整建堆
		AdjustDown(arr,i,n);
	}
	int end = n - 1;
	while (end>0) {
		swap(arr[0], arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}

4.3时间复杂度分析

建堆:O(n),排序:O(n*logn)。总体时间复杂度为:O(n*logn)。

4.4稳定性分析

分析代码逻辑,我们发现:在排序前和排序后,数值相等的数据会改变它们之间的前后顺序,所以堆排序是不稳定的!

4.5向上调整建堆与向下调整建堆的方式进行堆排性能对比

如图:X86平台,release 版本下,一千万个随机数,采用向下调整的方式耗时697毫秒,采用向上调整的方式耗时786毫秒。差距虽然不太大,但还是有的。所以我们以后都用向下调整的方式进行堆排。
 

4.6堆排与希尔排序性能对比

十万个随机数,堆排比希尔排序快3毫秒。

一百万个随机数,堆排比希尔排序慢8毫秒。

我们前面分析的结果:堆排的性能应该比希尔排序略高一些,为什么这里堆排要慢一点呢?

那是因为rand()只能产生三万多个随机数,而我们却用它生成了一百万个数据,那么肯定有很多很多重复的数据。有很多重复的数据,数组就比较接近有序,这样希尔排序中预排序和最后一步的插入排序效率就更高。

而对于堆排序而言,特别是建堆这个过程,向下调整建堆最起码要循环n/2次,更何况还要涉及到父子结点的交换。所以较多的重复数据对堆排效率的提升并不明显!

还是一百万个数据,我们用rand()+i的方式减少重复数据,这时堆排就比希尔排序快不少。

5.冒泡排序

排序思想:先将最大的数冒到数组的最后,再将次大的数冒到数组的倒数第二个位置,依次类推。

代码:

void BubbleSort(vector<int>& arr) {
	int n=arr.size();
	for (int i = 0; i < n; i++) {
		bool exchange = false;
		for (int j = 0; j < n - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				swap(arr[j], arr[j + 1]);
				exchange = true;
			}
		}
		if (exchange) break;
	}
}

5.1复杂度分析

这是经过优化后的冒泡排序。

时间复杂度:O(n^2)。

在排升序数组时,时间复杂度度降为O(n)。

5.2稳定性分析

分析代码逻辑,我们发现:在排序前和排序后,数值相等的数据不会改变它们之间的前后顺序,所以冒泡排序是稳定的!

5.3冒泡排序与插入排序性能对比

单从时间复杂度上看,这两个排序算法好像差不多。但时间复杂度只是一个大致的衡量,忽略了很多细节,下面我们通过代码来细致分析两者的优劣:

测试代码:

void TestOP() {
	srand(time(nullptr));
	vector<int> arr1(100000);
	vector<int> arr2(100000);
	int n = 100000;
	for (int i = 0; i < n; i++) {
		int j = rand();
		arr1[i] = j;
		arr2[i] = j;
	}

	int begin1 = clock();
	BubbleSort(arr1);
	int end1 = clock();

	int begin2 = clock();
	InsertSort(arr2);
	int end2 = clock();

	cout << "InsertSort:" << (end2 - begin2) << endl;
	cout << "BubbleSort:" << (end1 - begin1) << endl;
}
int main() {
	TestOP();
	return 0;
}

release版本下:十万个随机数,插入排序用时:1184毫秒。冒泡排序用时:11215毫秒。

孰优孰劣想必在大家心中已经有答案了。

6.快速排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

崽崽..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值