C 语言中常见排序算法

常见排序算法(C 语言实现)—— 原理、代码与复杂度分析

文档说明

本文档整理了冒泡排序、插入排序、选择排序、希尔排序、快速排序、桶排序、基数排序七种经典排序算法的原理说明C 语言实现代码复杂度分析及核心特性,便于学习和查阅。

目录

  1. 冒泡排序

  2. 插入排序

  3. 选择排序

  4. 希尔排序

  5. 快速排序

  6. 桶排序

  7. 基数排序


1. 冒泡排序

1.1 原理说明

冒泡排序是最基础的交换排序算法,核心思想是:

  • 将数组分为无序区和有序区,重复遍历无序区,每次比较相邻两个元素,若顺序错误则交换;

  • 每一轮遍历会将无序区中最大的元素 “冒泡” 到无序区的末尾(有序区的开头);

  • 优化:若某一轮遍历未发生任何交换,说明数组已完全有序,可提前终止。

1.2 C 语言实现代码

#include <stdio.h>

void bubbleSort(int arr[], int n) {

   int i, j, temp;

   int swapped; // 标记是否发生交换,用于优化

  

   for (i = 0; i < n-1; i++) {

       swapped = 0;

       for (j = 0; j < n-i-1; j++) {

           if (arr[j] > arr[j+1]) {

               // 交换相邻元素

               temp = arr[j];

               arr[j] = arr[j+1];

               arr[j+1] = temp;

               swapped = 1;

           }

       }

       // 无交换则提前退出

       if (swapped == 0)

           break;

   }

}

int main() {

   int arr[] = {64, 34, 25, 12, 22, 11, 90};

   int n = sizeof(arr)/sizeof(arr[0]);

  

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   bubbleSort(arr, n);

  

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   return 0;

}

1.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好:O (n) 数组已有序,仅需 1 轮遍历
最坏:O (n²) 数组逆序,需 n-1 轮遍历
平均:O (n²) 适用于小规模数据
空间复杂度 O(1) 原地排序,仅需常数级空间

1.4 关键特性

  • 稳定排序(不改变相同元素的相对顺序);

  • 实现简单,效率低,仅适用于入门学习或极小规模数据。


2. 插入排序

2.1 原理说明

插入排序的核心思想是 “逐步构建有序序列”:

  • 将数组分为 “已排序区”(初始为第一个元素)和 “未排序区”;

  • 每次从未排序区取出第一个元素,插入到已排序区的合适位置;

  • 插入过程中,已排序区中比当前元素大的元素依次后移,腾出位置完成插入。

2.2 C 语言实现代码

#include <stdio.h>

void insertionSort(int arr[], int n) {

   int i, key, j;

   for (i = 1; i < n; i++) {

       key = arr[i];  // 取出未排序区的第一个元素

       j = i - 1;     // 指向已排序区最后一个元素

       // 已排序区元素后移,找到插入位置

       while (j >= 0 && arr[j] > key) {

           arr[j + 1] = arr[j];

           j = j - 1;

       }

       arr[j + 1] = key;  // 插入到正确位置

   }

}

int main() {

   int arr[] = {12, 11, 13, 5, 6};

   int n = sizeof(arr) / sizeof(arr[0]);

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   insertionSort(arr, n);

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   return 0;

}

2.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好:O (n) 数组已有序,仅需比较无需移动
最坏:O (n²) 数组逆序,每个元素需移动全部已排序区
平均:O (n²) 适合小规模 / 基本有序数据
空间复杂度 O(1) 原地排序,仅需临时变量存储当前元素

2.4 关键特性

  • 稳定排序;

  • 在线算法(可边接收数据边排序);

  • 实际应用中比冒泡排序高效(移动次数少于交换次数)。


3. 选择排序

3.1 原理说明

选择排序的核心思想是 “选择最小值”:

  • 将数组分为 “已排序区” 和 “未排序区”;

  • 每次遍历未排序区,找到最小元素的索引,将其与未排序区的第一个元素交换;

  • 重复上述过程,直到未排序区为空。

3.2 C 语言实现代码

#include <stdio.h>

void selectionSort(int arr[], int n) {

   int i, j, min_idx, temp;

  

   for (i = 0; i < n-1; i++) {

       // 找到未排序区的最小元素索引

       min_idx = i;

       for (j = i+1; j < n; j++) {

           if (arr[j] < arr[min_idx])

               min_idx = j;

       }

      

       // 交换最小元素与未排序区第一个元素

       if (min_idx != i) {

           temp = arr[i];

           arr[i] = arr[min_idx];

           arr[min_idx] = temp;

       }

   }

}

