编程中常见的排序算法

文章目录


前言

本文将详细介绍日常中常见的几种排序算法,包括冒号排序,插入排序,希尔排序,选择排序,快速排序,归并排序,堆排序,计数排序。小编会从各算法的思路,时间复杂度,代码展示以及优缺点方面等方面进行阐述。


一、冒号排序

  • 冒号排序的基本思想

冒号排序的基本思想是通过对相邻元素的比较和交换来达到排序的目的,由于是相邻的元素进行比较,所以每一轮的遍历都找到最大或者最小的数放在数组的末尾。

  • 冒号排序的动态图演示

以动态演示图为例:数组:[5,8,6,3,9,2,1] 长度:7 排序方式:升序

第一趟:

5,**8** ,6,3,9,2,1\] 5\<8,不交换 \[5,6,**8** ,3,9,2,1\] 8\>6 ,交换 \[5,6,3,**8** ,9,2,1\] 8\>3,交换 \[5,6,3,8,**9** ,2,1\] 8\<9,不交换 \[5,6,3,8,2,**9** ,1\] 9\>2,交换 \[5,6,3,8,2,1,**9** \] 9\>1 交换 这样一趟就跑完了一共比较了6次,得到最大数9 第二趟跟第一趟是一样的,只是比第一趟少比较一次,因为9不参与比较,一共比较了5次。一次类推,直到全部遍历完。

  • 冒号排序的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)//注意这里是n-1,因为最多比较n-1(两两比较).
	{
		int falg = 1;
		for (int j = 0; j < n-1-i; j++)//每遍历一次数组的长度就会减少1
		{
			if (arr[j] > arr[j + 1])//交换(升序)
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] =tmp;
				falg = -1;
			}
		}
		if (falg == 1)break;//这里优化了一下,如果第一次遍历没有交换的话,
		//说明数组本来就是有序,所以这里就判断一下。
	}
}
int main()
{
	int arr[] = {1,2,3,4,5 };//有序
	BubbleSort(arr, sizeof(arr) / sizeof(arr[0])); 
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) 
	{
		cout << arr[i] << " ";
	}
	int arr1[] = { 2,3,5,9,6,8,4,1,7 };//无序  
	BubbleSort(arr1, sizeof(arr1)/sizeof(arr1[0]));
	cout << endl; 
	for (int j = 0; j < sizeof(arr1) / sizeof(arr1[0]); j++) 
	{
		cout << arr1[j] << " "; 
	}
	return 0;
}
  • 冒号排序的优缺点以及时间复杂度

时间复杂度 :最好的情况下(本来就是有序的情况下)O(N),最坏情况下是O(N^2),时间复杂度一般考虑最坏情况,所以是:O(N ^2)。
优点 :实现简单,对小规模和几乎有序的数据效率较高
缺点:当在处理大规模的数据时效率比较底下,在面对大规模的数据排序时,一般优先考虑其他效率比较高的排序算法。

二、插入排序

  • 插入排序的基本思想;

插排序是一种简单且直观的排序方法。它的基本思想是将一个已经记录好的数插入到一个已经排好序的有序表中,从而获得一个新的,长度加1的有序表。

  • 插入排序的动态演示图

如上图是所示:每插入一个数之前,那么这个数之前的所有数都是有序的,然后在把要插入的数从后往前和有序表中的数依次比较,如果比有序表中的数要小,那么就把它放在要插入的那个数的位置,然后继续往前依次比较,如果要有序表中的数要大,那就直接插入到当前的位置。

  • 插入排序的代码演示
cpp 复制代码
#include<iostream>
using namespace std;
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)//注意这里是n-1哦,如果是n的话会越界的,因为当i=n-1的时候,那么这时arr[end+1]就越界访问了
	{
		//单趟
		int end=i;//假设[0,end]这个区间是有序的 
		int tmp = arr[end + 1];//记录一下arr[end+1]位置的数据,在比较的过程中这个位置的数据可能会被前面的数覆盖
		while (end >= 0)
		{
			if (arr[end] > tmp)//比前面的数小,继续往前找
			{
				arr[end + 1] = arr[end];
				--end;
			}
			else //比前面的数大,直接插入
			{
				//这里不直接插入的原因是,当在已有序的数组的最前面插入数据时,
				// end就会变成-1,这时循环结束,但是数据还没有插入,所以又要在循坏外判断一下
				//end是否等于-1,所以为了方便,我们直接在循坏外直接插入就可以解决问题了

				//arr[end + 1] = tmp;
				break;
			}
			/*if (end == -1)
			{
				arr[end + 1] = tmp;
			}*/
			arr[end + 1] = tmp; //插入数据
		}
	}
}
int main()
{
		int arr[] = { 2,3,5,9,6,8,4,1,7 };
		InsertSort(arr,sizeof(arr)/sizeof(arr[0]));  
		for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
		{
			cout << arr[i] << " ";
		}
		return 0;
}
  • 插入排序的优缺点以及时间复杂度分析

