20260228 142212 Cpp 入门第十二课

20260228_142212_CPP_入门第十二课.md

第十二章:排序算法——让数据井然有序

你好!欢迎来到第十二章!在日常生活中,我们经常需要把东西排好序,比如按身高排队、按分数排名、按时间顺序排列照片。在计算机科学中,排序是最基础也最重要的算法之一。虽然C++的STL提供了强大的 sort 函数,但学习排序算法能帮助我们理解计算机如何工作,锻炼逻辑思维。这一章我们就来学习几种经典的排序算法,并亲手实现它们!


12.1 什么是排序算法?

排序就是将一组数据按照某种规则重新排列,比如从小到大(升序)或从大到小(降序)。

想象你有一堆乱序的扑克牌,你要把它们按数字从小到大整理好。你会怎么做?不同的方法对应不同的排序算法。

本章我们将学习:
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序(进阶)
- 归并排序(进阶)


12.2 冒泡排序

12.2.1 算法思想

冒泡排序就像水中的气泡,轻的气泡会慢慢浮到上面。每次比较相邻的两个元素,如果顺序不对(比如前大后小),就交换它们。这样每一轮都会把最大的数“冒泡”到最后。

过程演示(升序):
初始数组:[5, 3, 8, 1, 2]

第一轮:
- 比较5和3,5>3,交换 → [3, 5, 8, 1, 2]
- 比较5和8,5<8,不交换 → [3, 5, 8, 1, 2]
- 比较8和1,8>1,交换 → [3, 5, 1, 8, 2]
- 比较8和2,8>2,交换 → [3, 5, 1, 2, 8](最大数8已就位)

第二轮:
- 比较3和5,3<5,不交换 → [3, 5, 1, 2, 8]
- 比较5和1,5>1,交换 → [3, 1, 5, 2, 8]
- 比较5和2,5>2,交换 → [3, 1, 2, 5, 8](次大数5就位)

第三轮:
- 比较3和1,3>1,交换 → [1, 3, 2, 5, 8]
- 比较3和2,3>2,交换 → [1, 2, 3, 5, 8](第三大3就位)

第四轮:
- 比较1和2,1<2,不交换 → 已完成

12.2.2 代码实现

#include <iostream>
using namespace std;

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {          // 需要 n-1 轮
        for (int j = 0; j < n - 1 - i; j++) {  // 每轮比较次数减少
            if (arr[j] > arr[j + 1]) {          // 如果前大后小,交换
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {5, 3, 8, 1, 2};
    int n = sizeof(arr) / sizeof(arr[0]);

    bubbleSort(arr, n);

    cout << "排序后:";
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

12.2.3 优化

如果某一轮没有发生任何交换,说明数组已经有序,可以提前结束。

void bubbleSortOptimized(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;               // 标记是否交换
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);    // 可以用swap函数
                swapped = true;
            }
        }
        if (!swapped) break;                 // 没有交换,退出
    }
}

12.2.4 阶段性练习

  1. 练习1:用冒泡排序对数组 [9, 2, 5, 1, 7] 进行升序排序,写出每轮结果。
  2. 练习2:实现降序冒泡排序(从大到小)。
  3. 练习3:统计冒泡排序中交换的次数。

12.3 选择排序

12.3.1 算法思想

选择排序的思路很简单:每一轮从未排序的部分中找出最小的元素,放到已排序部分的末尾。

过程演示(升序):
初始:[5, 3, 8, 1, 2]
- 第一轮:找到最小元素1,与第一个元素5交换 → [1, 3, 8, 5, 2]
- 第二轮:从剩余[3,8,5,2]中找到最小2,与第二个元素3交换 → [1, 2, 8, 5, 3]
- 第三轮:从剩余[8,5,3]中找到最小3,与第三个元素8交换 → [1, 2, 3, 5, 8]
- 第四轮:从剩余[5,8]中找到最小5,与第四个元素5(自身)不交换 → 完成

12.3.2 代码实现

void selectionSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;                     // 假设当前是最小
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;                  // 更新最小下标
            }
        }
        if (minIndex != i) {                   // 交换
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

12.3.3 阶段性练习

  1. 练习1:用选择排序对数组 [8, 3, 6, 1, 4] 排序,写出每轮结果。
  2. 练习2:实现降序选择排序。
  3. 练习3:选择排序的交换次数最多是多少?

12.4 插入排序

12.4.1 算法思想

插入排序就像整理扑克牌:每次将一张牌插入到已经有序的手牌中的正确位置。

过程演示(升序):
初始:[5, 3, 8, 1, 2]
- 第一张牌5,有序部分:[5]
- 取3,插入到5前面 → [3, 5, 8, 1, 2]
- 取8,比5大,位置不变 → [3, 5, 8, 1, 2]
- 取1,依次比较8、5、3,插入最前 → [1, 3, 5, 8, 2]
- 取2,依次比较8、5、3,插入3和1之间 → [1, 2, 3, 5, 8]

12.4.2 代码实现

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];           // 当前要插入的元素
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];    // 向后移动
            j--;
        }
        arr[j + 1] = key;           // 插入正确位置
    }
}