int main() {

   int arr[] = {64, 25, 12, 22, 11};

   int n = sizeof(arr)/sizeof(arr[0]);

  

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   selectionSort(arr, n);

  

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   return 0;

}

3.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好 / 最坏 / 平均:O (n²) 无论数组是否有序,都需遍历未排序区找最小值
空间复杂度 O(1) 原地排序,仅需临时变量交换元素

3.4 关键特性

  • 不稳定排序(交换可能打乱相同元素的相对顺序);

  • 交换次数最少(最多 n-1 次),适合 “写操作代价高” 的场景(如磁盘排序);

  • 效率低于插入排序(即使数据有序,仍需遍历)。


4. 希尔排序

4.1 原理说明

希尔排序是插入排序的改进版(又称 “缩小增量排序”),核心思想是 “分组插入排序”:

  1. 选择一个递减的增量序列(如 n/2, n/4, ..., 1);

  2. 按增量将数组分为多个子序列(相隔增量的元素为一组);

  3. 对每个子序列进行插入排序,使数组 “基本有序”;

  4. 逐步缩小增量,重复分组排序,直到增量为 1;

  5. 增量为 1 时,数组已基本有序,最后一次插入排序效率极高。

4.2 C 语言实现代码

#include <stdio.h>

void shellSort(int arr[], int n) {

   int gap, i, j, temp;

  

   // 初始增量为数组长度的一半,逐步减半

   for (gap = n/2; gap > 0; gap /= 2) {

       // 对每个增量进行插入排序

       for (i = gap; i < n; i++) {

           temp = arr[i];

          

           // 插入排序:比较相隔gap的元素

           for (j = i; j >= gap && arr[j-gap] > temp; j -= gap) {

               arr[j] = arr[j-gap];

           }

          

           arr[j] = temp;

       }

   }

}

int main() {

   int arr[] = {12, 34, 54, 2, 3};

   int n = sizeof(arr)/sizeof(arr[0]);

  

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   shellSort(arr, n);

  

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

  

   return 0;

}

4.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好:O (n) 数组已有序,增量为 1 时接近插入排序
最坏:O (n²)(Shell 增量)/ O (n^(3/2))(Hibbard 增量) 取决于增量序列选择
平均:O (n log n) ~ O (n²) 增量序列越优,复杂度越低
空间复杂度 O(1) 原地排序,仅需常数级空间

4.4 关键特性

  • 不稳定排序(分组排序会打乱相同元素的相对顺序);

  • 增量序列是性能关键(常见:Shell 增量、Hibbard 增量、Knuth 增量);

  • 适用于中等规模数据,性能优于冒泡 / 插入 / 选择排序。


5. 快速排序

5.1 原理说明

快速排序是基于分治思想的高效排序算法,核心思想是 “基准分区 + 递归排序”:

  1. 选择基准:从数组中选取一个元素作为 “基准(pivot)”(常用策略:选尾元素、中间元素、随机元素,随机基准可避免最坏情况);

  2. 分区操作:遍历数组,将小于基准的元素移到基准左侧,大于基准的元素移到基准右侧,最终基准处于 “排序后的正确位置”;

  3. 递归排序:分别对基准左侧和右侧的子数组重复 “选基准 + 分区” 操作,直到子数组长度≤1(天然有序)。

核心分区逻辑

  • 定义左右指针(low指向子数组起点,high指向终点);

  • 以尾元素为基准,左指针向右找 “大于基准” 的元素,右指针向左找 “小于基准” 的元素;

  • 找到后交换左右指针指向的元素,直到左右指针相遇;

  • 将基准与左指针元素交换,完成分区(基准左侧全小、右侧全大)。

5.2 C 语言实现代码

#include <stdio.h>

// 分区函数:返回基准元素的最终位置

int partition(int arr[], int low, int high) {

   int pivot = arr[high]; // 选尾元素作为基准

   int i = (low - 1);     // 小于基准区域的右边界

  

   for (int j = low; j <= high - 1; j++) {

       // 若当前元素小于等于基准,划入左侧区域

       if (arr[j] <= pivot) {

           i++; // 扩大左侧区域

           // 交换元素

           int temp = arr[i];

           arr[i] = arr[j];

           arr[j] = temp;

       }

   }

   // 将基准放到正确位置(左侧区域的下一位)

   int temp = arr[i + 1];

   arr[i + 1] = arr[high];

   arr[high] = temp;

  

   return (i + 1);

}

