> 技术文档 > 数据结构----八大排序算法

数据结构----八大排序算法

目录

插入排序

直接插入排序

代码实现:

核心思想:

时间(空间)复杂度

时间复杂度

空间复杂度

希尔排序(又称缩小增量法)

核心思想:

代码实现:

时间(空间)复杂度

选择排序

直接选择排序

核心思想:

实现代码:

时间复杂度与空间复杂度

特点:

堆排序

代码实现:

排序原理:

特点:

交换排序

冒泡排序

代码实现:

特点(复杂度):

快速排序

代码实现1(hoare版本):

思路:

注意:

代码实现2(挖坑法):

思路:

代码实现3(lomuto前后指针):

思路:

非递归版本的快速排序(借助数据结构---栈)

代码实现:

优点:

复杂度:

时间复杂度

空间复杂度

归并排序

算法思想:

代码实现:

复杂度:

时间复杂度

空间复杂度

稳定性

计数排序

代码实现:

时间复杂度

空间复杂度

特点:

排序比较:

测试代码(不运行):


插入排序

把待排序的记录按其关键码值的大小逐个插入到⼀个已经排好序的有序序列中,直到所有的记录插入完为止,得到⼀个新的有序序列 。

直接插入排序

代码实现:
#include//直接插入排序void InsertSort(int* arr, int n){for (int i = 0; i = 0){if (tmp <arr[end]){arr[end+1] = arr[end];arr[end] = tmp;end--;}else {break;}}arr[end + 1] = tmp;}}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);InsertSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test();return 0;}

可简化:

// 修正后的直接插入排序void InsertSort(int* arr, int n){ for (int i = 0; i = 0 && tmp < arr[end]) { arr[end + 1] = arr[end]; // 元素后移,给tmp腾位置 end--; } arr[end + 1] = tmp; // 将tmp插入到正确位置 }}
  1. 移除了 while 循环中的 arr[end] = tmp; 交换操作,仅保留 arr[end + 1] = arr[end]; 实现元素后移。
  2. 循环结束后,通过 arr[end + 1] = tmp; 一次性将暂存的 tmp 插入到正确位置,符合直接插入排序的逻辑。

核心思想:

将待插入元素 tmp 暂存,然后将大于 tmp 的元素依次后移,最后将 tmp 插入到正确位置。

时间(空间)复杂度

时间复杂度
  • 最佳情况O(n)
    当数组已经完全有序时,每次插入元素只需与已排序序列的最后一个元素比较一次(无需移动元素),总比较次数为 n-1,时间复杂度为线性级。

  • 最坏情况O(n²)
    当数组完全逆序时,每次插入第 i 个元素(从第 2 个元素开始),需要与已排序序列的 i-1 个元素全部比较,并将它们依次后移。总操作次数为 1+2+3+...+(n-1) = n(n-1)/2,时间复杂度为平方级。

  • 平均情况O(n²)
    对于随机无序的数组,每次插入元素的平均比较和移动次数约为 n/2,总操作次数约为 n²/4,时间复杂度仍为平方级。

空间复杂度

O(1)(原地排序),且排序稳定

希尔排序(又称缩小增量法)

先选定⼀个整数(通常是 gap = n/3+1 ),把待排序⽂件所有记录分成各组,所有的距离相等的记录分在同⼀组内,并对每⼀组内的记录进行排序,然后gap=gap/3+1得到下⼀个整数,再将数组分成各组,进行插⼊排序,当gap=1时,就相当于直接插⼊排序。

核心思想:

先将整个数组分割成若干个子序列,对每个子序列进行插入排序;逐渐缩小分组间隔,重复排序过程;最后当间隔为 1 时,进行一次完整的插入排序,此时数组已基本有序,效率极高

代码实现:
#include//希尔排序void ShellSort(int* arr, int n){int gap = n;while (gap > 1){gap = gap / 3 + 1;//对每组数据插入排序for (int i = 0; i = 0 && tmp < arr[end]){arr[end + gap] = arr[end];end-=gap;}arr[end + gap] = tmp;}}}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);ShellSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

