八大排序算法

系列文章目录

《数据结构》

相关练习请关注
《Leetcode&nowcode代码强化刷题》

文章目录


前言

欢迎来到本专栏!对编程新手而言,C 语言是入门关键,而数据结构与算法是其核心逻辑 ------ 不仅是计算机基础,更是提升代码效率、应对学习与面试的必备能力。​

但很多初学者常被 "概念抽象""代码难写""学了不会用" 困住,难以真正掌握。为此,本专栏从初学者视角出发,从基础讲起:讲数组、链表等数据结构时,先结合生活案例讲用途,再逐行带练 C 语言实现;讲排序、查找等算法时,拆解核心思想 + 真题案例,还配练习题巩固,确保 "学一个会一个"。​

无论你是刚学完 C 语言想进阶,还是学数据结构遇瓶颈,这里都能给你清晰的学习路径。接下来,让我们一起攻克重点,在编程中成长,期待与你共同进步!​


正文

这篇文章我们重点为大家介绍一下我们的八种排序算法,主要带着大家去了解各种排序的它的思想,以及代码实现,最后还会分析它的复杂度等等

大家可以参考下面的图片,先大致看一下:

接下来的排序我们都用这个序列做示范,并且默认排升序

c 复制代码
int a[]={ 5, 3, 9, 6, 2, 4, 7, 1, 8};

插入排序

首先我们来看插入排序,我们先直接插入排序,再来看它的优化版,也就是希尔排序

直接插入排序

基本思想

把待排元素,与一个有序序列从后向前比较,把它插入到这个有序序列之中

比如我们这个序列

c 复制代码
int a[]={ 5, 3, 9, 6, 2, 4, 7, 1, 8};

当我们选取第一个,作为只有一个元素的序列,它本身就是有序的,之后我们,依次拿后面无序的序列中的第一个元素插入到有序序列中

怎么插入呢?

我们拿待插入元素和有序序列的最后一个元素进行比较,如果比有序序列直接加入有序序列中,如果比有序序列,我们继续找有序序列的上一个元素,进行比较,一直比较到对应的插入位置

这种排序方法,就类似于我们玩扑克牌,对每一张拿到手里的牌进行排序

代码实现

那么我们来用代码实现一下这一过程

首先,我们这个排序函数,需要接收待排序数组的地址元素个数

c 复制代码
void InsertSort(int* arr, int n);

其次,我们需要两个变量,一个指向有序序列最后一个元素,另一个指向我们的待插入元素

c 复制代码
int end = 0;//记录已经排序好的数据的末尾
int tmp = arr[end + 1];//tmp保存要排序数据的值,也就是end后面的值

每次我们排序完一个数据,这个有序序列的末位置都会++,所以我们利用我们的for循环中的i去不断初始化每次的end

c 复制代码
for (int i = 0; i < n-1; i++)//i到n-1,说明当前数组已经有序
{
	int end = i;//记录已经排序好的数据的末尾
	int tmp = arr[end + 1];//tmp保存要排序数据的值,也就是end后面的值
}

对于每一次排序 ,我们去确定tmp的位置,就需要不断把tmparr[end]比较 ,比arr[end] ,我们要让end向前走 ,继续比较,最坏情况就是走到头 ,说明tmp应该放在第一位

c 复制代码
while (end >= 0)
{
	if (tmp<arr[end])//需要插入
	{
		arr[end + 1] = arr[end];//原来的尾巴,变到end+1的位置
		end--;//继续向后比较
	}
	else//不需要插入
	{
		break;
	}
}

这里因为我们待排序的值已经存到tmp中了,所以我们可以直接让比tmp大的值向后覆盖

当我们跳出循环之后,此时end所指向的位置的下一个 位置,就是tmp应该在的位置

c 复制代码
//找到了tmp应该在的位置
arr[end + 1] = tmp;

这样我们的直接插入排序就写完了,我们来整体看看

c 复制代码
//直接插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)//i到n-1,说明当前数组已经有序
	{
		int end = i;//记录已经排序好的数据的末尾
		int tmp = arr[end + 1];//tmp保存要排序数据的值,也就是end后面的值
		//开始把tmp往有序数据中插入
		while (end >= 0)
		{
			if (tmp<arr[end])//需要插入
			{
				arr[end + 1] = arr[end];//原来的尾巴,变到end+1的位置
				end--;//继续向后比较
			}
			else//不需要插入
			{
				break;
			}
		}
		//找到了tmp应该在的位置
		arr[end + 1] = tmp;
	}

}