时间复杂度:最好情况下,即数组已经是有序的情况下,时间复杂度为O(N);最坏情况下,即数组完全逆序,时间复杂度为O(N^2)。

优点:在于其实现简单且直观,对于小规模数据或者部分有序的数据,其性能表现良好。 此外,插入排序是一种稳定的排序算法,即相等的元素在排序后不会改变它们的相对顺序。然而相比与冒号排序,插入排序的效率比对比较高一点。

缺点:其时间复杂度较高,对于大规模数据或者无序数据的排序效率较低。 这是因为插入排序在每次插入元素时都需要移动已排序的元素,导致大量的数据移动操作。

三、希尔排序

  • 希尔排序的基本思想

希尔排序(Shell Sort)是一种改进的插入排序算法。 它的基本思想是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序

  • 希尔排序的图解展示

这里通过利用gap来分成若干的小组,然后控制这些小组有序从而使得整体数据近似有序,不过这里要注意:

  • 当gap越大时,大的数据会更快的跳到后面,小的数据也会很快的跳到前面,不过整体数据也不接近有序。
  • 当gap越小时,大的数据会更慢跳到后面,小的数据也会很慢跳到前面,但是整体数据越接近有序。当gap=1时,直接就是插入排序,整体数据变有序。
  • 那要怎样控制gap使其效率最高呢?这里一般会对gap进行gap=gap/3+1操作(这里gap=gap/2也行,当然这里除什么数都可以,因为gap是变化且逐渐缩小的,只要我们保证最后一次gap=1即可),gap每次只会越来越小直到 gap=1 让数据有序,gap>1都是在对数据进行预排序,整体数据还没有达到有序的状态。
  • 希尔排序的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void ShellSort1(int* arr, int n)//一组一组插入
{
	int gap = n;
	while (gap>1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++)//有多少组就排多少次
		{
			for (int i = j; i < n - gap; i += gap)//因为间隔是gap,所以i要+=gap,这里的n-gap跟插入排序哪里的越界是一样的逻辑。
			{
				int end = i;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (arr[end] > tmp)
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else break;
				}
				arr[end + gap] = tmp;
			}
		}
	}
}
/*void ShellSort(int* arr, int n)//多组插入
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; ++i)
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end-=gap;
				} 
				else break;
			}
			arr[end+gap] = tmp;
		}
	}	
}
*/
int main()
{
//这两种代码都是一样的,只不过一个是一组一组的插入,另一个事多组插入。认真观察的小伙伴就会发现,当gap=1是,代码跟排序的代码是一样的。
	int arr[] = { 2,3,5,9,6,8,4,1,7 };
	ShellSort1(arr, sizeof(arr) / sizeof(arr[0]));  
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout<<endl;
	ShellSort(arr, sizeof(arr) / sizeof(arr[0])); 
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
  • 希尔排序的优缺点以及时间复杂度分析
    时间复杂度 :希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定,所以这里就直接记住它的时间复杂度是:O(N^1.3)
    优点
    空间复杂度低 :希尔排序是一种原地排序算法,空间复杂度为O(1),只需要少量的额外存储空间。
    效率较高 :相比于简单插入排序,希尔排序通过分组和逐步减少增量的方式,显著减少了元素的移动次数,提高了排序效率
    适用于大规模数据 :希尔排序在处理大规模数据时表现出色,尤其对于具有一定局部有序性的数据。
    缺点
    不稳定 :希尔排序是不稳定的排序算法,在交换元素的过程中可能会改变相同元素的相对次序。
    时间复杂度依赖于增量序列:希尔排序的时间复杂度取决于所选择的增量序列,分析起来比较困难。在某些增量序列下,时间复杂度可接近O(n^(3/2)),但在最坏情况下可能退化为O(n²)。
    增量序列选择困难:增量序列的选择对性能有很大影响,选择不当可能导致效率下降。

四、选择排序

  • 选择排序的基本思想
    选择排序(Selection Sort)是一种简单直观的排序算法。其基本思想是每次从未排序的部分中选出最小(或最大)的元素,将其放到已排序部分的末尾。这个过程重复进行,直到所有元素都排序完毕。
  • 选择排序的动态演示图

选择排序呢跟冒号排序有点相似,但是还有区别的:冒号是两两比较,然而选择则是按顺序比较, 冒泡排序每一轮比较后,位置不对都需要换位置,选择排序每一轮比较都只需要换一次位置。如上图所示,每一轮比较之后,选择最小的或者最大的数放在最前面或者后面。

  • 选择排序的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c; 
}
void SelectionSort(int* arr, int n)
{
	int end = n , begin = 0;
	while (begin < end)
	{
		int min = begin;
		for (int i = begin +1; i < end; i++)//找出最小的数的下标
		{
			if (arr[i] < arr[min])
				min = i;
		}
		Swap(arr[begin], arr[min]); //把最小的数放在最前面 
		begin++;
	}
}
int main()
{
	int arr[] = { 9,1,2,5,7,4,6,3};
	SelectionSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}