时间(空间)复杂度

希尔排序的时间复杂度取决于增量序列的选择,目前尚未有严格的数学证明,常见分析如下:

  • 若使用增量序列 n/2, n/4, ..., 1,时间复杂度约为 O(n²)(最坏情况);
  • 若使用更优的增量序列(如 Hibbard 序列 2^k - 1),时间复杂度可降至 O(n^(3/2)) 或更低。

总体而言,希排序的效率远高于直接插入排序,尤其适合中大型数组。

空间复杂度:o(1)

选择排序

直接选择排序

核心思想:

每次从待排序的元素中找到最小(或最大)的元素,将其与待排序部分的第一个元素(最后一个)交换位置,逐渐缩小待排序范围,直到整个数组有序。

实现代码:

#include//直接选择排序void SelectSort(int* arr, int n){int begin = 0;int end = n - 1;while (begin < end){int maxi = begin;int mini = begin;for (int i = begin + 1; i <= end; i++){if (arr[i]  arr[maxi]){maxi = i;}}if (maxi == begin){maxi = mini;}Swap(&arr[mini], &arr[begin]);Swap(&arr[maxi], &arr[end]);begin++;end--;}}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);SelectSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

时间复杂度与空间复杂度

  • 时间复杂度
    无论数组是否有序,都需要遍历待排序部分寻找最小元素,因此最佳、最坏、平均情况均为 O (n²)

  • 空间复杂度
    仅需常数级额外空间(用于交换元素),因此为 O(1)(原地排序)。

特点:
  1. 不稳定性
    当待排序部分存在多个相同元素时,交换可能改变它们的相对位置。例如 [3, 2, 3, 1] 中,第一个 3 会与 1 交换,导致两个 3 的相对位置反转。

  2. 交换次数少
    每轮排序只需要一次交换(找到最小元素后),总交换次数为 n-1 次,远少于冒泡排序。

  3. 适用场景
    适合小规模数据排序,或对交换操作成本较高的场景(因交换次数少)。

堆排序

代码实现:

#pragma once#include#include#include#include//二叉树结构typedef int HPDataType;typedef struct heap{HPDataType* arr;int size; //有效数据个数int capacity; //空间大小}HP;//交换void Swap(int* x, int* y){int tmp = *x;*x = *y;*y = tmp;}//向下调整算法void AdjustDown(HPDataType* arr, int parent, int n){int child = parent * 2 + 1;//左孩子while (child < n){//大堆:if (child + 1 < n && arr[child] //小堆: arr[parent]){//调整Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else {break;}}}void HeapSort(int* arr, int n){//建堆---向下调整法----这里是建大堆for (int i = (n - 2) / 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--;}}
  • 排序原理:
    1. 先将无序数组构建成大堆(堆顶为最大值)
    2. 反复将堆顶最大值与未排序部分的末尾元素交换
    3. 调整剩余元素保持堆结构,直到所有元素排序完成
特点:
  • 时间复杂度:O (n log n)(建堆 O (n) + 排序过程 O (n log n))
  • 空间复杂度:O (1)(原地排序)
  • 稳定性:不稳定排序(交换过程可能改变相等元素的相对位置)

交换排序

冒泡排序

代码实现:
#include//冒泡排序void BubbleSort(int* arr, int n){for (int i = 0; i < n; i++){for (int j = 0; j  arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//HeapSort(a, n);BubbleSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

可升级

// 冒泡排序(升序)void BubbleSort(int* arr, int n) { // 控制排序的轮数 for (int i = 0; i < n - 1; i++) { int flag = 0; // 标记本轮是否发生交换 // 每轮比较相邻元素,将较大的元素\"冒\"到后面 for (int j = 0; j  arr[j + 1]) { Swap(&arr[j], &arr[j + 1]); flag = 1; // 发生了交换 } } // 如果本轮没有交换,说明数组已经有序,提前退出 if (flag == 0) { break; } }}

