C 练习实例37 – 排序(保姆级教程)

C 练习实例37 – 排序:从冒泡到快速,掌握数组排序的核心逻辑

在学习 C 语言的过程中,排序是一个绕不开的经典练习。它不仅是算法入门的“敲门砖”,更是理解循环、条件判断、数组操作与函数封装的综合实践。今天我们就来深入探讨 C 练习实例37 – 排序,通过几个经典排序算法的实现,带你一步步掌握排序的本质。

排序,就像是把杂乱无章的书本按书名顺序排列;在程序中,它就是将一组无序的数据,按照从小到大或从大到小的规则重新组织。无论是学生成绩、商品价格,还是用户注册时间,排序都无处不在。

接下来,我们将从最基础的冒泡排序开始,逐步深入到更高效的快速排序,每一步都配有完整的代码与逐行注释,确保你不仅能“看懂”,还能“写对”。


冒泡排序:最直观的“浮水”思想

冒泡排序的原理非常简单:每次比较相邻的两个元素,如果前一个比后一个大(升序排列),就交换它们的位置。这样,大的元素就像气泡一样,逐渐“浮”到数组的末尾。

这个过程重复进行,直到整个数组有序。

创建数组与初始化

我们先创建一个整型数组,并初始化一些无序数据,作为排序的输入。

#include <stdio.h>

int main() {
    // 定义一个包含 8 个整数的数组
    int arr[] = {64, 34, 25, 12, 22, 11, 90, 5};

    // 计算数组长度
    int n = sizeof(arr) / sizeof(arr[0]);

    // 输出原始数组
    printf("原始数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 冒泡排序的核心逻辑
    for (int i = 0; i < n - 1; i++) {
        // 每一轮外层循环,最大的元素会“冒泡”到末尾
        for (int j = 0; j < n - i - 1; j++) {
            // 如果前一个元素大于后一个元素,就交换
            if (arr[j] > arr[j + 1]) {
                // 临时变量 temp 用于交换两个数
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }

    // 输出排序后的数组
    printf("排序后数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

代码注释说明:

  • sizeof(arr) / sizeof(arr[0]):计算数组元素个数,避免硬编码。
  • 外层循环 i 控制排序轮数,每轮减少一次比较,因为最大值已归位。
  • 内层循环 j 从头到未排序部分的末尾,比较相邻元素。
  • 交换操作使用临时变量 temp,这是 C 语言中交换两个变量的标准做法。

运行结果如下:

原始数组:64 34 25 12 22 11 90 5 
排序后数组:5 11 12 22 25 34 64 90 

冒泡排序虽然效率不高(时间复杂度 O(n²)),但它的逻辑清晰,非常适合初学者理解排序的基本思想。


选择排序:每一次都选“最小值”

选择排序的思路更像“挑班长”——从所有同学中选出最优秀的一个,放到第一位;再从剩下的中选出第二优秀,放到第二位……如此反复,直到全部排好。

选择排序的实现

#include <stdio.h>

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 选择排序主循环
    for (int i = 0; i < n - 1; i++) {
        // 假设当前位置 i 就是最小值的位置
        int min_idx = i;

        // 在未排序部分中寻找最小值的下标
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;  // 更新最小值的下标
            }
        }

        // 如果最小值不在当前位置,则交换
        if (min_idx != i) {
            int temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
    }

    printf("排序后数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

关键点解释:

  • min_idx 用于记录当前未排序部分中最小元素的索引。
  • 内层循环遍历 i+1n-1,因为 i 之前的部分已经排好。
  • 仅当 min_idx != i 时才交换,避免无谓操作。

选择排序的时间复杂度同样是 O(n²),但交换次数更少,通常比冒泡排序略快。


插入排序:像整理扑克牌一样排序

插入排序的思路,就像是你在打扑克牌时,每摸一张牌,就把它插到正确的位置。它适合小规模数据,或部分有序的数据。

插入排序的实现

#include <stdio.h>

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 插入排序核心逻辑
    for (int i = 1; i < n; i++) {
        int key = arr[i];  // 当前要插入的元素
        int j = i - 1;     // 已排序部分的最后一个元素下标

        // 从右往左比较,找到 key 应该插入的位置
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];  // 将大于 key 的元素向右移动
            j--;
        }

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

    printf("排序后数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

逐行解读:

  • key = arr[i]:取出当前待插入的元素。
  • j = i - 1:从已排序部分的末尾开始比较。
  • while 循环:将大于 key 的元素向右移动一位,腾出空间。
  • 最后 arr[j + 1] = key:把 key 插入到合适位置。

插入排序在数据接近有序时表现优异,时间复杂度可接近 O(n),是许多高级排序算法的基础。


快速排序:分治思想的典范

快速排序是 C 练习实例37 – 排序中最具代表性的算法之一。它采用“分而治之”的策略:选一个基准值(pivot),将数组分为两部分——小于基准的放左边,大于基准的放右边,然后递归处理左右两部分。

快速排序的实现

#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; j++) {
        // 如果当前元素小于或等于基准
        if (arr[j] <= pivot) {
            i++;
            // 交换 arr[i] 和 arr[j]
            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 pivot_index = partition(arr, low, high);

        // 递归排序基准左边部分
        quickSort(arr, low, pivot_index - 1);

        // 递归排序基准右边部分
        quickSort(arr, pivot_index + 1, high);
    }
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 调用快速排序
    quickSort(arr, 0, n - 1);

    printf("排序后数组:");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

核心逻辑解析:

  • partition 函数:将数组按基准值划分为两部分。
  • lowhigh 定义当前处理的子数组边界。
  • 递归调用 quickSort 实现分治。

快速排序平均时间复杂度为 O(n log n),是实际应用中最常用的排序算法之一。


总结:从 C 练习实例37 – 排序中收获什么?

通过今天的 C 练习实例37 – 排序,我们不仅实现了四种经典排序算法,更重要的是理解了算法设计的基本思想:

  • 冒泡排序:直观,适合入门。
  • 选择排序:每次选最小,减少交换。
  • 插入排序:像整理扑克牌,适合小数据。
  • 快速排序:分治思想,效率最高。

这些算法虽小,但背后蕴含着编程思维的精髓:如何用有限的代码,解决复杂的问题。

对于初学者,建议从冒泡排序开始,逐步理解循环与比较逻辑;中级开发者可深入研究快速排序的递归与分治机制,提升算法设计能力。

记住,排序不只是“让数字变有序”,更是锻炼你逻辑思维、代码组织与问题拆解能力的绝佳练习。

最后,当你能独立写出快速排序时,你就已经迈过了 C 语言学习中一个重要的门槛。继续加油,编程之路,越走越远。