但是这个代码还可以优化,我们可以同时找出最小和最大值,最小的放在前面,最小的放在后面。

cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c; 
}
void SelectionSort1(int* arr, int n)  
{ 
	int end = n, begin = 0; 
	while (begin < end) 
	{ 
		int min = begin; int 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 (begin == max)//以[9, 1, 2]为例,现在经过上面的代码之后min=1,max=0,所以会变成[1,9,2],
			max = min;   //但是这时max还是1的下标,如果这里不判断一下直接交换的话,那么就是1和2交换。并没有达到排序的要求。

		Swap(arr[end-1], arr[max]);//把最大的数放在最后面 
		begin++;
		--end;
	}
}
int main()
{
	int arr[] = { 9,1,2,5,7,4,6,3};
	SelectionSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	return 0;
}
  • 选择排序的优缺点以及时间复杂度

时间复杂度分析:选择排序的时间复杂度为 O(N^2),其中 n 是数组的长度。因为无论输入数据如何,选择排序总是需要进行相同次数的比较操作。

优点:不占用额外的内存空间

缺点:不适合用于大规模的数据排序,排序效率低下而且还是一种不稳定的排序算法,因为在交换过程中可能会改变相同元素的相对顺序。

五、堆排序

  • 堆排序的基本思想

堆排序的基本思想分为两步:
构建堆:把无序的数组构建成一个大堆或者小堆,这一步确保了堆顶元素是当前未排序部分的最大值或者最小值。

排序:交换堆顶和数组最后一个元素,然后调整剩余数组使其重新变成一个大堆或者小堆,然后重新重复上面动作直到数组有序。

  • 堆排序的过程演示图

这里要注意一点就是:如果排升序需要建大堆,降序则是建小堆

  • 堆排序的代码展示

代码分为两部分:

一部分是堆排序的主题函数(HeapSort)

另一部分则是调整堆的函数(AdjustUp,AdjustDown)

cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(int* arr, int chiled)//向上调整建堆
{
	int parent = (chiled - 1) / 2;
	while (chiled > 0)
	{
		if (arr[chiled] > arr[parent])
		{
			Swap(&arr[chiled], &arr[parent]);
			chiled = parent;
			parent = (chiled - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void AdjustDown(int* arr, int n, int parent)//向下调整建堆
{
	int chiled = 2 * parent + 1;
	while (chiled < n)
	{
		if (chiled + 1 < n && arr[chiled + 1] > arr[chiled])//假设法
		{
			++chiled;
		}
		if (arr[parent] < arr[chiled])
		{
			Swap(&arr[parent], &arr[chiled]);
			parent = chiled;
			chiled = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}
	int end = n - 1;//最后一个数据
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		--end;
	}
}
int main()
{
	int arr[] = { 9,8,6,5,7,4 };
	HeapSort(arr, sizeof(arr) / sizeof(int));
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl; 
	return 0;
}
 
  • 堆排序的优缺点以及时间复杂度分析
    时间复杂度 :堆排序的总时间复杂度为 O(N log N)。这是因为构建初始堆的时间复杂度为 O(N),而调整堆的时间复杂度为 O(N log N),两者相加后,主导项为 O(N log N)
    优点
    堆排序的时间复杂度为 O(N log N),这使得它在处理大数据量时非常高效 。与其他排序算法相比,堆排序的效率与快速排序和归并排序相当,达到了基于比较的排序算法效率的峰值。堆排序的空间复杂度为 O(1),即它只需要常数级别的辅助空间。这意味着堆排序在内存使用方面非常节省,不需要额外的内存空间来存储中间结果堆排序的效率相对稳定 。无论待排序序列是否有序,堆排序的时间复杂度始终保持 O(N log N)。堆排序的另一个优点是它的实现相对简单 。堆排序不使用递归等高级计算机科学概念,因此更容易理解和实现。这使得堆排序在教学和学习中具有一定的优势。
    缺点:由于堆的维护在数据频繁变动时较为繁琐,堆排序在实际应用中不如快速排序常见。

六、快速排序

  • 快速排序的基本思想
    快速排序是一种被广泛的应用的排序方法,快速排序的工作原理是通过分治的方法将数组排序的。其基本思想就是通过一趟排序将带排序序列分为左右两个子序列,左子序列的数据要小于有子序列的数据,然后在对左右子序列进行排序,直到整个序列有序。
  • 快速排序的动态演示图

注意上图只是对序列进行一趟排序。大概意思就是先固定了key的数,然后在分别从左右两边分别往中间走,右边遇到比key小的数就停下来,左边遇到比key大数就停下,然后再交换左右停下时对应的数,当左右相遇的时候,然后再把key和左右相遇时所对应的数交换。这样就完成了一趟。然后再以左右相遇时的位置为准把序列分为左右序列,最后在对左右序列进行第一趟的操作就行,直到序列有序。

  • 那后面又是怎样排序的呢?如下图所示:

但是这里又有一个问题就是,那为什么在相遇时的数就一定比key小呢?原因如下:


同理:当选右边作key时,左边先走,可以保证相遇时的数一定比key大。

  • 快速排序的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
void QuickSort(int*arr,int left,int right) 
{
	if (left >= right)//递归结束条件
		return;
	int key = left;//这里注意我们在定义key的时候是给下标,而不是直接给值,如果是key=arr[left],那么在交换key的时候就
                   //是跟局部变量交换了,那么在列表中key对应的数还是原来的数。没有达到预期效果,这里是交换key对应列表里的数而不是局部变量
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && arr[end] >= arr[key])  //从右往左找比key小的数 
			--end; 
		while (begin < end && arr[begin] <= arr[key])//从左往右找比key大的数
			++begin; 
		Swap(arr[begin], arr[end]);//交换两数
	}
	Swap(arr[key], arr[begin]); //交换key和相遇时的数
	key = begin;

	//分成左右子序列然后重新上面操作直到序列有序
	QuickSort(arr, left, key-1);
	QuickSort(arr, key+1, right);  
}
int main()
{
	int arr[] = {6,4,8,3,7,8,9,1,2,3,6};
	QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0])-1);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	return 0;
}
  • 快速排序的栈溢出问题
    栈溢出的原因是因为递归的深度太深从而导致栈溢出问题。那么造成快速排序栈溢出的原因就是排序本来就是有序的序列,如果有序的序列长度太长会有栈溢出的风险。

    像上图那样,如果序列是有序的话,那快速排序的时间复杂度会从O(N log N)变成O(N^2)。效率会大大折扣。为了解决这一问题。有两种方法:
  • 随机取key值,那key到底取多大呢?不确定,所以不建议
  • 三数取中 ,意思是三个数中取中间个数。建议使用
    如果是三数取中的话,那么在有序的情况下,就会变成二分的形式,效率会变得更高。具体代码如下:
cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
}
int Getmidi(int* arr, int left, int right)
{
	int min = (left + right) / 2;
	if (arr[left] < arr[min])
	{
		if (arr[min] < arr[right])
		{
			return min;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else return right;
	}
	else//arr[left]>arr[min]
	{
		if (arr[min] > arr[right])
		{
			return min;
		}
		else if (arr[left] < arr[right])
		{
			return left;
		}
		else return right;

	}
}

void QuickSort(int*arr,int left,int right) 
{
	if (left >= right)//递归结束条件
		return;

	//三数取中,
    int midi = Getmidi(arr, left, right);
    Swap(arr[left], arr[midi]);


	int key = left;//这里注意我们在定义key的时候是给下标,而不是直接给值,如果是key=arr[left],那么在交换
                   //key的时候就是跟局部变量交换了,没有达到预期效果,这里是交换key对应列表里的数而不是局部变量
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && arr[end] >= arr[key])  //从右往左找比key小的数 
			--end; 
		while (begin < end && arr[begin] <= arr[key])//从左往右找比key大的数
			++begin; 
		Swap(arr[begin], arr[end]);//交换两数
	}
	Swap(arr[key], arr[begin]); //交换key和相遇时的数
	key = begin;

	//分成左右子序列然后重新上面操作直到序列有序
	QuickSort(arr, left, key-1);
	QuickSort(arr, key+1, right);  
}
int main()
{
	int arr[] = {6,4,8,3,7,8,9,1,2,3,6};
	QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0])-1);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	return 0;
}
  • 小区间优化问题

小区间优化呢就是当快速排序在递归到一个小的区间时,比如下标0到9的子序列时候,其实这里时候就可以不用快速排序来排序了,可以用其他的排序来对这个子序列排序,那为什么不用快速排序呢?加入想让这10数有序那是不是就要多次递归(调用),递归是有消耗的。所以这里我们采用其他的排序方式来完成这个小区间的排序工作。这里选择的排序方式是:插入排序
代码展示:

cpp 复制代码
void QuickSort(int*arr,int left,int right) 
{
	if (left >= right)//递归结束条件
		return;

	//三数取中,
    int midi = Getmidi(arr, left, right);
    Swap(arr[left], arr[midi]);

	if (right - left + 1 < 10)//小区间优化问题
	{
		InsertSort(arr + left, right - left + 1);//注意这里arr要加上left因为可能会对右子序列排序
	}
	int key = left;//这里注意我们在定义key的时候是给下标,而不是直接给值,如果是key=arr[left],那么在交换
                   //key的时候就是跟局部变量交换了,没有达到预期效果,这里是交换key对应列表里的数而不是局部变量
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && arr[end] >= arr[key])  //从右往左找比key小的数 
			--end; 
		while (begin < end && arr[begin] <= arr[key])//从左往右找比key大的数
			++begin; 
		Swap(arr[begin], arr[end]);//交换两数
	}
	Swap(arr[key], arr[begin]); //交换key和相遇时的数
	key = begin;

	//分成左右子序列然后重新上面操作直到序列有序
	QuickSort(arr, left, key-1);
	QuickSort(arr, key+1, right);  
}
  • 使用前后指针实现快速排序

上面实现的是hoare版本的代码。可能hoare版本的不好理解,不好理解的点在于为什么和key交换的值一定比key小。为了更直观理解,所以这里我们用一个前后 双指针来完成排序的代码。先来看动态图吧。

动态图演示的意思呢就是分别有两个指针一个prve指向开头位置,另一个cur指向prve+1的位置。然后cur往后走,遇到比key小的数就停下来。然后prve+1,最后在把prve和cur指向的数交换。直到cur走出序列,然后更新key。注意:这里也只是一趟。思路更hoare版本的差不多。

  • 前后双指针实现快排的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
} 
void QuickSort1(int *arr,int left,int right)
{
	
	if (left >= right)return;
	//三数取中, 
	/*int midi = Getmidi(arr, left, right); 
	Swap(arr[left], arr[midi]); */

	int key = left;
	int prv = left, cur = prv + 1;
	while (cur <= right)
	{
		if (arr[cur] < arr[key]&& ++prv!=cur)//防止自己和自己交换
		{
			Swap(arr[prv], arr[cur]);
		}
		cur++;
	}
	Swap(arr[key], arr[prv]);
	key = prv;
	QuickSort1(arr, left, key - 1); 
	QuickSort1(arr, key + 1, right); 
}
int main()
{
	int arr[] = { 4,7,5,8,9,3,2,1,6 };
	QuickSort1(arr, 0, sizeof(arr) / sizeof(arr[0])-1);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}
  • 快速排序的非递归实现

