C语言算法专题总结(一)排序

C 语言排序算法专题总结

一、冒泡排序(Bubble Sort)

基本思想

比较相邻两个数的大小,每一趟将最大数"冒"至数组末尾。

优化策略

若某一趟没有进行交换,则说明已经有序,可以设置 flag 提前停止。

代码实现

c 复制代码
void bubbleSort(int a[], int n){
    for(int i = 0; i < n - 1; i++){
        for(int j = 0; j < n - i - 1; j++){
            if(a[j] > a[j + 1]){
                int tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
            }
        }
    }
}

时间复杂度

  • 最好情况:O(n) - 已经有序
  • 平均情况:O(n²)
  • 最坏情况:O(n²)

空间复杂度

O(1)

稳定性

稳定排序


二、选择排序(Selection Sort)

基本思想

每一趟在未排序部分中选择最小的数,放在已排序部分的末尾。

代码实现

c 复制代码
void selectionSort(int a[], int n){
    for(int i = 0; i < n; i++){
        int minIndex = i;
        // 找到最小值的索引
        for(int j = i + 1; j < n; j++){
            if(a[minIndex] > a[j]){
                minIndex = j;
            }
        }
        // 交换
        int tmp = a[i];
        a[i] = a[minIndex];
        a[minIndex] = tmp;
    }
}

时间复杂度

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

空间复杂度

O(1)

稳定性

不稳定排序


三、插入排序(Insertion Sort)

基本思想

前 i 个数字是已排序的部分,将第 i+1 个数字插入到前面已排序序列的适当位置。

代码实现

c 复制代码
void insertionSort(int a[], int n){
    for(int i = 1; i < n; i++){
        int key = a[i], j = i;
        // 比 key 大的元素向后移动
        while(j > 0 && key < a[j - 1]){
            a[j] = a[j - 1];
            j--;
        }
        a[j] = key;
    }
}

时间复杂度

  • 最好情况:O(n) - 已经有序
  • 平均情况:O(n²)
  • 最坏情况:O(n²)

空间复杂度

O(1)

稳定性

稳定排序


四、快速排序(Quick Sort)

基本思想

采用分治法,选定一个基准元素(pivot),将数组分为两部分:左侧都比基准小,右侧都比基准大,然后递归排序两部分。

代码实现

c 复制代码
// 分区函数
int partition(int a[], int low, int high){
    int key = a[low];  // 选择第一个元素作为基准
    while(low < high){
        // 从右向左找小于基准的元素
        while(a[high] >= key && low < high) high--;
        a[low] = a[high];
        // 从左向右找大于基准的元素
        while(a[low] <= key && low < high) low++;
        a[high] = a[low];
    }
    a[low] = key;
    return low;
}

// 快速排序主函数
void quickSort(int a[], int n, int low, int high){
    if(low < high){
        int pivotkey = partition(a, low, high);
        quickSort(a, n, low, pivotkey - 1);   // 左半部分
        quickSort(a, n, pivotkey + 1, high);  // 右半部分
    }
}

使用库函数 qsort

c 复制代码
#include <stdlib.h>

// 比较函数
int compare(const void *a, const void *b){
    int num1 = *((int*)a);
    int num2 = *((int*)b);
    return num1 - num2;  // 升序
}

// 调用示例
qsort(arr, n, sizeof(int), compare);

自定义排序规则示例

c 复制代码
// 西电考题:按各位数字之和降序排列,和相等时按数字本身升序
int sumOfDigits(int num){
    int sum = 0;
    while(num > 0){
        sum += num % 10;
        num /= 10;
    }
    return sum;
}

int compare(const void *a, const void *b){
    int num1 = *((int*)a);
    int num2 = *((int*)b);
    int sum1 = sumOfDigits(num1);
    int sum2 = sumOfDigits(num2);
    
    if(sum1 == sum2){
        return num1 - num2;  // 和相等时按数字升序
    } else {
        return sum2 - sum1;  // 按数字和降序
    }
}

时间复杂度

  • 最好情况:O(n log n)
  • 平均情况:O(n log n)
  • 最坏情况:O(n²) - 已经有序时

空间复杂度

O(log n) - 递归栈

稳定性

不稳定排序


五、归并排序(Merge Sort)

基本思想

采用分治法,将数组分为两部分分别排序,然后合并两个有序序列。

代码实现

c 复制代码
// 合并两个有序子序列
void merge(int a[], int low, int mid, int high){
    int i = low, j = mid + 1, k = 0;
    int *temp = (int*)malloc((high - low + 1) * sizeof(int));
    
    // 比较两个子序列的元素,较小的放入临时数组
    while(i <= mid && j <= high){
        if(a[i] <= a[j]){
            temp[k++] = a[i++];
        } else {
            temp[k++] = a[j++];
        }
    }
    
    // 复制剩余元素
    while(i <= mid) temp[k++] = a[i++];
    while(j <= high) temp[k++] = a[j++];
    
    // 将临时数组复制回原数组
    for(i = 0; i < high - low + 1; i++){
        a[low + i] = temp[i];
    }
    
    free(temp);
}