用flag标记,避免过度浪费时间

特点(复杂度):

  • 时间复杂度
    • 最坏情况(完全逆序):O (n²)
    • 最好情况(已经有序):O (n)(因为有 flag 优化)
    • 平均情况:O (n²)
  • 空间复杂度:O (1),只需要常数级的额外空间
  • 稳定性:稳定排序,相等元素的相对位置不会改变
  • 效率较低,适合小规模数据的排序场景

快速排序

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

代码实现1(hoare版本):

#include//hoare版本--找基准值int _QuickSort(int* arr, int left, int right){int keyi = left;++left;while (left <= right){//right:从右往左走,找比基准值小的while (left  arr[keyi]){right--;}while (left <= right && arr[left] < arr[keyi]){left++;}if (left = right){return;}//找基准值int keyi = _QuickSort(arr, left, right);QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//HeapSort(a, n);//BubbleSort(a, n);QuickSort(a,0,n-1);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

思路:

  • 右指针逻辑:从右侧开始,跳过所有大于基准值的元素,直到找到第一个小于基准值的元素(或指针交叉)。
  • 左指针逻辑:从左侧开始,跳过所有小于基准值的元素,直到找到第一个大于基准值的元素(或指针交叉)。
  • 交换操作:当左右指针都找到目标元素且未交叉时,交换这两个元素,确保小元素在左、大元素在右,然后指针继续移动。
  • left > right时,循环结束,right的位置就是基准值的正确位置(此时right左侧全是小于基准值的元素,右侧全是大于基准值的元素)。
  • 交换基准值(初始在keyi)和right位置的元素,完成基准值归位。
  • 返回right(基准值最终索引),用于后续递归排序基准值左侧和右侧的子数组。

注意:

找基准值处,判断条件中,不能取等号

代码实现2(挖坑法):

​#include// 交换函数// void Swap(int* a, int* b) {// int tmp = *a;// *a = *b;// *b = tmp;// }// 挖坑法求基准值位置int _QuickSort(int* arr, int left, int right){ int hole = left; // 初始坑位置(基准值位置) int keyi = arr[hole]; // 缓存基准值 while (left < right) { // 右指针找小于基准值的元素,填左坑 while (left = keyi) { --right; } arr[hole] = arr[right]; hole = right;  // 更新坑位置 // 左指针找大于基准值的元素,填右坑 while (left < right && arr[left] = right) { return; } int keyi = _QuickSort(arr, left, right); QuickSort(arr, left, keyi - 1); // 排序左半部分 QuickSort(arr, keyi + 1, right); // 排序右半部分}// 打印数组void arrprint(int* arr, int n){ for (int i = 0; i < n; i++) { printf(\"%d \", arr[i]); } printf(\"\\n\");}// 测试函数void test01(){ int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 }; int n = sizeof(a) / sizeof(a[0]); printf(\"排序之前:\"); arrprint(a, n); // 输出:5 3 9 6 2 4 7 1 8 QuickSort(a, 0, n - 1); printf(\"排序之后:\"); arrprint(a, n); // 输出:1 2 3 4 5 6 7 8 9 }int main(){ test01(); return 0;}​

思路:

  • 核心思想:通过 \"挖坑 - 填坑\" 的方式实现分区。初始将基准值位置视为 \"坑\",然后从右向左找小于基准值的元素填左坑,再从左向右找大于基准值的元素填右坑,最终将基准值填入最后一个坑中。

代码实现3(lomuto前后指针):

思路:

创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