// 快速排序递归函数

void quickSort(int arr[], int low, int high) {

   if (low < high) {

       // 获取基准位置

       int pi = partition(arr, low, high);

      

       // 递归排序基准左侧子数组

       quickSort(arr, low, pi - 1);

       // 递归排序基准右侧子数组

       quickSort(arr, pi + 1, high);

   }

}

// 辅助打印函数

void printArray(int arr[], int size) {

   for (int i = 0; i < size; i++)

       printf("%d ", arr[i]);

   printf("\n");

}

int main() {

   int arr[] = {10, 7, 8, 9, 1, 5};

   int n = sizeof(arr) / sizeof(arr[0]);

  

   printf("排序前的数组:\n");

   printArray(arr, n);

  

   quickSort(arr, 0, n - 1);

  

   printf("排序后的数组:\n");

   printArray(arr, n);

  

   return 0;

}

5.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好:O (n log n) 基准选得均衡,每次分区将数组拆分为两个等长子数组
最坏:O (n²) 数组已有序 / 逆序,且选首尾为基准(分区为 1 和 n-1)
平均:O (n log n) 实际应用中最接近理论最优的复杂度
空间复杂度 最好:O (log n) 递归栈深度(均衡分区)
最坏:O (n) 递归栈深度(极端分区)
平均:O (log n) 仅需递归栈的额外空间

5.4 关键特性

  • 不稳定排序:分区交换会打乱相同元素的相对顺序;

  • 优化方向

  1. 基准选择:改用 “三数取中”(首、中、尾取中间值)或随机基准,避免最坏情况;

  2. 小规模子数组:递归到子数组长度 < 10 时,改用插入排序(减少递归开销);

  3. 非递归实现:用栈模拟递归,降低栈溢出风险;

  • 适用场景:大规模数据排序(实际工程中最常用的排序算法,效率高于希尔排序);

  • 原地排序:仅需常数级额外空间(递归栈除外)。


6. 桶排序

6.1 原理说明

桶排序是分布式排序算法,核心思想是 “空间换时间”:

  1. 确定数据范围,创建若干个 “桶”(每个桶对应一个区间);

  2. 将元素按值映射到对应的桶中;

  3. 对每个非空桶内的元素进行排序(可复用插入 / 快速排序);

  4. 按桶的顺序依次收集元素,形成有序数组。

6.2 C 语言实现代码

#include <stdio.h>

#include <stdlib.h>

// 定义链表节点(存储桶内元素)

typedef struct Node {

   int data;

   struct Node* next;

} Node;

// 插入节点到链表(保持升序)

void insert(Node** head_ref, int value) {

   Node* new_node = (Node*)malloc(sizeof(Node));

   new_node->data = value;

   new_node->next = NULL;

   if (*head_ref == NULL || (*head_ref)->data >= value) {

       new_node->next = *head_ref;

       *head_ref = new_node;

       return;

   }

   Node* current = *head_ref;

   while (current->next != NULL && current->next->data < value) {

       current = current->next;

   }

   new_node->next = current->next;

   current->next = new_node;

}

// 桶排序主函数

void bucketSort(int arr[], int n, int max) {

   // 创建max+1个桶(假设数据范围0\~max)

   Node** buckets = (Node**)calloc(max + 1, sizeof(Node*));

   if (buckets == NULL) {

       fprintf(stderr, "内存分配失败\n");

       exit(EXIT_FAILURE);

   }

   // 将元素分配到桶中

   for (int i = 0; i < n; i++) {

       insert(\&buckets[arr[i]], arr[i]);

   }

   // 从桶中收集元素回原数组

   int index = 0;

   for (int i = 0; i <= max; i++) {

       Node* current = buckets[i];

       while (current != NULL) {

           arr[index++] = current->data;

           Node* temp = current;

           current = current->next;

           free(temp); // 释放节点内存

       }

   }

   free(buckets); // 释放桶数组

}

int main() {

   int arr[] = {4, 1, 5, 2, 3, 5};

   int n = sizeof(arr) / sizeof(arr[0]);

   int max = 5; // 已知数据最大值

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   bucketSort(arr, n, max);

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   return 0;

}