我们调用打印一下看看

我们还可以看看下面的动图演示

复杂度分析

我们可以发现,当这个数组中的元素,越接近有序 ,我们while循环的次数越少 ,但是最坏情况,我们的数组是一个逆序的,这样每次while循环,我们都需要从尾走到头

所以我们说直接插入排序的时间复杂度O(N^2)

我们只创建了有限个变量,所以我们的空间复杂度O(1)

以上就是我们的直接插入排序

希尔排序

希尔排序法⼜称缩⼩增量法

基本思想

希尔排序法的基本思想是:先选定⼀个整数(通常是gap = n/3+1),把

待排序序列所有元素分成各组,所有的距离相等的元素分在同⼀组内,并对每⼀组内的元素进⾏排序,然后gap=gap/3+1得到下⼀个整数, 将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插⼊排序

它是在直接插⼊排序 算法的基础上进⾏改进⽽来的,综合来说它的效率肯定是要**⾼于** 直接插⼊排序算法的

大家可以参考上面的图

我再简单的阐述一下,就是我们先选择一个gap,然后每个相距 gap个元素的数据为一组 ,对每组 数据都进行直接插入排序 ,每组排好之后,gap变小,再进行上面的操作,当gap缩小到1的时候,就是我们的直接插入排序 ,但是此时的序列已经是我们经过上面的几次分组排序优化 过的了,所以这一次的直接插入排序效率很高

代码实现

首先,我们的希尔排序,需要的参数还是排序序列的地址和元素个数

c 复制代码
void ShellSort(int* arr, int n);

其次,我们需要一个gap

c 复制代码
int gap = 5;

gap>1的时候,我们要反复对序列进行划组 ,排序

所以我们进行一个while循环

c 复制代码
while(gap>=1)
{
}

循环内部的核心逻辑还是一样,只不过我们需要注意,现在序列中的数据都相隔gap个位置,所以我们的核心代码也要进行修改

c 复制代码
//每隔gap个数据为一组,
int end = i;//记录已经排序好的数据的末尾
int tmp = arr[end + gap];//tmp保存要排序数据的值,也就是end后面的值
//开始把tmp往有序数据中插入
while (end >= 0)
{
	if (tmp < arr[end])//需要插入
	{
		arr[end + gap] = arr[end];//原来的尾巴,变到end+gap的位置
		end-=gap;//继续向后比较,组中的每个数据相隔gap个
	}
	else//不需要插入
	{
		break;
	}
}
//找到了tmp应该在的位置
arr[end + gap] = tmp;

对于每次end的取值,如果我们直接想,它有gap组,每组都要一个个排序,就会写成这样

c 复制代码
for(int j=0;j<n-gap;j++)
{
   for(int i=j;i<n;i+=gap)
    {
    }
}

这样嵌套两层循环太麻烦了,我们可以简化一下

我们不一定需要每次 把一组排完 ,再去排第二组

因为每个组每个元素 都要进行排序,所以我们可以按照元素的顺序,到哪就排哪 ,这样我们只需要一个循环就可以了

c 复制代码
for (int i = 0; i < n - gap; i++)//i到n-gap
{
	//每隔gap个数据为一组,
	int end = i;//记录已经排序好的数据的末尾
	int tmp = arr[end + gap];//tmp保存要排序数据的值,也就是end后面的值
	//开始把tmp往有序数据中插入
	while (end >= 0)
	{
		if (tmp < arr[end])//需要插入
		{
			arr[end + gap] = arr[end];//原来的尾巴,变到end+gap的位置
			end-=gap;//继续向后比较,组中的每个数据相隔gap个
		}
		else//不需要插入
		{
			break;
		}
	}
	//找到了tmp应该在的位置
	arr[end + gap] = tmp;
}

最后真正让我们数据排序好的,其实是gap=1的时候,所以每次循环,我们都要让gap/3+1
为什么除3加1除3是为了让gap变小+1是为了保证我们一定会进行gap=1的直接插入排序,在此之前都是为了不断把这个序列优化

所以我们也把gap>1时的操作,叫作预排序

我们看看整体代码