为什么要实现非递归呢?一个防止递归的深度太深从而导致栈溢出,虽然一般情况下不会。但是还是有必要实现一下的。让递归变成非递归一般的方式有用循环,栈来实现。这里我们采用的是数据结构中栈后进先出的性质来实现快排的非递归。

思路:每走一次,取出栈顶区间,然后单趟排序,最后左右子区间入栈。

cpp 复制代码
#include<iostream>
#include<vector>
#define CONTAINER vector
using namespace std;
void Swap(int& a, int& b)
{
	int c = a;
	a = b;
	b = c;
} 
template<class T>
class Stack :public CONTAINER<T>  //继承vector
{
public:
	void push(const T& data)
	{
		CONTAINER<T>::push_back(data);
	}
	const T& top()const
	{
		return CONTAINER<T>::back();
	}
	void pop()
	{
		CONTAINER<T>::pop_back();
	} 
	bool empty()
	{
		return CONTAINER<T>::empty();
	}
};
int QuickSort(int* arr, int left, int right)   
{
	if (left >= right)
		return 0;
	int key = left;
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end&&arr[end] >= arr[key])  
			--end;
		while (begin < end&&arr[begin] <= arr[key]) 
			++begin;
		Swap(arr[begin], arr[end]);
	}
	Swap(arr[key], arr[begin]);
	return  begin;  
}
void QuickSort_Stack(int* arr, int left, int right)  
{

	Stack<int>s;
	s.push(right);//进栈 
	s.push(left); 

	while (!s.empty())
	{
		//出栈
	   int begin=s.top();//取出栈顶区间
	   s.pop();
	   int end = s.top();
	   s.pop();

	   int key = QuickSort(arr, begin, end);//单趟排序
	   if (key+1<end)//左右子区间分别入栈
	   {
		   s.push(end);
		   s.push(key + 1);
	   }
	   if (key - 1 > begin)
	   {
		   s.push(key - 1);
		   s.push(begin);
	   }
	}
}
int main()
{
	int arr[] = { 4,7,5,8,9,3,2,1,6 };
	QuickSort_Stack(arr, 0, sizeof(arr) / sizeof(arr[0])-1);
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

七、归并排序

  • 归并排序的基本思想

    归并排序是一种基于分治法的高效排序算法,其核心思想是将数组分成两个子数组,分别排序后再合并成一个有序数组。

  • 归并排序的动态演示图

    如过动态图不好理解,那来看看张图,原理是一样。

通过动画演示,可以更直观地理解归并排序的过程。以下是归并排序的动画演示步骤:

  • 初始数组:将数组分成两个子数组。
  • 递归分割:继续将每个子数组分割,直到每个子数组只有一个元素。
  • 合并:从最小的子数组开始,逐步合并成更大的有序数组,直到整个数组有序。
  • 归并排序的代码展示
    这里我们需要用到一个新的数组来进行归并,然后再把新数组中的数据拷贝到原来数组里面,
cpp 复制代码
#include<iostream>
using namespace std;
void _MergeSoet(int *arr, int *tmp,int begin , int end) 
{

	if (begin >= end)return;
	int mid = (begin + end) / 2;
	//如果[begin,mid]和[mid+1,end]有序就可以合并了。
	_MergeSoet(arr, tmp, begin, mid);
	_MergeSoet(arr, tmp, mid+1,end);

	//合并
	int begin1 = begin; int end1 = mid;
	int begin2 = mid + 1; int end2 = end;
	int i = begin; 
	while (begin1<=end1&&begin2<=end2)//有一组结束循环就结束,不过我们这里写的是继续的条件,所以用的是&&
	{
		if (arr[begin1] < arr[begin2])//小的先插入
		{
			tmp[i++] = arr[begin1++];
		}
		else tmp[i++] = arr[begin2++];
	}
	while (begin1 <= end1)//判断数组是否走完
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)//判断数组是否走完
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr+begin, tmp+begin, sizeof(int) * (end - begin + 1));//这里要加begin是因为不是拷贝这个数组,而是begin到end这个区间,可以从动态图中看出 
}
void MergeSort(int *arr,int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == nullptr)
	{
		perror("malloc fail");
		exit(1);
	}
	_MergeSoet(arr, tmp, 0, n-1 );  
	free(tmp);
	tmp = nullptr;
}
int main()
{
	int arr[] = { 9,6,8,7,4,5,6,3,2,1 };
	MergeSort(arr, sizeof(arr) / sizeof(int));
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

这里在控制区间的时候要注意不能定义成:[begin,mid-1]和[mid,end],因为这样会陷入无限递归,就会有栈溢出,具体如下:

  • 归并的时间复杂度分析
    归并排序的时间复杂度为O(N log N)。这是因为每次分解数组都需要log N步,而在每一步中,合并有序数组的过程需要线性时间,即O(N)。因此,总的时间复杂度是O(N log N)。
    在归并排序中,无论数组的初始状态如何,分解和合并的步骤都是固定的,这意味着归并排序的最好、最坏和平均时间复杂度都是O(N log N)。
  • 归并空间复杂度分析
    归并排序的空间复杂度为O(N),这是因为合并过程中需要一个与原数组相同大小的临时数组来存储合并后的结果。此外,由于归并排序是递归进行的,还需要额外的空间来存储递归调用的栈信息,这个空间的大小是O(log N)。

八、计数排序

  • 计数排序的基本思想

计数排序是一种非比较型排序算法,其核心思想是通过统计每个元素的出现次数,并利用这些计数信息将元素放置到正确的位置,从而实现排序。它适用于整数或有限范围内的数据排序,时间复杂度为 O(n + k),其中 n 是待排序元素数量,k 是数据范围大小。

  • 基数排序的动态演示图

如上图所示,遍历原数组把数据放在另一个申请数组中,重复的元素就在原来的基础上加1,当把原数组遍历完之后,然后在把申请数组里的元素依次放入原数组。

  • 计数排序的代码展示
cpp 复制代码
#include<iostream>
using namespace std;
void CoubtSort(int* arr, int n)
{
	int min = arr[0]; int max = arr[0];
	for (int i = 1; i < n; i++)
	{
		if (min > arr[i])
			min = arr[i];

		if (max < arr[i])
			max = arr[i];
	}
	int range = max - min + 1;
	//申请给定的范围空间
	int* count = (int*)calloc(range, sizeof(int));
	if (count == nullptr)
	{
		perror("calloc fail");
		exit(1);
	}
	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}
	//排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			arr[j++] = i + min;//把元素依次放入原数组中
		}
	}
	free(count);
	count = nullptr;
}
int main()
{
	int arr[] = { 3,6,8,7,9,5,4,2,1 };
	CoubtSort(arr, 9);
	for (int i = 0; i < 9; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl; 
	return 0;
}
  • 计数排序的优缺点以及时间复杂度分析
    时间复杂度
    计数排序的时间复杂度为 O(N+K),其中 N 是数组长度,K 是数据范围(最大值与最小值的差)。空间复杂度为 O(K),需要额外的存储空间来保存计数数组。由于不涉及元素间的比较,计数排序是一种稳定排序算法。
    优点
    时间复杂度较低,适用于数据范围较小的整数排序。
    是一种稳定排序算法,相同元素的相对顺序不会改变。
    缺点
    仅适用于整数或有限范围内的数据。
    当数据范围过大时,空间复杂度较高,效率下降。

总结

今天就到这里吧,我们下期再见,如果有什么问题咋们评论区讨论吧

相关推荐
拾光Ծ2 小时前
【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略
数据结构·c++·算法
Yupureki2 小时前
从零开始的C++学习生活 4:类和对象(下)
c语言·数据结构·c++·学习
草莓熊Lotso2 小时前
揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
开发语言·c++
还有几根头发呀2 小时前
[特殊字符] LeetCode 143 重排链表(Reorder List)详解
数据结构·链表
hui函数2 小时前
python全栈(基础篇)——day04:后端内容(字符编码+list与tuple+条件判断+实战演示+每日一题)
开发语言·数据结构·python·全栈
晨非辰5 小时前
《剑指Offer:单链表操作入门——从“头删”开始破解面试》
c语言·开发语言·数据结构·c++·笔记·算法·面试
啊我不会诶8 小时前
24ICPC成都站补题
数据结构·算法
仟千意9 小时前
数据结构:栈和队列
数据结构
渡我白衣9 小时前
list 与 forward_list:一场 STL 中的“链表哲学”之争
数据结构·c++·list