6.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好 / 平均:O (n + k) k 为桶数,数据均匀分布时,桶内排序代价极低
最坏:O (n²) 所有元素集中在一个桶,退化为桶内排序的复杂度
空间复杂度 O(n + k) 需存储桶数组和桶内元素(链表 / 数组)

6.4 关键特性

  • 稳定性:取决于桶内排序算法(用插入排序则稳定);

  • 数据敏感性:仅适用于数据范围已知、分布均匀的场景(如浮点数、年龄排序);

  • 可并行化:每个桶的排序可独立执行。


7. 基数排序

7.1 原理说明

基数排序是非比较型排序,核心思想是 “逐位排序”:

  1. 将整数按位数分解(个位、十位、百位...);

  2. 从最低位(LSD)或最高位(MSD)开始,逐位对元素进行稳定排序(通常用计数排序 / 桶排序);

  3. 每一轮排序保证当前位有序,最终所有位排序完成后,数组整体有序。

LSD(最低位优先):最常用,从个位→十位→百位依次排序,适合整数排序;

MSD(最高位优先):从百位→十位→个位依次排序,适合字符串 / 字典序排序。

7.2 C 语言实现代码(LSD)

#include <stdio.h>

#include <stdlib.h>

// 获取数组最大值(确定最大位数)

int getMax(int arr[], int n) {

   int max = arr[0];

   for (int i = 1; i < n; i++)

       if (arr[i] > max)

           max = arr[i];

   return max;

}

// 按指定位数(exp)进行计数排序(稳定)

void countSort(int arr[], int n, int exp) {

   int* output = (int*)malloc(n * sizeof(int));

   int count[10] = {0};

   // 统计当前位的数字出现次数

   for (int i = 0; i < n; i++)

       count[(arr[i] / exp) % 10]++;

   // 计算累积次数(确定元素在output中的位置)

   for (int i = 1; i < 10; i++)

       count[i] += count[i - 1];

   // 构建输出数组(从后往前,保证稳定性)

   for (int i = n - 1; i >= 0; i--) {

       output[count[(arr[i] / exp) % 10] - 1] = arr[i];

       count[(arr[i] / exp) % 10]--;

   }

   // 复制回原数组

   for (int i = 0; i < n; i++)

       arr[i] = output[i];

   free(output);

}

// 基数排序主函数

void radixSort(int arr[], int n) {

   if (n <= 0) return;

  

   int max = getMax(arr, n);

   // 逐位排序(个位exp=1,十位exp=10,百位exp=100...)

   for (int exp = 1; max / exp > 0; exp *= 10)

       countSort(arr, n, exp);

}

int main() {

   int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};

   int n = sizeof(arr) / sizeof(arr[0]);

   printf("排序前的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   radixSort(arr, n);

   printf("\n排序后的数组:\n");

   for (int i = 0; i < n; i++)

       printf("%d ", arr[i]);

   return 0;

}

7.3 复杂度分析

维度 复杂度 说明
时间复杂度 最好 / 平均 / 最坏:O (d×(n + k)) d 为最大位数,k 为基数(十进制 k=10),每轮计数排序 O (n+k)
空间复杂度 O(n + k) 需存储输出数组和计数数组

7.4 关键特性

  • 稳定排序;

  • 仅适用于 “可分解位” 的数据(整数、字符串、可转换为整数的浮点数);

  • 处理负数:需将所有数加偏移量转为非负,排序后恢复;

  • 大规模数据下效率高(线性时间复杂度),但空间开销较大。


总结

排序算法 时间复杂度(平均) 空间复杂度 稳定性 适用场景
冒泡排序 O(n²) O(1) 稳定 入门学习、极小规模数据
插入排序 O(n²) O(1) 稳定 小规模 / 基本有序数据
选择排序 O(n²) O(1) 不稳定 写操作代价高的场景
希尔排序 O(n log n) ~ O(n²) O(1) 不稳定 中等规模数据
快速排序 O(n log n) O(log n) 不稳定 大规模数据(工程首选)
桶排序 O(n + k) O(n + k) 稳定 数据范围已知、分布均匀
基数排序 O(d×(n + k)) O(n + k) 稳定 大规模整数 / 字符串排序
posted @ 2025-12-04 09:26  Afangdong  阅读(145)  评论(0)    收藏  举报