c 复制代码
//希尔排序
//每相隔gap个元素的数据取一个数据为一组,进行InsertSort
//gap除三加一,gap=1,就是InsertSort
void ShellSort(int* arr, int n)
{
	int gap = 5;
	while(gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)//i到n-gap
		{
			//每隔gap个数据为一组,
			int end = i;//记录已经排序好的数据的末尾
			int tmp = arr[end + gap];//tmp保存要排序数据的值,也就是end后面的值
			//开始把tmp往有序数据中插入
			while (end >= 0)
			{
				if (tmp < arr[end])//需要插入
				{
					arr[end + gap] = arr[end];//原来的尾巴,变到end+gap的位置
					end-=gap;//继续向后比较,组中的每个数据相隔gap个
				}
				else//不需要插入
				{
					break;
				}
			}
			//找到了tmp应该在的位置
			arr[end + gap] = tmp;
		}
	}

}

看看排序效果

复杂度分析

空间复杂度一眼出O(1)

时间复杂度,while循环的复杂度是logn ,这个我们知道,那么里面的复杂度是多少呢,其实这特别难计算,因为它随着我们的gap而变化,但是我们的一些大佬,经过各种推理,最后给出,希尔排序的时间复杂度O(n^1.3)

以上就是我们的希尔排序,就时间复杂度而言,已经是一个很不错的排序算法了


选择排序

直接选择排序

基本思想

再未排序序列中选一个最小(或最大)元素,与第一个元素进行交换 ,直到未排序序列中就剩一个元素,大家可以参考下面的动图

我们为了效率,可以一次性找最大最小,然后让它们分别和最后一位,第一位进行交换

代码实现

首先,我们的直接选择排序需要的参数还是序列地址元素个数

c 复制代码
void SelectSort(int* arr, int n);

然后我们需要定义两个变量 ,分别指向我们的最大最小元素这次应该放的位置

c 复制代码
int begin = 0;//放未排序元素中最小的值
int end = n - 1;//放未排序元素中最大值

之后我们循环找最值放最值

c 复制代码
while (begin < end)
{
	int mini = begin;//记录最小的下标
	int maxi = begin;//记录最大的下标
	for (int i = begin+1; i <= end; i++)//寻找最大和最小值
	{
		if (arr[i] < arr[mini])
		{
			mini = i;
		}
		if (arr[i] > arr[maxi])
		{
			maxi = i;
		}
	}
	}
	Swap(&arr[mini], &arr[begin]);
	Swap(&arr[maxi], &arr[end]);
	begin++;
	end--;
}

这里的Swap函数是我自己写的,大家用的时候也要声明定义一下

按道理,我们的直接选择排序就写好了,我们来看看效果

可以发现,我们中间的元素不是有序的,这时候,我们就需要模拟一下排序,看看有什么问题

最后我们发现,原来在交换的时候,会出现这样的情况

最大最小,刚好在我们需要交换的位置,这导致我们交换两次之后,反而等于没交换

所以我们要进行特判

c 复制代码
if (maxi == begin)//特判
{
	maxi = mini;//让我们的maxi先到mini的位置等着,mini会把begin上max值,送过来,再让maxi和end进行交换
}

这时候我们再看看

ok了

复杂度分析

循环嵌套,时间复杂度O(N^2)
空间复杂度O(1)

堆排序

基本思想

堆排序,本质上是借助堆的思想进行排序(而不是借助数据结构堆),因为我们的堆顶元素总是最值 元素,所以我们只需要不断的拿堆顶元素出来,理论上就可以得到一个有序序列

代码实现

首先,堆排序我们需要的参数还是序列地址和元素个数

c 复制代码
//堆排
void HeapSort(int* arr, int n)

利用堆的思想,那我们首先需要把我们想要排序的序列建成一个堆

这里我们就需要进行选取建堆法

在堆那部分,我们介绍了两个算法,一个是向上调整算法,另一个是向下调整算法,这两个算法都可以用来建堆,那么我们用哪个呢?

这时候我们就需要对比一下它们的时间复杂度

我们现在看看向上调整算法

通过计算,向上调整算法的时间复杂度O(N*logN)

再来看看向下调整算法建堆

向下调整算法建堆时间复杂度O(logN)

对比一下,我们可以发现,使用向下调整算法时间复杂度更优

那么我们该如何用向下调整算法建堆呢?

我们的思想是,先找到最小的一颗树,也就是从上到下,从左到右的最后一颗树,先把进行调整

我们可以根据结点总数,直接找到最后一棵子树根节点 的,调整完之后,一次往整个树的根结点去走,不断的调整每棵子树