#include // 交换函数void Swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp;}// Lomuto前后指针法分区(返回基准值位置)int _QuickSort(int* arr, int left, int right) { int keyi = left;  // 基准值位置(最左元素) int prev = left;  // 前指针 int cur = prev + 1; // 后指针 while (cur <= right) { // 找到小于基准值的元素,移动到左侧区域 if (arr[cur] = right) { return;  // 区间为空或单个元素,无需排序 } // 分区并获取基准值位置 int keyi = _QuickSort(arr, left, right); // 递归排序左右子区间 QuickSort(arr, left, keyi - 1); QuickSort(arr, keyi + 1, right);}// 打印数组void arrprint(int* arr, int n) { for (int i = 0; i < n; i++) { printf(\"%d \", arr[i]); } printf(\"\\n\");}// 测试int main() { int a[] = {5, 3, 9, 6, 2, 4, 7, 1, 8}; int n = sizeof(a) / sizeof(a[0]); printf(\"排序前:\"); arrprint(a, n); QuickSort(a, 0, n - 1); printf(\"排序后:\"); arrprint(a, n); // 输出:1 2 3 4 5 6 7 8 9 return 0;}

核心逻辑:

  • 核心逻辑cur 逐个扫描元素,当遇到小于基准值的元素时,先将 prev 右移(扩大 “小于区域”),如果此时 prev 和 cur 不在同一位置(说明中间有大于基准值的元素),则交换两者的元素,确保小元素被纳入左侧区域。
  • 例如:若数组为 [5,3,9,6,2](基准值 5),当 cur 指向 3(小于 5)时,prev 右移到索引 1,与 cur 重合,不交换;当 cur 指向 2(小于 5)时,prev 右移到索引 2,与 cur(索引 4)不重合,交换后数组变为 [5,3,2,6,9],左侧区域正确包含小于 5 的元素。
  • 遍历结束后,prev 指向 “小于基准值区域” 的最后一个元素,将基准值(初始在keyi)与 prev 位置的元素交换,此时基准值左侧全是小于它的元素,右侧全是大于等于它的元素。
  • 返回 prev(基准值的最终索引),用于外层递归排序左侧 [left, prev-1] 和右侧 [prev+1, right] 区间。

非递归版本的快速排序(借助数据结构---栈)

代码实现:
#include#include#include#include//定义栈的结构typedef int STDataType;typedef struct Stack{STDataType* arr;int top;//有效数据个数int capacity;//容量}ST;// 交换函数void Swap(int* a, int* b){ int temp = *a; *a = *b; *b = temp;}//初始化void StackInit(ST* ps){assert(ps);ps->arr = NULL;ps->top = ps->capacity = 0;}// 销毁栈void StackDestroy(ST* ps){ assert(ps); free(ps->arr); ps->arr = NULL; ps->top = ps->capacity = 0;}//入栈----栈顶void StackPush(ST* ps, STDataType x){if (ps->top == ps->capacity){//增容int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));if (tmp == NULL){perror(\"realloc\");exit(1);}//空间申请成功ps->arr = tmp;ps->capacity = newCapacity; }ps->arr[ps->top++] = x;}//栈是否为空bool StackEmpty(ST* ps){assert(ps);return ps->top == 0;}//出栈----栈顶void StackPop(ST* ps){assert(!StackEmpty(ps));--ps->top;}//出栈顶数据,元素不会删除STDataType StackTop(ST* ps){assert(!StackEmpty(ps));return ps->arr[ps->top - 1];}//获取栈中有效元素个数int StackSize(ST* ps){return ps->top;}//非递归版本的快速排序——栈void QuickSortNoR(int* arr, int left, int right){ST st;StackInit(&st);StackPush(&st, left);StackPush(&st, right);while (!StackEmpty(&st)){//取栈顶两次int end = StackTop(&st);StackPop(&st);int begin = StackTop(&st);StackPop(&st);//[begin,end]找基准值int keyi = begin;int prev = begin, cur = prev + 1;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[keyi], &arr[prev]);keyi = prev;//begin keyi end//左序列:[begin,keyi-1] 右序列:[keyi+1,end];if (keyi + 1 < end){StackPush(&st, keyi + 1);StackPush(&st, end);}if (begin < keyi - 1){StackPush(&st, begin);StackPush(&st, keyi - 1);}}StackDestroy(&st);}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//HeapSort(a, n);//BubbleSort(a, n);//QuickSort(a,0,n-1);QuickSortNoR(a, 0, n - 1);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}
优点:

通过栈数据结构模拟了递归调用的过程,避免了递归版本可能出现的栈溢出问题,同时保持了快速排序的时间复杂度特性

复杂度:
时间复杂度
  • 平均情况:O(n log n)

    • 每次划分操作将数组分成两部分,大约需要 O (n) 时间
    • 整个排序过程需要 log n 层划分(类似二叉树的高度)
    • 总时间复杂度为 O (n log n)
  • 最坏情况:O(n²)

    • 当数组已经有序或接近有序时,每次划分可能将数组分成极不平衡的两部分(例如一个部分只有 1 个元素,另一个部分有 n-1 个元素)
    • 此时需要 n 层划分,总时间复杂度退化为 O (n²)
    • 可以通过合理选择基准值(如三数取中法)来避免这种情况
空间复杂度
  • 平均情况:O(log n)

    • 栈中最多存储 log n 个区间信息(对应递归版本的递归调用栈深度)
    • 额外空间主要用于栈存储
  • 最坏情况:O(n)

    • 在极端情况下,栈中可能需要存储 n 个区间信息
    • 与递归版本的最坏情况空间复杂度相同

归并排序

算法思想:

建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide and Conquer)的⼀个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。

代码实现:

#include#include//非递归版本的快速排序——栈void QuickSortNoR(int* arr, int left, int right){ST st;StackInit(&st);StackPush(&st, left);StackPush(&st, right);while (!StackEmpty(&st)){//取栈顶两次int end = StackTop(&st);StackPop(&st);int begin = StackTop(&st);StackPop(&st);//[begin,end]找基准值int keyi = begin;int prev = begin, cur = prev + 1;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}cur++;}Swap(&arr[keyi], &arr[prev]);keyi = prev;//begin keyi end//左序列:[begin,keyi-1] 右序列:[keyi+1,end];if (keyi + 1 < end){StackPush(&st, keyi + 1);StackPush(&st, end);}if (begin = right){return;}//left rightint mid = (left + right) / 2;//根据mid划分两个区间[left,mid],[mid+1,right]_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right, tmp);//合并两个有序序列int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = begin1;while (begin1 <= end1 && begin2<=end2){if (arr[begin1] < arr[begin2]){tmp[index++] = arr[begin1++];}else {tmp[index++] = arr[begin2++];}}while (begin1 <= end1){tmp[index++] = arr[begin1++];}while (begin2 <= end2){tmp[index++] = arr[begin2++];}//将tmp中有序数组导入 arr 数组for (int i = left; i <= right; i++){arr[i] = tmp[i];}}void MergeSort(int* arr, int n){if (arr == NULL || n <= 1)return;int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){printf(\"malloc fail\");exit(1);}_MergeSort(arr, 0, n - 1, tmp);free(tmp);tmp =NULL;}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//HeapSort(a, n);//BubbleSort(a, n);//QuickSort(a,0,n-1);//QuickSortNoR(a, 0, n - 1);MergeSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

复杂度:

时间复杂度

  • 最好情况:O(n log n)
  • 平均情况:O(n log n)
  • 最坏情况:O(n log n)

归并排序的时间复杂度非常稳定,无论输入数据的分布如何(有序、逆序或随机),时间复杂度始终是 O (n log n)。这是因为:

  1. 归并排序采用分治策略,将数组不断二分,直到子数组长度为 1,这个过程需要 log n 层(类似二叉树的高度)
  2. 每一层都需要对所有元素进行一次合并操作,总耗时为 O (n)
  3. 因此总时间复杂度为 O (n log n)

空间复杂度

  • 空间复杂度:O(n)

归并排序需要额外的空间来存储合并过程中的临时数组(tmp 数组),其大小与原数组相同,因此空间复杂度为 O (n)。

