系列文章目录
相关练习请关注
《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的位置,就需要不断把tmp和arr[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双指针法
思路:
用双指针,cur和prev,cur来遍历整个序列,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]之前 ,则称这种排序算法是稳定 的;否则称为不稳定的
最后我们可以参考一下这个表格,对比每个排序算法

总结
每个排序算法单独来说,可能都不算太难,但是想要一起掌握还是有些困难的,尤其是其中的细节 实现,所以我们要时不时自己手撕一下代码,理解一下思想原理,这样才能夯实排序的基础
我们的排序就先讲到这里,后面还会有一篇
进阶的排序文章,如果有什么问题和建议可以评论留言,感谢大家的支持