c 复制代码
//向下调整建堆法
for (int i = (n-1-1)/2; i>=0 ; i--)
{
	AdjustDown(arr, i, n);
}

这里假设我们建了一个大堆 ,此时我们把堆顶元素最后一个元素进行交换 ,交换后,我们对整个堆进行向下调整,这时候我们不用带上最后一个元素,反复进行整个操作,最后,就可以得到一个升序的序列

c 复制代码
int end = n - 1;
//循环把最值元素放到最后
while (end > 0)
{
	Swap(&arr[0], &arr[end]);
	AdjustDown(arr, 0, end);
	end--;
}

所以说,排升序,建大堆,排降序,建小堆

整体代码

c 复制代码
//堆排
void HeapSort(int* arr, int n)
{
	//向下调整建堆法
	for (int i = (n-1-1)/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--;
	}
}

向下调整算法可以到我们的数据结构(二叉树)学习

复杂度分析

时间复杂度,我们再向下调整算法的基础上加了一个循环,所以是O(N*logN)
空间复杂度O(1)

堆排序就时间复杂度来说,已经很优秀了


交换排序

冒泡排序

基本思想

冒泡排序的基本思想就是,前后元素两两比较交换

代码实现
c 复制代码
void BubbleSort(int* a, int n)
{
	for (size_t i = 0; i < n-1; i++)
	{
		int flag = 1;
		for (size_t j = 0; j < n-1-i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j] ,&a[j + 1]);
				flag = 0;
			}
		}
		if (flag)
		{
			break;
		}
	}
}

冒泡排序的实现相对简单,两层循环嵌套,第二层循环在每次进行时,因为已经排过一个元素了,所以循环次数递减,为了防止已经有序的序列我们反复进行排序,所以用一个flag检查一下这一次循环有没有元素交换

复杂度分析

两层循环,时间复杂度O(N^2)
空间复杂度O(1)

快速排序

基本思想

任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序集合分割两⼦序列左⼦序列 中所有元素均⼩于基准值右⼦序列 中所有元素均⼤于基准值,然后最左右⼦序列重复该过程,直到所有元素都排列在相应位置上为⽌

代码实现(递归版)

快排我们需要这个序列的整个区间范围

所以我们的参数需要序列地址左右区间

c 复制代码
//快速排序(递归版)
void QuickSort(int* arr, int left, int right);

我们函数体部分先大致实现一下,首先我们需要把一个基准值放到对应位置,然后根据这个基准值位置,划分序列,每个序列再找基准值,划分序列,直到给的序列就剩一个元素为止

c 复制代码
if (left >= right)
{
	return;
}
//把给定区间按照基准值keyi给划分成左右区间
int keyi = _QuickSort(arr, left, right);
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);

这里我们关键的就是如何找这个基准值的位置

假设我们每次以第一个元素为基准值,这时候我们有三个方法找基准值位置

hoare版本

算法思路 :

①创建左右指针,确定基准值

②从右向左找出⽐基准值**⼩** 的数据,从左向右找出⽐基准值**⼤** 的数据,左右指针数据交换 ,进⼊下次循环

我们来看看具体代码

c 复制代码
//hoare版本插入基准值
int _QuickSort1(int* arr, int left, int right)
{
	int keyi = left++;//把区间内的第一个元素当作基准值
	while (left <= right)
	{
		//right从右向左,找到比keyi位置小的数据
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}
		//left从左向右,找比keyi位置大的数据
		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}
		//已经找到了比arr[keyi]大的和小的数据了
		if(left<=right)//如果符合要求
		Swap(&arr[left++], &arr[right--]);//注意--++,继续向后,防止死循
	}
	//rihgt所在位置就是基准值该在的位置
	Swap(&arr[keyi], &arr[right]);
	return right;
}

这里有几个问题

问题1:为什么跳出循环后right位置的值⼀定不⼤于key?

因为,当 left > right 时,即right⾛到left的左侧,⽽left扫描过的数据均不⼤于 key,因此right此时指向的数据⼀定不⼤于 key

问题2:为什么left 和 right指定的数据和key值相等时也要交换?

相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量重复时,⽆法进⾏有效的分割排序

上面有几个场景,大家可以模拟试试

我们看看排序效果

挖坑法

思路:

创建左右指针,⾸先从右向左找出⽐基准**⼩** 的数据,找到后⽴即放⼊左边坑 中,当前位置变为新的"坑",然后从左向右找出⽐基准**⼤** 的数据,找到后⽴即放⼊右边坑 中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放⼊当前的"坑"中,返回当前"坑"下标