// 归并排序主函数
void mergeSort(int a[], int low, int high){
    if(low < high){
        int mid = low + (high - low) / 2;
        mergeSort(a, low, mid);      // 左半部分
        mergeSort(a, mid + 1, high); // 右半部分
        merge(a, low, mid, high);    // 合并
    }
}

时间复杂度

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

空间复杂度

O(n) - 需要额外空间

稳定性

稳定排序

特点

常考稳定性,适合链表排序


六、堆排序(Heap Sort)

基本思想

基于堆数据结构(完全二叉树),构建大根堆/小根堆后进行排序。

代码实现

c 复制代码
// 堆化函数:维护以 i 为根的子树满足大根堆性质
void heapify(int a[], int n, int i){
    int largest = i;
    int left = i * 2 + 1;   // 左孩子
    int right = i * 2 + 2;  // 右孩子
    
    // 找出父节点和两个孩子中的最大值
    if(left < n && a[largest] < a[left]) largest = left;
    if(right < n && a[largest] < a[right]) largest = right;
    
    if(largest != i){
        // 交换
        int tmp = a[i];
        a[i] = a[largest];
        a[largest] = tmp;
        
        // 递归调整被影响的子树
        heapify(a, n, largest);
    }
}

// 堆排序主函数
void heapSort(int a[], int n){
    // 构建初始大根堆
    for(int i = n / 2 - 1; i >= 0; i--){
        heapify(a, n, i);
    }
    
    // 依次将堆顶元素与末尾元素交换
    for(int i = n - 1; i > 0; i--){
        int tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        
        // 重新调整堆
        heapify(a, i, 0);
    }
}

时间复杂度

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

空间复杂度

O(1)

稳定性

不稳定排序


七、基数排序(Radix Sort)

基本思想

按照低位优先(LSD)或高位优先(MSD)的方式,逐位进行排序。通常配合计数排序使用。

代码实现

c 复制代码
// 计数排序辅助函数:按指定位数排序
void countingSortForRadix(int arr[], int n, int exp){
    int output[n], count[10] = {0};
    
    // 统计当前位的基数
    for(int i = 0; i < n; i++){
        count[(arr[i] / exp) % 10]++;
    }
    
    // 累加基数
    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];
    }
}

// 基数排序主函数
void radixSort(int arr[], int n){
    // 找出最大值确定位数
    int max = arr[0];
    for(int i = 1; i < n; i++){
        if(arr[i] > max) max = arr[i];
    }
    
    // 按每一位进行计数排序
    for(int exp = 1; max / exp > 0; exp *= 10){
        countingSortForRadix(arr, n, exp);
    }
}

时间复杂度

  • O(d × (n + k)),d 为位数,k 为基数(通常为 10)

空间复杂度

O(n + k)

稳定性

稳定排序


八、双向冒泡排序(Cocktail Sort)

基本思想

冒泡排序的改进版,每一轮在两个方向上进行冒泡:先从左到右将最大值移到右端,再从右到左将最小值移到左端。

代码实现

c 复制代码
void cocktailSort(int arr[], int n){
    int start = 0, end = n - 1;
    
    while(start < end){
        // 从左到右,将最大值移到右端
        for(int i = start; i < end; i++){
            if(arr[i] > arr[i + 1]){
                int tmp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = tmp;
            }
        }
        end--;
        
        // 从右到左,将最小值移到左端
        for(int i = end; i > start; i--){
            if(arr[i] < arr[i - 1]){
                int tmp = arr[i];
                arr[i] = arr[i - 1];
                arr[i - 1] = tmp;
            }
        }
        start++;
    }
}

时间复杂度

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

空间复杂度

O(1)

稳定性

稳定排序


九、统一测试代码

以下代码可以用来测试上述所有排序算法,只需替换调用的排序函数即可:

c 复制代码
#include<stdio.h>
#include<stdlib.h>

// 函数声明
void bubbleSort(int a[], int n);         // 冒泡排序
void selectionSort(int a[], int n);      // 选择排序
void insertionSort(int a[], int n);      // 插入排序
void quickSort(int a[], int n, int low, int high);     // 快速排序
void mergeSort(int a[], int low, int high);            // 归并排序
void heapSort(int a[], int n);
void radixSort(int arr[], int n);
void cocktailSort(int arr[], int n);