此外,递归实现的归并排序还需要考虑递归调用栈的空间,递归深度为 log n,所以递归栈的空间复杂度为 O (log n)。但通常在分析归并排序的空间复杂度时,主要考虑的是临时数组的开销,因此整体空间复杂度仍记为 O (n)。

稳定性

归并排序是一种稳定的排序算法,在合并过程中,当两个元素相等时,会优先保留前面的元素,不会改变它们的相对顺序。

计数排序

代码实现:

#include#include//计数排序void CountSort(int* arr, int n){//找最大和最小值int min = arr[0], max = arr[0];for (int i = 1; i < n; i++){if (arr[i]  max){max = arr[i];}}//max-min+1确定count数组的大小int range = max - min + 1;int* count = (int*)malloc(sizeof(int) * range);if (count == NULL){perror(\"malloc fail!\");exit(1);}//初始化用callocmemset(count, 0, sizeof(int) * range);for (int i = 0; i < n; i++){count[arr[i] - min]++;}//将count数组还原到原数组中,使其有序int index = 0;for (int i = 0; i < range; i++){while (count[i]--){arr[index++] = i + min;}}}void arrprint(int* arr, int n){for (int i = 0; i < n; i++){printf(\"%d \", arr[i]);}printf(\"\\n\");}void test01(){int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };int n = sizeof(a) / sizeof(a[0]);printf(\"排序之前:\");arrprint(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//HeapSort(a, n);//BubbleSort(a, n);//QuickSort(a,0,n-1);//QuickSortNoR(a, 0, n - 1);//MergeSort(a, n);CountSort(a, n);printf(\"排序之后:\");arrprint(a, n);}int main(){test01();return 0;}

时间复杂度

  • 最好情况:O(n + range)
  • 平均情况:O(n + range)
  • 最坏情况:O(n + range)

其中,n是数组元素个数,range是最大值与最小值的差值加 1(即max - min + 1)。

时间复杂度由三部分组成:

  1. 查找最大值和最小值:O (n)
  2. 统计元素出现次数:O (n)
  3. 从计数数组还原到原数组:O (range)

空间复杂度

  • 空间复杂度:O(range)

主要开销是计数数组count所占用的空间,大小为range

特点:

  1. 非比较排序:不通过元素间的比较来排序,而是利用元素的数值特性
  2. 稳定性:在还原数组时保持相同元素的相对顺序,是稳定的排序算法
  3. 适用场景:适合数值范围较小且为整数的数组排序,当range远小于n时效率很高
  4. 局限性:不适合浮点数或数值范围极大的整数排序

排序比较:

测试代码(不运行):

// 测试排序的性能对⽐void TestOP(){srand(time(0));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);int* a3 = (int*)malloc(sizeof(int) * N);int* a4 = (int*)malloc(sizeof(int) * N);int* a5 = (int*)malloc(sizeof(int) * N);int* a6 = (int*)malloc(sizeof(int) * N);int* a7 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];a3[i] = a1[i];a4[i] = a1[i];a5[i] = a1[i];a6[i] = a1[i];a7[i] = a1[i];}int begin1 = clock();InsertSort(a1, N);int end1 = clock();int begin2 = clock();ShellSort(a2, N);int end2 = clock();int begin3 = clock();SelectSort(a3, N);int end3 = clock();int begin4 = clock();HeapSort(a4, N);int end4 = clock();int begin5 = clock();QuickSort(a5, 0, N - 1);int end5 = clock();int begin6 = clock();MergeSort(a6, N);int end6 = clock();int begin7 = clock();BubbleSort(a7, N);int end7 = clock();printf(\"InsertSort:%d\\n\", end1 - begin1);printf(\"ShellSort:%d\\n\", end2 - begin2);printf(\"SelectSort:%d\\n\", end3 - begin3);printf(\"HeapSort:%d\\n\", end4 - begin4);printf(\"QuickSort:%d\\n\", end5 - begin5);printf(\"MergeSort:%d\\n\", end6 - begin6);printf(\"BubbleSort:%d\\n\", end7 - begin7);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);}int main(){//test01();TestOP();return 0;}