大家可以看看动图理解一下

代码实现

c 复制代码
//挖坑法找基准值
int _QuickSort2(int* arr, int left, int right)
{
	int hole = left;//坑位
	int key = arr[hole];//保存基准值
	//这里不需要left++
	//因为只有两个数据了,left++,就排不了了
	while (left < right)
	{
		while (left<right&&arr[right] > key)//right从右向左找比基准值要小的
		{
			right--;
		}
		//找到后,去填坑,变新坑
		arr[hole] = arr[right];
		hole = right;
		while (left < right && arr[left] < key)//left从左向右找比基准值大的
		{
			left++;
		}
		//找到后,去填坑,变新坑
		arr[hole] = arr[left];
		hole = left;
	}
	//找到基准值坑位
	arr[hole] = key;
	return hole;
}

我们需要不断的找坑,填坑

看看排序效果

lumoto双指针法

思路:

用双指针,curprevcur来遍历整个序列,prev始终指向比基准值小的序列的最后一个元素,最后交换 让基准值到prev的位置

代码实现

c 复制代码
//lumoto法找基准值(双指针)
int _QuickSort3(int* arr, int left, int right)
{
	int keyi = left;
	int prev = left;//指向所有比基准值小的元素的区间的末尾元素
	int cur = prev+1;//遍历区间
	while (cur <= right)
	{
		if (arr[keyi] > arr[cur]&&(++prev)!=cur)//比基准值小
		{
			Swap(&arr[prev], &arr[cur]);
		}
		cur++;
	}
	Swap(&arr[keyi], &arr[prev]);
	return prev;
}

大家可以参考动图理解

看看排序效果

复杂度分析

空间复杂度,每次递归都会把区间/2,所以递归次数logN*每次开的有限空间,所以空间复杂度为O(logN )

时间复杂度,递归logN次,每次要遍历一遍序列,所以时间复杂度O(NlogN)

归并排序

归并排序

基本思想

先将序列分解成子序列,将已有序的⼦序列合并,得到完全有序的序列;即先使每个⼦序列有序,再使⼦序列段间有序

简单说,就把序列不断分,分到最小单位,在对他们进行不断地合并有序序列的操作

我们可以参考上面这张图片理解

代码实现

归并排序,我们还是需要它的序列地址和元素个数

c 复制代码
//归并排序
void mergeSort(int* a, int n);

对这个序列进行分解合并,我们需要单独封装成一个函数,传入分解区间

先递归分解,分解完之后,我们在递归合并,考虑到合并的时候会出现数据覆盖的情况,我们可以单独开一个大小和原序列一样的空间,用来存放排序好的序列