int main(){
    int n;
    printf("请输入数字的个数:");
    scanf("%d", &n);
    
    // 动态分配内存
    int *arr = (int *)malloc(n * sizeof(int));
    if(arr == NULL){
        printf("内存分配失败\n");
        return 0;
    }
    
    printf("请输入%d个数字:", n);
    for(int i = 0; i < n; i++){
        scanf("%d", &arr[i]);
    }
    
    // 对数组进行从小到大的排序
    // 可以替换为以下任意排序函数:
    // bubbleSort(arr, n);           // 冒泡排序
    // selectionSort(arr, n);        // 选择排序
    // insertionSort(arr, n);        // 插入排序
    // quickSort(arr, n, 0, n-1);    // 快速排序
    // mergeSort(arr, 0, n-1);       // 归并排序
    // heapSort(arr, n);             // 堆排序
    // radixSort(arr, n);            // 基数排序
    cocktailSort(arr, n);            // 双向冒泡排序
    
    // 输出排序结果
    for(int i = 0; i < n; i++){
        printf("%d ", arr[i]);
    }
    
    // 释放内存
    free(arr);
    return 0;
}

使用说明:

  1. 编译时需要将所有排序函数的实现代码一起编译
  2. 通过注释/取消注释不同的排序函数调用来测试不同算法
  3. 快速排序调用:quickSort(arr, n, 0, n-1)
  4. 归并排序调用:mergeSort(arr, 0, n-1)

十、各种排序算法对比

排序算法 最好时间 平均时间 最坏时间 空间复杂度 稳定性 适用场景
冒泡排序 O(n) O(n²) O(n²) O(1) 稳定 数据量小、基本有序
选择排序 O(n²) O(n²) O(n²) O(1) 不稳定 数据量小、对稳定性无要求
插入排序 O(n) O(n²) O(n²) O(1) 稳定 数据量小、基本有序
快速排序 O(n log n) O(n log n) O(n²) O(log n) 不稳定 大规模数据、一般情况首选
归并排序 O(n log n) O(n log n) O(n log n) O(n) 稳定 要求稳定性、链表排序
堆排序 O(n log n) O(n log n) O(n log n) O(1) 不稳定 要求最坏情况也是 O(n log n)
基数排序 O(d×n) O(d×n) O(d×n) O(n+k) 稳定 整数排序、位数固定
双向冒泡 O(n) O(n²) O(n²) O(1) 稳定 数据量小、基本有序

十一、排序算法选择建议

1. 小规模数据(n ≤ 50)

  • 推荐:插入排序、冒泡排序
  • 理由:简单易懂,常数因子小

2. 大规模数据

  • 一般情况:快速排序(效率最高)
  • 要求稳定性:归并排序
  • 要求最坏情况好:堆排序

3. 特殊场景

  • 基本有序:插入排序、冒泡排序
  • 整数排序且范围已知:基数排序、计数排序
  • 链表排序:归并排序
  • 内存受限:堆排序、原地排序

4. 考试重点

  • 稳定性判断:归并、插入、冒泡、基数是稳定的
  • 时间复杂度分析:重点掌握快排、归并、堆排序
  • 实际应用:qsort 库函数的使用

十二、常见问题

1. 如何判断排序算法的稳定性?

:如果排序后相同元素的相对位置不变,则为稳定排序。例如:[3a, 1, 3b, 2] 排序后为 [1, 2, 3a, 3b],3a 仍在 3b 前面。

2. 快速排序为什么不稳定?

:因为在分区过程中,可能会跨越多个位置交换元素,导致相同元素的相对位置改变。

3. 归并排序为什么稳定?

:因为在合并时,当两个元素相等时,优先选择左边子序列的元素,保持了原有顺序。

4. 如何选择基准元素(快速排序)?

  • 固定位置(如第一个、最后一个)
  • 随机选择
  • 三数取中(首、中、尾的中位数)

5. 堆排序中建堆的时间复杂度?

:O(n),不是 O(n log n)。因为大部分节点的高度较小。

相关推荐
Felven2 小时前
B. Roof Construction
c语言
美式请加冰2 小时前
模拟的介绍和使用
java·开发语言·算法
无限进步_2 小时前
深入解析vector:一个完整的C++动态数组实现
c语言·开发语言·c++·windows·git·github·visual studio
云泽8082 小时前
蓝桥杯算法精讲:贪心算法之区间问题深度剖析
算法·贪心算法·蓝桥杯
tankeven2 小时前
HJ129 小红的双生数
c++·算法
万能的小裴同学2 小时前
C++ 简易的FBX查看工具
开发语言·c++·算法
Boop_wu2 小时前
[Java 算法] 前缀和(2)
算法·哈希算法·散列表
Hello.Reader2 小时前
深入浅出 Adam 优化算法从直觉到公式
深度学习·算法
识君啊2 小时前
拆分与合并的艺术·分治思想:Java归并排序深度解析
java·数据结构·算法·排序算法·归并排序·分治