十大常用排序算法总结(C++)


写在前面

博主根据十大必学经典排序算法这篇博文进行了学习,并把自己遇到的困难和所写的代码进行记录,本文按照如下顺序进行讲解(其中我把冒泡放在了插入排序之前)

00复杂度计算

参考算法分析神器—时间复杂度

01选择排序

思想

选择排序就是不断地从未排序的元素中选择最大(或最小)的元素放入已排好序的元素集合中,直到未排序中仅剩一个元素为止

如何选出最小元素

先随便选一个元素假设它为最小的元素(默认为无序区间第一个元素),然后让这个元素与无序区间中的每一个元素进行比较,如果遇到比自己小的元素,那更新最小值下标,直到把无序区间遍历完,那最后的最小值就是这个无序区间的最小值

代码
#include<iostream>
using namespace std;
void selectSort(int arr[], int length);
int main()
{
	int arr[8] = { 6,5,3,1,8,7,2,4 };
	selectSort(arr, 8);
	for (int i = 0; i < 8; i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
void selectSort(int arr[],int length)
{
	//选择length-1次
	for (int i = 0; i < length-1; i++)
	{
		int minIndex = i;//设i为最小元素的下标
		//在除前i个元素之外的外围内选择一个最小的元素
		for (int j = i+1; j <length; j++)
		{
			if (arr[minIndex]>arr[j])
			{
				//记录最小元素的下标
				minIndex = j;

			}
		}
		//交换
		int temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
}
时间复杂度

O(n^2)

稳定性

由于选择元素之后会发生交换操作,所以有可能把前面的元素交换到后面,所以不是稳定的排序

参考资料

参考优质文章

02冒泡排序

思想

从第一个数开始,让它和右边相邻的数进行比较,如果左边的数大于右边的石子,那么就交换两个数的位置,(也可以左小于右交换,这里采用大于交换),这样每比较一次,大的就跑到右边,直到跑到最右边。

代码
#include<iostream>
using namespace std;
void bubbleSort(int arr[], int len);
int main() {
	int arr[8] = { 6,5,3,1,8,7,2,4 };
	bubbleSort(arr, 8);
	for (int i = 0; i < 8; i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
void bubbleSort(int arr[], int len) {
	//比较的趟数
	for (int i = 0; i < len-1; i++)
	{
		//第i趟比较的次数
		for (int j = 0; j < len-i-1; j++)
		{
			//交换
			if (arr[j]>arr[j+1])
			{
				int temp = arr[j+1];//存储第j+1个元素
				arr[j+1] = arr[j];//将j个元素赋值给第j+1个元素
				arr[j] = temp;//将第j+1个元素赋值给第j个元素
			}
		}
	}
}
时间复杂度

O(n^2)

稳定性

所谓稳定性,其实就是说,当你原来待排的元素中间有相同的元素,在没有排序之前它们之间有先后顺序,在排完后它们之间的先后顺序不变,我们就称这个算法是稳定的

冒泡排序就是一个稳定的排序了,因为在交换的时候,如果两个数相同,那么就不交换[if (arr[j] > arr[j+1]){ 交换}],相同元素不会因为算法中哪条语句而相互交换位置的。

参考资料

参考优质文章

03插入排序

思想

所谓直接插入排序,就是把未排序的元素一个一个地插入到有序的集合中,插入时就像你那样,把有序集合从后向前扫一遍,找到合适的位置插入

适用场景
  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
  • 小规模数据或者基本有序时效率高
代码
#include<iostream>
#include<stdio.h>
using namespace std;
void insert_sort(int arr[], int length);
int main()
{
	int arr[8] = { 6,5,3,1,8,7,2,4 };
	insert_sort(arr, 8);
	for (int i = 0; i <8; i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
void insert_sort(int arr[], int length)
{
	int i,j;//j为要插入的位置
	for (i = 1; i < length; i++)
	{
		int temp = arr[i];//保留当前元素
		//寻找要插入的位置
		for (j = i; j > 0 && arr[j - 1] > temp; j--)
		{
			arr[j] = arr[j - 1];
		}
		//插入到找到的位置
		arr[j] = temp;
	}
}
时间复杂度

时间复杂度O(n^2)

稳定性

插入排序是稳定的排序算法,因为在比较的时候,如果两个数相等的话,不会进行移动,前后两个数的次序不会发生改变

参考资料

参考优质文章

04希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

思想

首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高

适用场景

希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
因此插入排序适用于数据规模较大并且无序的场景

代码
#include<iostream>
using namespace std;
void ShellSort(int arr[], int len);
int main() {
	int arr[8] = { 6,5,3,1,8,7,2,4 };
	ShellSort(arr, 8);
	for (int i = 0; i < 8; i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
void ShellSort(int arr[], int len)
{
	//设置增量
	for (int gap = len/2; gap>0 ; gap/=2)
	{
		//对每一个根据增量获得的分组进行插入排序
		int i, j;
		for (i = gap; i <len; i++)
		{
			int temp = arr[i];
			for (j = i; j>0 && arr[j-gap]>temp; j-=gap)
			{
				arr[j] = arr[j - gap];
			}
			arr[j] = temp;
		}
	}
}

希尔排序只是在外层多加了一个控制变量gap的循环罢了

时间复杂度

希尔排序的复杂度和增量序列是相关的
{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n^2)
Hibbard提出了另一个增量序列{1,3,7,…,2^k-1},这种序列的时间复杂度(最坏情形)为 O(n^1.5)
Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,…}

稳定性

不是稳定的,虽然插入排序是稳定的,但是希尔排序在插入的时候是跳跃性插入的,有可能徘徊稳定性

参考资料

具体参考优质文章

05归并排序

归并排序2020.8.18更新
代码
//归并排序
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
	vector<int> sortArray(vector<int>& nums) {
		vector<int> temp(nums.size());
		merge_sort(nums, temp, 0, nums.size()-1);
		return nums;
	}
	void merge_sort(vector<int>& arr, vector<int>& temp, int left, int right) {
		//left==right的时候,就递归到只有一个元素-->终止条件
		if (left < right) {
			//分:将数组一分为二
			int center = left + (right - left) / 2;
			//治:将左边的数组排序,left->center
			merge_sort(arr, temp, left, center);
			//治:将右边的数组排序,center+1->right
			merge_sort(arr, temp, center + 1, right);
			//合:合并两个有序数组
			merge(arr, temp, left, center, right);
		}
	}
	void merge(vector<int>& arr, vector<int>& temp, int left, int center, int right) {
		int i = left, j = center + 1;
		//先通过比较将两个有序数组合并为一个有序数组,结果暂时放到temp数组中
		for (int k = left; k <= right; k++) {//从小到大排序
			//如果左边数组arr[left...center]中的元素取完[即比较完](i>center),
			//则直接copy右边数组的元素到辅助数组,右边数组同理
			if (i > center) { temp[k] = arr[j++]; }
			else if (j > right) { temp[k] = arr[i++]; }
			else if (arr[i] < arr[j]) { temp[k] = arr[i++]; }
			else { temp[k] = arr[j++]; }
		}
		//再将已经排好序的辅助数组中的值复制到原数组arr中
		for (int k = left; k <= right; k++) {
			arr[k] = temp[k];
		}
	}
};
class Solution {
public:
	vector<int> sortArray(vector<int>& nums) {
		vector<int> temp(nums.size());
		merge_sort(nums, temp, 0, nums.size()-1);
		return nums;
	}
	void merge_sort(vector<int>& arr, vector<int>& temp, int left, int right) {
		//left==right的时候,就递归到只有一个元素-->终止条件
		if (left < right) {
			//分:将数组一分为二
			int center = left + (right - left) / 2;
			//治:将左边的数组排序,left->center
			merge_sort(arr, temp, left, center);
			//治:将右边的数组排序,center+1->right
			merge_sort(arr, temp, center + 1, right);
			//合:合并两个有序数组
			int i = left, j = center + 1;
				//先通过比较将两个有序数组合并为一个有序数组,结果暂时放到temp数组中
			for (int k = left; k <= right; k++) {//从小到大排序
				//如果左边数组arr[left...center]中的元素取完[即比较完](i>center),
				//则直接copy右边数组的元素到辅助数组,右边数组同理
				if (i > center) { temp[k] = arr[j++]; }
				else if (j > right) { temp[k] = arr[i++]; }
				else if (arr[i] < arr[j]) { temp[k] = arr[i++]; }
				else { temp[k] = arr[j++]; }
			}
				//再将已经排好序的辅助数组中的值复制到原数组arr中
			for (int k = left; k <= right; k++) {
				arr[k] = temp[k];
			}
		}
	}
};
思想

所谓归并排序,就是将待排序的数分成两半后排好序,然后再将两个
排好序的序列合并成一个有序序列

适用场景

归并排序是建立在归并操作的一种高效的排序方法,该方法采用了分治的思想,比较适用于处理较大规模的数据,但比较耗内存

#include<iostream>
using namespace std;
int temp[8];
void MergeSort(int arr[], int temp[], int left, int right);
void Merge(int arr[], int temp[], int left, int center, int right);
int main()
{
	int arr[8] = { 6,5,3,1,8,7,2,4 };
	MergeSort(arr, temp,0,7);
	for (int i = 0; i < 8; i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
void MergeSort(int arr[],int temp[],int left,int right) {
	//递归边界
	if (left>=right)
	{
		return;
	}
	int center = (left + right) / 2;
	//分治
	//分治左边
	MergeSort(arr, temp, left, center);
	//分治右边
	MergeSort(arr, temp, center + 1, right);
	//合并
	Merge(arr, temp, left, center, right);
}
void Merge(int arr[],int temp[],int left,int center,int right)
{
	int i=left, j=center+1;
	//合并
	for (int k=left; k<=right; k++)
	{
		//如果center左侧取完,则直接copy右边数组到辅助数组
		if (i>center)
		{
			temp[k] = arr[j++];//自己写错了
		}
		//如果center右侧取完,则直接copy左边数组到辅助数组
		else if (j > right)
		{
			temp[k] = arr[i++];//自己写错了
		}
		//如果右侧的元素大于左侧的元素,则将左侧元素赋值到temp中
		else if (arr[i] < arr[j]) {
			temp[k] = arr[i++];
		}
		//如果右侧的元素小于左侧的元素,则将右侧元素赋值到temp中
		else
		{
			temp[k] = arr[j++];
		}
	}
	//再将临时数组中的元素赋值到arr数组中
	for (int i = left; i <= right; i++)
	{
		arr[i] = temp[i];
	}

}
时间复杂度

复杂度为O(NlogN)计算,详细计算见参考文章

稳定性

是稳定的,因为在合并的时候,如果相等,选择前面的元素到辅助数组

参考文章

参考优质文章

归并排序的非递归版本

博主待学习

06快速排序

思想

快速排序也是和归并排序差不多,基于分治的思想以及采取递归的方式来处理子问题。例如对于一个待排序的源数组arr = { 4,1,3,2,7,6,8}。
我们可以随便选一个元素,假如我们选数组的第一个元素吧,我们把这个元素称之为”主元“吧。
然后将大于或等于主元的元素放在右边,把小于或等于主元的元素放在左边。
通过这种规则的调整之后,左边的元素都小于或等于主元,右边的元素都大于或等于主元,很显然,此时主元所处的位置,是一个有序的位置,即主元已经处于排好序的位置了。
主元把数组分成了两半部分。把一个大的数组通过主元分割成两小部分的这个操作,我们也称之为分割操作(partition)
接下来,我们通过递归的方式,对左右两部分采取同样的方式,每次选取一个主元 元素,使他处于有序的位置。
那什么时候递归结束呢?当然是递归到子数组只有一个元素或者0个元素了

使用场景

不需要像归并排序那样,还需要一个临时的数组来辅助排序,这可以节省掉一些空间的消耗,而且他不像归并排序那样,把两部分有序子数组汇总到临时数组之后,还得在复制回源数组,因此适用于对空间消耗有要求的排序

分割操作
分割操作:单向调整
代码——单向调整
#include<iostream>
using namespace std;
void QuickSort(int a[], int left, int right);
int partion(int a[], int left, int right);
int main()
{
	int a[7] = { 8,1,3,2,7,6,4 };
	QuickSort(a, 0, 6);
	for (int i = 0; i < 7; i++)
	{
		cout << a[i] << "";
	}
	return 0;
}
int partion(int a[], int left, int right)
{
	int temp, pivot;//pivot存放主元
	int i, j;
	i = left;
	pivot = a[right];//选取最右边元素为主元
	for (j = left; j < right; j++)
	{
		//如果第j个元素小于主元,则第j个元素与第i个元素进行交换
		if (a[j] < pivot) {
			temp = a[j];
			a[j] = a[i];
			a[i] = temp;
			i++;//i向前移动一位
		}

	}
	//在下标i的左边,元素均小于主元
	//将主元与第i个元素进行交换
	a[right] = a[i];
	a[i] = pivot;
	//返回最终的主元下标
	return i;
}
void QuickSort(int a[], int left, int right)
{
	if (left < right)
	{
		//求出每次快排分割的边界
		int center = partion(a, left, right);
		QuickSort(a, left, center-1);
		QuickSort(a, center+1, right);
	}
}
分割操作:双向调整

i 向右遍历的过程中,如果遇到大于或等于主元的元素时,则停止移动,j向左遍历的过程中,如果遇到小于或等于主元的元素则停止移动。

代码——双向调整
//双向调整的快速排序
#include<iostream>
using namespace std;
void QuickSort(int a[], int left, int right);
int partion2(int a[], int left, int right);
int main()
{
	int a[7] = { 8,1,3,2,7,6,4 };
	QuickSort(a, 0, 6);
	for (int  i = 0; i <=6; i++)
	{
		cout << a[i] << " " << endl;
	}
	return 0;
}
void QuickSort(int a[], int left, int right)
{
	if (left < right)
	{
		int center = partion2(a, left, right);
		QuickSort(a, left, center - 1);
		QuickSort(a, center + 1, right);
	}
}
int partion2(int a[], int left, int right)
{
	int i=left+1, j=right, temp;
	int pivot = a[left];//主元
	while (true)
	{
		//向右遍历扫描
		while (i <= j && a[i] <= pivot) i++;
		//向左遍历扫描
		while (i <= j && a[j] >= pivot) j--;
		if (i >= j) break;
		//交换
		temp = a[i];
		a[i] = a[j];
		a[j] = temp;
	}
	//把a[j]与主元交换
	a[left] = a[j];
	a[j] = pivot;
	return i;
}
时间复杂度

快速排序的最坏时间复杂度是O(n^2).
快速排序的平均时间复杂度是O(nlogn).

例如有可能会出现一种极端的情况,每次分割的时候,主元左边的元素个数都为0,而右边都为n-1个。这个时候,就需要分割n次了。而每次分割整理的时间复杂度为O(n),所以最坏的时间复杂度为O(n^2)。
而最好的情况就是每次分割都能够从数组的中间分割了,这样分割logn次就行了,此时的时间复杂度为O(nlogn)。
平均时间复杂度,则是假设每次主元等概率着落在数组的任意位置,最后算出来的时间复杂度为O(nlogn),至于具体的计算过程,我就不展开了。
不过显然,像那种极端的情况是极少发生的。

稳定性

不稳定排序算法

参考资料

快速排序具体讲解见参考优质文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值