c 复制代码
//创建一个临时的数组,用于对分解后的数据进行存储
int* tmp = (int*)malloc(sizeof(int) * n);
c 复制代码
void _mergeSort(int* a, int left, int right,int* tmp)
{
	//分解,需要递归分解
	if (left >= right)//递归结束条件
	{
		return;
	}
	int mid = (left + right) / 2;
	//左右区间:[left,mid][mid+1,right]
	//分解左右区间
	_mergeSort(a, left, mid,tmp);
	_mergeSort(a, mid + 1, right,tmp);

	//合并两个有序序列:[left,mid][mid+1,right]
	//不想改变我们左右区间的值,使用变量保存之后使用
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	//需要一个下标,控制我们目前合并的区间范围
	int index = left;
	//循环合并
	while (begin1 <= end1 && begin2 <= end2)
	{
		//比较两个区间的值,进行合并
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//要么begin1区间没放完
	//要么begin2区间没放完
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}
	//放完了
	//把tmp数组中的值,放回a数组中对应的位置[left,right]

	for (size_t i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}

最后我们来看看整体代码

c 复制代码
void _mergeSort(int* a, int left, int right,int* tmp)
{
	//分解,需要递归分解
	if (left >= right)//递归结束条件
	{
		return;
	}
	int mid = (left + right) / 2;
	//左右区间:[left,mid][mid+1,right]
	//分解左右区间
	_mergeSort(a, left, mid,tmp);
	_mergeSort(a, mid + 1, right,tmp);

	//合并两个有序序列:[left,mid][mid+1,right]
	//不想改变我们左右区间的值,使用变量保存之后使用
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	//需要一个下标,控制我们目前合并的区间范围
	int index = left;
	//循环合并
	while (begin1 <= end1 && begin2 <= end2)
	{
		//比较两个区间的值,进行合并
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//要么begin1区间没放完
	//要么begin2区间没放完
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}
	//放完了
	//把tmp数组中的值,放回a数组中对应的位置[left,right]

	for (size_t i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}

//归并排序
void mergeSort(int* a, int n)
{
	//创建一个临时的数组,用于对分解后的数据进行存储
	int* tmp = (int*)malloc(sizeof(int) * n);
	//[0,n-1]
	_mergeSort(a, 0, n - 1,tmp);
	free(tmp);
	tmp = NULL;
}

归并排序的代码有好多细节,大家可以参考图片,和我的代码中一些注释,自己多练习理解一下

复杂度分析

时间复杂度,因为我们每次递归都会二分,而且循环,所以时间复杂度为:O(N*logN)

空间复杂度,我们申请了一个size大小的空间,空间复杂度O(N)


以上就是我们常见的比较排序的算法

非比较排序

接下来,我们看一个非比较排序的算法

计数排序

基本思想

统计相同元素出现次数,根据统计的结果将序列回收到原来的序列中

代码实现

我们要先开辟一个count数组,用来存每个元素出现的次数,之后按照顺序,和出现次数,把它们依次还原到原序列

这里我们的count数组的大小,只需要开max-min+1个空间大小就好了,保证我们序列中的每个元素都可以计数就可以,而且我们在记录每个元素的时候,可以让它减去最小值,最后我们还原的时候,再把它们加上就好了

我们看看代码

c 复制代码
void CountSort(int* arr, int n)
{
	//找最大值和最小值
	int max = arr[0], min = arr[0];
	for (size_t i = 0; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if (arr[i] < min)
		{
			min = arr[i];
		}
	}
	//找到开辟数组的范围
	int range = max - min - 1;
	//开辟计数数组
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	//对计数空间进行初始化
	memset(count, 0, sizeof(range) * range);
	//映射计数
	for (size_t i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}
	//根据count数组,把元素放回原数组中
	//遍历count数组,大小为range
	int index = 0;
	for (size_t i = 0; i < range; i++)
	{
		//根据count数组中的值进行循环
		while (count[i]--)
		{
			arr[index++] = i + min;
		}
	}
	free(count);
	count = NULL;
}
复杂度分析

时间复杂度O(N)
空间复杂度O(N)

我们可以发现,计数排序虽然时间复杂度很优秀,但是空间消耗很大,所以他一般只适合 对于一些比较集中的数据进行排序


排序算法复杂度及稳定性分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中r[i]=r[j,且r[i]在r[j]之前 ,⽽在排序后的序列中,r[i]仍在r[j]之前 ,则称这种排序算法是稳定 的;否则称为不稳定

最后我们可以参考一下这个表格,对比每个排序算法

总结

每个排序算法单独来说,可能都不算太难,但是想要一起掌握还是有些困难的,尤其是其中的细节 实现,所以我们要时不时自己手撕一下代码,理解一下思想原理,这样才能夯实排序的基础

我们的排序就先讲到这里,后面还会有一篇进阶的排序文章,如果有什么问题和建议可以评论留言,感谢大家的支持

相关推荐
blammmp8 小时前
算法专题十九:记忆化搜索(暴搜->记忆化搜索)
算法·深度优先·记忆化搜索
MicroTech20258 小时前
边缘智能的创新:MLGO微算法科技推出基于QoS感知的边缘大模型自适应拆分推理编排技术
科技·算法·ai
王哈哈^_^10 小时前
【数据集】【YOLO】目标检测游泳数据集 4481 张,溺水数据集,YOLO河道、海滩游泳识别算法实战训练教程。
人工智能·算法·yolo·目标检测·计算机视觉·分类·视觉检测
巴里巴气10 小时前
第73题 矩阵置零
线性代数·算法·矩阵
voice67011 小时前
密码学实验二
算法·密码学·哈希算法
Blossom.11811 小时前
把AI“编”进草垫:1KB决策树让宠物垫自己报「如厕记录」
java·人工智能·python·算法·决策树·机器学习·宠物
寂静山林12 小时前
UVa 10989 Bomb Divide and Conquer
算法
兮山与12 小时前
算法23.0
算法
共享家952713 小时前
数独系列算法
算法·深度优先