【数据结构】:C 语言常见排序算法的实现与特性解析


🎬 博主名称月夜的风吹雨
🔥 个人专栏 : 《C语言》《基础数据结构》

⛺️任何一个伟大的思想,都有一个微不足道的开始!


各位读者朋友,提前说明:本文系统解析 C 语言常见排序算法(含插入、选择、交换、归并及非比较排序),涵盖每类算法的实现思路、核心代码与特性对比,篇幅稍长但干货密度较高 ------ 从基础算法到复杂度分析,每部分均搭配具体思考逻辑,适合用来梳理排序算法体系。建议大家可先浏览目录定位感兴趣的章节,或收藏后分时段消化,以便更好地理解不同算法的适用场景与实现细节~

引言

排序是数据处理中的基础操作,指将一组记录按关键字大小(递增或递减)排列的过程,广泛应用于购物筛选、院校排名、数据检索等场景。本文基于 C 语言,系统解析插入排序、选择排序、交换排序、归并排序及非比较排序等常见算法的实现逻辑,梳理每个算法的设计思路、关键代码与核心特性,为不同场景下的排序方案选择提供参考。


文章目录

  • 引言
  • 一、排序的基本概念与算法分类
    • [1.1 核心概念](#1.1 核心概念)
    • [1.2 算法分类](#1.2 算法分类)
  • 二、插入排序类算法
    • [2.1 直接插入排序](#2.1 直接插入排序)
      • [2.1.1 基本思想](#2.1.1 基本思想)
      • [2.1.2 实现思路与代码](#2.1.2 实现思路与代码)
      • [2.1.3 特性总结](#2.1.3 特性总结)
    • [2.2 希尔排序(缩小增量排序)](#2.2 希尔排序(缩小增量排序))
      • [2.2.1 基本思想](#2.2.1 基本思想)
      • [2.2.2 实现思路与代码](#2.2.2 实现思路与代码)
      • [2.2.3 特性总结](#2.2.3 特性总结)
  • 三、选择排序类算法
    • [3.1 直接选择排序](#3.1 直接选择排序)
      • [3.1.1 基本思想](#3.1.1 基本思想)
      • [3.1.2 实现思路与代码](#3.1.2 实现思路与代码)
      • [3.1.3 特性总结](#3.1.3 特性总结)
    • [3.2 堆排序](#3.2 堆排序)
  • 四、交换排序类算法
    • [4.1 冒泡排序](#4.1 冒泡排序)
      • [4.1.1 基本思想](#4.1.1 基本思想)
      • [4.1.2 实现思路与代码](#4.1.2 实现思路与代码)
      • [4.1.3 特性总结](#4.1.3 特性总结)
    • [4.2 快速排序](#4.2 快速排序)
      • [4.2.1 Hoare 版本(左右指针法)](#4.2.1 Hoare 版本(左右指针法))
      • [4.2.2 挖坑法](#4.2.2 挖坑法)
      • [4.2.3 前后指针法(Lomuto 版本)](#4.2.3 前后指针法(Lomuto 版本))
      • [4.2.4 非递归版本(栈模拟递归)](#4.2.4 非递归版本(栈模拟递归))
      • [4.2.5 特性总结](#4.2.5 特性总结)
  • 五、归并排序
    • [5.1 基本思想](#5.1 基本思想)
    • [5.2 实现思路与代码](#5.2 实现思路与代码)
    • [5.3 特性总结](#5.3 特性总结)
  • 六、非比较排序:计数排序
    • [6.1 基本思想](#6.1 基本思想)
    • [6.2 实现思路与代码](#6.2 实现思路与代码)
    • [6.3 特性总结](#6.3 特性总结)
  • 七、排序算法复杂度与稳定性汇总
  • 八、总结

一、排序的基本概念与算法分类

1.1 核心概念

  • 稳定性:若待排序列中存在相同关键字的记录,排序后其相对次序保持不变,则算法稳定,否则不稳定;
  • 时间复杂度:排序过程中关键字比较与数据移动的次数,反映算法效率;
  • 空间复杂度:排序过程中额外占用的存储空间,反映资源消耗。

1.2 算法分类

根据排序逻辑的差异,常见排序算法可分为四类:

🔍在讲解排序算法之前,我们需要先理解 "稳定性" 的概念。稳定性指的是排序后相同元素的相对位置是否保持不变:若位置改变则为不稳定,保持不变则为稳定。

例如 ,给定数组:[ 5, 5, 3],排序后变为:[3, 5, 5]。可以看到原本排在5前面的5,排序后位置发生了改变,这就是不稳定的排序表现。


二、插入排序类算法

插入排序的核心思想是 "将待排元素插入已有序的子序列",适用于元素接近有序的场景。

2.1 直接插入排序

2.1.1 基本思想

将数组分为 "已有序段" (初始为第 1 个元素)和 "待插入段" ,依次将待插入段的元素(从第 2 个开始)插入已有序段的合适位置,使有序段逐渐扩展至整个数组。

2.1.2 实现思路与代码

  • 插入第i个元素时,需先保存该元素(避免后续移动覆盖),再从已有序段的末尾(i-1位置)向前比较:若有序段元素大于待插元素,则向后移动;直到找到小于等于待插元素的位置,将待插元素插入该位置后。
c 复制代码
void InsertSort(int* a, int n) 
{
    // 遍历待插入段(从第2个元素开始,i是待插入段的前一个下标)
    for (int i = 0; i < n - 1; i++) 
    {
        int end = i;          // 已有序段的末尾下标
        int tmp = a[end + 1]; // 保存待插入元素(避免移动时覆盖)
        
        // 从后向前比较,移动有序段元素
        while (end >= 0) 
        {
            if (a[end] > tmp) 
            {  //这里一定要用>,>=会改变稳定性
                a[end + 1] = a[end]; // 元素后移
                end--;
            } 
            else 
            {
                break; // 找到插入位置,退出循环
            }
        }
        a[end + 1] = tmp; // 插入待插元素
    }
}

2.1.3 特性总结

  • 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均),( O ( N ) O(N) O(N))(最好,数组已有序);
  • 空间复杂度 :( O ( 1 ) O(1) O(1))(原地排序);
  • 稳定性:稳定;
  • 适用场景:小规模数据或接近有序的数据。

2.2 希尔排序(缩小增量排序)

2.2.1 基本思想

直接插入排序的优化 :通过 "增量gap" 将数组分为若干组,每组内进行直接插入排序(预排序);逐渐缩小gap,重复预排序;当gap=1时,数组已接近有序,执行最后一次直接插入排序 ,大幅减少移动次数。

2.2.2 实现思路与代码

  • 如何选择gap?常用gap = gap / 3 + 1(确保最后gap=1);每组内的插入逻辑与直接插入一致,仅将 "相邻元素" 改为 "间隔gap的元素"。
c 复制代码
void ShellSort(int* a, int n) 
{
    int gap = n;
    // 缩小gap,直到gap=1
    while (gap > 1) 
    {
        gap = gap / 3 + 1; // 增量计算,保证最后gap=1
        // 遍历每组的待插入元素
        for (int i = 0; i < n - gap; i++) 
        {
            int end = i;
            int tmp = a[end + gap]; // 保存待插入元素(间隔gap)
            
            // 组内从后向前比较移动
            while (end >= 0) 
            {
                if (a[end] > tmp) 
                {
                    a[end + gap] = a[end];
                    end -= gap;
                } 
                else 
                {
                    break;
                }
            }
            a[end + gap] = tmp; // 组内插入
        }
    }
}

2.2.3 特性总结

  • 时间复杂度 :难以精确计算,通常认为是( O ( N 1.3 ) ∼ O ( N 2 ) O(N^{1.3}) \sim O(N^2) O(N1.3)∼O(N2))(依赖gap序列);
  • 空间复杂度 :( O ( 1 ) O(1) O(1));
  • 稳定性:不稳定(分组排序可能打乱相同元素次序);
  • 适用场景:中大规模数据,比直接插入排序效率更高。

三、选择排序类算法

选择排序的核心思想是 "每次从待排序列中选出最值元素,放到指定位置",实现简单但效率较低。

3.1 直接选择排序

3.1.1 基本思想

同时查找待排序列的最大值最小值,将最小值放到序列起始位置最大值放到末尾位置,缩小待排范围;重复此过程,直到序列有序。

3.1.2 实现思路与代码

  • beginend双指针限定待排范围minimaxi记录最值下标;需注意特殊情况 :若begin是最大值下标(如begin=maxi),交换minibegin后,maxi需更新为mini(原最小值位置),避免后续交换错误。
c 复制代码
// 交换辅助函数
void Swap(int* a, int* b) 
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void SelectSort(int* a, int n) 
{
    int begin = 0, end = n - 1;
    // 待排范围缩小至begin >= end时结束
    while (begin < end) 
    {
        int mini = begin, maxi = begin;
        // 遍历待排范围,找最值下标
        for (int i = begin; i <= end; i++) 
        {
            if (a[i] > a[maxi]) 
            {
                maxi = i;
            }
            if (a[i] < a[mini]) 
            {
                mini = i;
            }
        }
        // 处理maxi与begin重合的情况
        if (begin == maxi) 
        {
            maxi = mini;
        }
        // 最小值放begin,最大值放end
        Swap(&a[mini], &a[begin]);
        Swap(&a[maxi], &a[end]);
        
        begin++;
        end--;
    }
}

3.1.3 特性总结

  • 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均 / 最好,均需遍历找最值);
  • 空间复杂度 :( O ( 1 ) O(1) O(1));
  • 稳定性:不稳定(如序列[5,8,5,2],第一次交换后第一个 5 会到末尾);
  • 适用场景:数据量小,对效率要求不高的场景。

3.2 堆排序

堆排序是选择排序的优化,基于堆(完全二叉树)的特性:堆顶元素为最值。排升序需建大堆(每次选最大元素放末尾),排降序建小堆;核心操作是AdjustDown(向下调整),具体实现可参考二叉树章节,此处简要总结特性:

二叉树详细讲解传送门 :👉《二叉树的人生:选择左还是右,这是个问题|我的 3 天二叉树小记》

  • 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(建堆( O ( N ) O(N) O(N)),调整( O ( N l o g N ) O(NlogN) O(NlogN));
  • 空间复杂度 :( O ( 1 ) O(1) O(1));
  • 稳定性:不稳定;
  • 适用场景:中大规模数据,需高效排序且空间有限。

四、交换排序类算法

交换排序的核心思想是 "通过比较交换,使关键字大的元素向后移动、小的向前移动",代表算法为冒泡排序和快速排序。

4.1 冒泡排序

4.1.1 基本思想

相邻元素两两比较,若逆序则交换,使较大元素逐渐 "沉底";引入exchange标志,若某趟无交换,说明序列已有序,提前退出,优化效率。

4.1.2 实现思路与代码

  • 外层循环控制排序趟数,内层循环比较相邻元素;每趟后最大元素已在末尾,内层循环范围可缩小(j < n - i - 1);exchange标志避免无效循环。
c 复制代码
void BubbleSort(int* a, int n) 
{
    int exchange = 0;
    // 外层循环:最多n趟(实际可能更少)
    for (int i = 0; i < n; i++) 
    {
        exchange = 0;
        // 内层循环:比较相邻元素,大的沉底
        for (int j = 0; j < n - i - 1; j++) 
        {
            if (a[j] > a[j + 1]) 
            {
                Swap(&a[j], &a[j + 1]);
                exchange = 1; // 标记有交换
            }
        }
        if (exchange == 0) 
        {
            break; // 无交换,序列有序,退出
        }
    }
}

4.1.3 特性总结

  • 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均),( O ( N ) O(N) O(N))(最好,已有序);空间复杂度:( O ( 1 ) O(1) O(1));
  • 稳定性:稳定(仅相邻元素交换,不打乱相同元素次序);
  • 适用场景:小规模数据或已接近有序的数据。

4.2 快速排序

快速排序是 Hoare 于 1962 年提出的高效算法,基于分治法:选基准值分割序列为 "左小右大" 的子序列,递归处理子序列,直至有序。

4.2.1 Hoare 版本(左右指针法)

实现思路与代码

  • 选左端点为基准值(keyi),left(左指针)从左向右找比基准大的元素,right(右指针)从右向左找比基准小的元素,交换二者;循环至left > right,最后将基准值与right位置元素交换,使基准归位(right位置元素≤基准);递归处理左(left ~ right-1)右(right+1~right)子序列。
c 复制代码
void PartSort1(int* a, int left, int right)
{
	assert(a);
	if (left >= right) return;  //递归返回条件

	int begin = left, end = right;
	int midi = FindMidi(a, left, (left + right) / 2, right);
	Swap(&a[left], &a[midi]);  //找到中间值与left交换元素
	int key = left;

	while (begin < end)
	{
		while (begin < end && a[end] >= a[key])
		{
			end--;
		}
		while (begin < end && a[begin] < a[key])
		{
			begin++;
		}
		if (begin < end)
		{
			Swap(&a[begin], &a[end]);
		}
	}
		Swap(&a[begin], &a[key]);
	
	PartSort1(a, left, begin - 1);  //左边递归
	PartSort1(a, begin + 1, right);  //右边递归
}

该算法存在一个显著缺陷:当基准元素(key)恰好是子数组中的极值(最大值或最小值)时,每次划分只能将数组分成单个元素和剩余部分,导致时间复杂度退化至 O ( N 2 ) O(N^2) O(N2)。为解决这一问题,我们采用三数取中法 进行优化:在每次递归前,选取首元素、中间元素和末元素,将三者中的中间值与首元素交换。这种策略能有效避免因key值选择不当造成的单侧递归问题。
有人可能会疑惑:当数组有序时,使用三数取中法是否会破坏原有的有序性?实际上,三数取中的交换操作只是临时性的调整。

假设对升序数组 [1,2,3,4,5] 进行快速排序,三数取中的步骤如下:

  1. 选基准:取左(1)、中(3)、右(5)的中位数(3),将其与最左元素(1)交换 → 数组变成 [3,2,1,4,5]。(这里看似 "打乱" 了前三个元素,但目的是让基准3更接近中间值,避免划分时出现 "一边倒"。)
  2. 划分(partition):将比3小的元素移到左边,比3大的移到右边 → 最终划分成 [1,2, | 3 |, 4,5]。(此时3已回到正确位置,左右子数组分别是[1,2]和[4,5],都是有序的。)
c 复制代码
//三数取中(防止key一直取当前递归的最小数:快排时间复杂度会变为O(N^2))
int FindMidi(int* a, int left, int midi, int right)
{
	if (a[left] > a[midi])
	{
		if (a[right] > a[left]) return left;
		else if (a[midi] > a[right]) return midi;
		else return right;
	}
	else  //a[left] <= a[midi]
	{
		if (a[right] < a[left]) return left;
		else if (a[right] > a[midi]) return midi;
		else return right;
	}
}

我们已经解决了key取值极端导致时间复杂度退化的问题。接下来需要处理的是栈溢出问题 ,即递归深度过大时可能引发的风险。在学习二叉树时可以看到:最底层元素占总数的50%倒数第二层占25%。因此,只要针对最后几层改用非递归方法,就能显著降低栈溢出的风险。

当递归处理的区间元素数量小于等于10时 ,我们可以改用更高效的排序方法直接完成排序。这时,插入排序就是最佳选择。

下面就是我们优化后的快排代码了:

c 复制代码
//进行三数取中
int FindMidi(int* a, int left, int midi, int right)
{
	if (a[left] > a[midi])
	{
		if (a[right] > a[left]) return left;
		else if (a[midi] > a[right]) return midi;
		else return right;
	}
	else  //a[left] <= a[midi]
	{
		if (a[right] < a[left]) return left;
		else if (a[right] > a[midi]) return midi;
		else return right;
	}
}

void PartSort1(int* a, int left, int right)
{
	assert(a);
	if (left >= right) return;  //没有元素或一个元素时直接返回
	
	//小区间优化(当递归数组元素个数小于等于10个时,直接用插入排序,防止过度递归)
	if (right - left + 1 <= 10)
	{
		InsertSort(a + left, right - left + 1);  //插入排序参数是排序的起始位置和元素个数
		return;
	}

	//不优化
	//if (left >= right) return;

	int begin = left, end = right;
	int midi = FindMidi(a, left, (left + right) / 2, right);
	Swap(&a[left], &a[midi]);  //找到中间值与left交换元素
	int key = left;

	while (begin < end)
	{
		while (begin < end && a[end] >= a[key])
		{
			end--;
		}
		while (begin < end && a[begin] < a[key])
		{
			begin++;
		}
		if (begin < end)
		{
			Swap(&a[begin], &a[end]);
		}
	}
	Swap(&a[begin], &a[key]);

	PartSort1(a, left, begin - 1);  //左边递归
	PartSort1(a, begin + 1, right);  //右边递归
}

4.2.2 挖坑法

实现思路与代码

  • 选左端点为基准值(key),形成 "坑位"hole);right从右向左找比基准小的元素,填入坑位,right成为新坑;left从左向右找比基准大的元素,填入新坑,left成为新坑;循环至left == right,将基准值填入最终坑位,基准归位;逻辑比 Hoare 版更直观,避免指针相遇的复杂判断
c 复制代码
//快排挖坑法
void PartSort2(int* a, int left, int right)
{
	assert(a);
	if (left >= right) return;  

	if (right - left + 1 >= 0)
	{
		InsertSort(a + left, right - left + 1);
		return;
	}

	int begin = left, end = right;
	int midi = FindMidi(a, left, (left + right) / 2, right);
	Swap(&a[left], &a[midi]);
	int key = a[left];

	while (begin < end)
	{
		while (begin < end && a[end] >= key)
			end--;
		a[begin] = a[end];

		while (begin < end && a[begin] < key)
			begin++;
		a[end] = a[begin];
	}

	a[begin] = key;

	PartSort2(a, left, begin - 1);
	PartSort2(a, end + 1, right);
}

4.2.3 前后指针法(Lomuto 版本)

实现思路与代码

  • prev(前指针)指向序列起始,cur(后指针)从prev+1开始;cur找比基准小的元素,若找到且++prev != cur,交换a[prev]a[cur]cur遍历结束后,交换基准值与a[prev],使基准左侧均为小于基准的元素,右侧均为大于基准的元素;逻辑简洁,易实现。

简单来说就是保证prev与cur之间全是比key大的元素,不是则交换(有些细节要自己去处理)

c 复制代码
//快排双指针法
void QuickSort(int* a, int left, int right)
{
	assert(a);
	if (left >= right) return; //递归停止条件
	//三数取中
	int keyi = FindMidi(a, left, (left + right) / 2, right);
	Swap(&a[left], &a[keyi]);

	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[left] && ++prev != cur)
			Swap(&a[cur], &a[prev]);
		cur++;
	}
	Swap(&a[prev], &a[left]);

	QuickSort(a, left, prev - 1);  //递归左边
	QuickSort(a, prev + 1, right); //递归右边
}

4.2.4 非递归版本(栈模拟递归)

实现思路与代码

  • 递归的本质是调用栈保存区间 ,非递归用栈模拟:先将初始区间(left, right)压栈(注意先压右区间,再压左区间,保证左区间先处理);循环弹出区间,分割后若子区间长度 > 1,继续压栈;直至栈空,排序完成。避免递归栈溢出(大规模数据递归深度过大)。
c 复制代码
//快速排序  非递归形式
void QuickSortNonR(int* a, int left, int right)
{
	assert(a);

	Stack ST;  
	StackInit(&ST);
	StackPush(&ST, right);
	StackPush(&ST, left);

	while (!StackEmpty(&ST))
	{
		int left = StackTop(&ST);
		StackPop(&ST);
		int right = StackTop(&ST);
		StackPop(&ST);

		int begin = left, end = right;
		int midi = FindMidi(a, left, (left + right) / 2, right);
		Swap(&a[left], &a[midi]);  //找到中间值与left交换元素
		int key = left;

		while (begin < end)
		{
			while (begin < end && a[end] >= a[key])
			{
				end--;
			}
			while (begin < end && a[begin] < a[key])
			{
				begin++;
			}
			if (begin < end)
			{
				Swap(&a[begin], &a[end]);
			}
		}
		Swap(&a[begin], &a[key]);

		//区间优化
		/*if (right - left + 1 >= 10)
		{
			InsertSort(a + left, right - left + 1);
			continue;
		}
		StackPush(&ST, right);
		StackPush(&ST, begin + 1);
		StackPush(&ST, begin - 1);
		StackPush(&ST, left);*/

		//不进行区间优化
		if (right > begin + 1)
		{
			StackPush(&ST, right);
			StackPush(&ST, begin + 1);
		}
		if (begin - 1 > left)
		{
			StackPush(&ST, begin - 1);
			StackPush(&ST, left);
		}
	}

	StackDestory(&ST);
}

4.2.5 特性总结

  • 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(平均 / 最好),( O ( N 2 ) O(N^2) O(N2))(最坏,序列已有序,基准选端点,还不进行三数取中);
  • 空间复杂度 :( O ( l o g N ) O(logN) O(logN))(递归栈,非递归栈),最坏( O ( N ) O(N) O(N));
  • 稳定性:不稳定(基准交换可能打乱相同元素次序);
  • 适用场景:中大规模数据,是实际应用中效率最高的排序算法之一。

五、归并排序

归并排序是分治法的典型应用,核心是 "先分解、后合并 ",确保排序稳定但需额外空间

5.1 基本思想

将序列分解为若干长度为 1 的子序列(天然有序),然后两两合并为有序子序列,重复合并过程,直至形成完整有序序列;合并时需借助临时数组,避免原位修改覆盖数据。

5.2 实现思路与代码

  • 递归分解:将区间[left, right]分为[left, mid][mid+1, right],递归处理子区间;合并有序子序列:用双指针begin1(左子区间)和begin2(右子区间),比较元素大小,依次存入临时数组tmp,最后将tmp中合并结果拷贝回原数组a
c 复制代码
//采用后序遍历
void _MergeSort(int* a,int left, int right, int* temp)
{
	if (left >= right) return;

	int midi = (left + right) / 2;
	//[begin1, end1 = midi(上一个区间的midi)] [begin2 = midi + 1, end2] 
	_MergeSort(a,left, midi, temp);
	_MergeSort(a, midi + 1, right, temp);

	int begin1 = left, end1 = midi;
	int begin2 = midi + 1, end2 = right;

	int i = 0;  //归并的每个小区间都是从temp[0]开始的
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
			temp[i++] = a[begin1++];
		else
			temp[i++] = a[begin2++];
	}
	//处理两个区间未归并的元素
	while (begin1 <= end1) temp[i++] = a[begin1++];
	while (begin2 <= end2) temp[i++] = a[begin2++];

	memcpy(a + left, temp, sizeof(int) * i);  //将归并好的区间直接覆盖到原区间(临时空间里面的有效元素)
}

//归并排序 递归版
//稳定
void MergeSort(int* a, int n) //这里的n指的是最后一个元素的下标
{
	assert(a);
	int* temp = (int*)malloc(sizeof(int) * (n + 1));
	_MergeSort(a, 0, n, temp);
	
	free(temp);
	temp = NULL;
}

5.3 特性总结

  • 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(分解与合并均为( O ( N l o g N ) ) O(NlogN)) O(NlogN)));
  • 空间复杂度 :( O ( N ) O(N) O(N))(临时数组tmp);
  • 稳定性:稳定(合并时相等元素按左子区间优先,保持相对次序);
  • 适用场景:需稳定排序、数据量较大的场景(如外部排序,数据无法一次性加载到内存)。

当然,这也有对应的非递归实现,感兴趣的话可以深入了解一下。

c 复制代码
//归并排序  非递归版
void MergeSortNonR(int* a, int n) //n指的是最后一个元素的下标
{
	assert(a);
	int* temp = (int*)malloc(sizeof(int) * (n + 1));
	if (temp == NULL)
	{
		perror("malloc fail:");
		return;
	}

	int gap = 1;
	while (gap <= n)
	{
		for (int i = 0; i <= n; i += gap * 2)
		{
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = i + gap, end2 = begin2 + gap - 1;

			//处理begin2或end1越界的情况
			if (begin2 > n)
				continue;
			//处理end2越界的情况
			if (end2 > n) 
				end2 = n;
			

			int j = 0;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (begin2 <= n && a[begin1] <= a[begin2])
					temp[j++] = a[begin1++];
				else
					temp[j++] = a[begin2++];
			}
			//处理两个区间未归并的元素
			while (begin1 <= end1) temp[j++] = a[begin1++];
			while (begin2 <= end2) temp[j++] = a[begin2++];

			//将归并的元素拷贝到原数组中
			memcpy(a + i, temp, sizeof(int) * j);
		}
		gap *= 2;
	}
	free(temp);
	temp = NULL;
}

六、非比较排序:计数排序

非比较排序不依赖关键字比较,基于 "鸽巢原理",适用于数据范围集中的场景。

6.1 基本思想

  1. 统计待排序列中每个元素的出现次数;
  2. 根据次数将元素 "回收" 到原数组,实现排序;
  3. 若数据范围过大(如[100, 109]),用max - min + 1计算范围(range),避免空间浪费。

6.2 实现思路与代码

  • 先遍历数组找maxmin,计算range;申请count数组统计次数(count[a[i]-min]++,将数据映射到[0, range-1]);最后遍历count数组,按次数将元素填回原数组(a[j++] = i + min)。
c 复制代码
#include <string.h> // 用于memset

void CountSort(int* a, int n) 
{
    if (n <= 1) 
    {
        return; // 无需排序
    }
    // 1. 找max和min,确定数据范围
    int min = a[0], max = a[0];
    for (int i = 1; i < n; i++) 
    {
        if (a[i] > max) 
        {
            max = a[i];
        }
        if (a[i] < min) 
        {
            min = a[i];
        }
    }
    int range = max - min + 1;
    
    // 2. 申请count数组,统计次数
    int* count = (int*)malloc(sizeof(int) * range);
    if (count == NULL) 
    {
        perror("malloc fail");
        return;
    }
    memset(count, 0, sizeof(int) * range); // 初始化count为0
    
    for (int i = 0; i < n; i++) 
    {
        count[a[i] - min]++; // 数据映射到count下标
    }
    
    // 3. 按次数回收元素到原数组
    int j = 0;
    for (int i = 0; i < range; i++) 
    {
        while (count[i]--) 
        {
            a[j++] = i + min; // 映射回原数据
        }
    }
    free(count);
}

6.3 特性总结

  • 时间复杂度 :( O ( N + r a n g e ) O(N + range) O(N+range))(N为数据量,range为数据范围);
  • 空间复杂度 :( O ( r a n g e ) O(range) O(range));
  • 稳定性:稳定(按统计顺序回收,相同元素相对次序不变);
  • 适用场景:数据范围小且集中的场景(如学生成绩、年龄排序)。

七、排序算法复杂度与稳定性汇总

八、总结

选择排序算法需结合数据规模、有序程度、稳定性需求综合判断:

  • 小规模数据 / 接近有序:直接插入排序、冒泡排序;
  • 中大规模数据:快速排序(优先)、堆排序、归并排序;
  • 数据范围集中:计数排序;
  • 需稳定排序:归并排序、计数排序、直接插入排序;
  • 空间有限:堆排序、希尔排序(避免归并 / 计数的额外空间)。

掌握不同算法的核心逻辑与适用场景,才能在实际开发中选择最优方案,同时为后续复杂数据结构(如平衡二叉树、哈希表)的学习奠定基础。

相关推荐
在繁华处4 小时前
C语言初步学习:数组的增删查改
c语言·数据结构·学习
Cx330❀4 小时前
《C++ 手搓list容器底层》:从结构原理深度解析到功能实现(附源码版)
开发语言·数据结构·c++·经验分享·算法·list
仰泳的熊猫4 小时前
LeetCode:98. 验证二叉搜索树
数据结构·c++·算法·leetcode
杨福瑞4 小时前
C语言数据结构:算法复杂度(1)
c语言·开发语言·数据结构
淘晶驰AK5 小时前
主流的 MCU 开发语言为什么是 C 而不是 C++?
c语言·开发语言·单片机
胖咕噜的稞达鸭5 小时前
算法入门:专题二---滑动窗口(长度最小的子数组)更新中
c语言·数据结构·c++·算法·推荐算法
深盾科技8 小时前
C/C++逆向分析实战:变量的奥秘与安全防护
c语言·c++·安全
_OP_CHEN11 小时前
C++基础:(十二)list类的基础使用
开发语言·数据结构·c++·stl·list类·list核心接口·list底层原理
奔跑吧邓邓子14 小时前
【C语言实战(8)】C语言循环结构(do-while):解锁编程新境界
c语言·实战·do-while