12.4.3 阶段性练习

  1. 练习1:用插入排序对 [7, 2, 5, 1, 9] 排序,写出每一步后数组的变化。
  2. 练习2:实现降序插入排序。
  3. 练习3:插入排序在什么情况下最快?什么情况下最慢?

12.5 快速排序(进阶)

12.5.1 算法思想

快速排序采用分治策略:选择一个基准值(pivot),将数组分成两部分,左边都比基准小,右边都比基准大,然后递归地对左右两部分排序。

过程演示(以第一个元素为基准):
数组:[5, 3, 8, 1, 2]
- 基准5,从右找小于5的2,从左找大于5的8,交换2和8 → [5, 3, 2, 1, 8]
- 继续,从右找小于5的1,从左找大于5的没有,交换1和基准位置?实际上标准做法是用两个指针,最终将基准放到正确位置。
最终得到 [2, 3, 1, 5, 8],左边都小于5,右边大于5。
- 递归排序左边 [2,3,1] 和右边 [8]。

12.5.2 代码实现

int partition(int arr[], int low, int high) {
    int pivot = arr[low];          // 选第一个为基准
    int i = low, j = high;
    while (i < j) {
        while (i < j && arr[j] >= pivot) j--; // 从右找小于pivot的
        if (i < j) arr[i++] = arr[j];
        while (i < j && arr[i] <= pivot) i++; // 从左找大于pivot的
        if (i < j) arr[j--] = arr[i];
    }
    arr[i] = pivot;                // 基准归位
    return i;                      // 返回基准位置
}

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);
    }
}

调用时:quickSort(arr, 0, n-1);

12.5.3 阶段性练习

  1. 练习1:模拟快速排序对 [4, 7, 2, 1, 5, 3] 的过程。
  2. 练习2:尝试以中间元素为基准实现快速排序。

12.6 排序算法的比较

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
冒泡排序 O(n²) O(n²) O(1) 稳定
选择排序 O(n²) O(n²) O(1) 不稳定
插入排序 O(n²) O(n²) O(1) 稳定
快速排序 O(n log n) O(n²) O(log n) 不稳定
归并排序 O(n log n) O(n log n) O(n) 稳定

稳定性:如果两个相等的元素在排序前后的相对位置不变,则排序算法是稳定的。


12.7 编程实例讲解

实例1:成绩排序

题目:输入n个学生的姓名和成绩,按成绩从高到低排序,如果成绩相同则按姓名字典序排序。

#include <iostream>
#include <string>
#include <algorithm>  // 用sort,但我们可以手写排序
using namespace std;

struct Student {
    string name;
    int score;
};

// 冒泡排序(按成绩降序,成绩相同按姓名升序)
void sortStudents(Student stu[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (stu[j].score < stu[j + 1].score || 
                (stu[j].score == stu[j + 1].score && stu[j].name > stu[j + 1].name)) {
                swap(stu[j], stu[j + 1]);
            }
        }
    }
}

int main() {
    int n;
    cout << "请输入学生人数:";
    cin >> n;
    Student stu[100];
    for (int i = 0; i < n; i++) {
        cin >> stu[i].name >> stu[i].score;
    }
    sortStudents(stu, n);
    cout << "\n排序结果:" << endl;
    for (int i = 0; i < n; i++) {
        cout << stu[i].name << " " << stu[i].score << endl;
    }
    return 0;
}

实例2:排序算法效率对比

用随机数生成大量数据,测试不同排序算法的时间。

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

// 冒泡排序、选择排序、插入排序、快速排序函数声明...

int main() {
    const int N = 10000;
    int arr1[N], arr2[N], arr3[N], arr4[N];
    srand(time(0));
    for (int i = 0; i < N; i++) {
        int val = rand() % 10000;
        arr1[i] = arr2[i] = arr3[i] = arr4[i] = val;
    }

    clock_t start, end;

    start = clock();
    bubbleSort(arr1, N);
    end = clock();
    cout << "冒泡排序时间:" << double(end - start) / CLOCKS_PER_SEC << "秒" << endl;

    // 测试其他排序...
    return 0;
}

12.8 第12章编程作业

作业1:三种简单排序的对比

编写程序,生成1000个随机整数,分别用冒泡、选择、插入排序进行排序,并输出每种排序的交换次数和比较次数(在代码中加入计数器)。

作业2:排序字符串数组

输入若干单词(字符串),用插入排序按字典序排序输出。

作业3:快速排序的实现

实现快速排序,并测试其效率。

作业4:排序稳定性验证

创建一个包含姓名和分数的结构体数组,其中有两个同分不同名的学生。分别用稳定和不稳定的排序算法排序,观察他们的相对顺序是否改变。

作业5:自定义排序规则

编写一个程序,按以下规则对整数排序:奇数在前,偶数在后;奇数按升序,偶数按降序。例如 [3,2,5,1,8,4] 排序后应为 [1,3,5,8,4,2]。


恭喜你完成了第十二章的学习!现在你已经掌握了多种排序算法,能够处理各种排序问题了。加油!🚀