• C++
  • 算法:排序概念和稳定性

  • @ 2025-5-1 15:40:17

算法教程:排序概念与稳定性(0基础详细讲解)


📘 前言:什么是排序?

在编程中,排序(Sorting)是指将一组数据按照某种顺序(如从小到大或从大到小)进行排列。这是计算机科学中最基本、最常用的操作之一。

本教程将从零开始讲解排序的基本概念,包括:

  • 排序的定义和作用
  • 排序算法的分类
  • 稳定排序不稳定排序
  • 各种排序方式的直观理解

🧠 第一章:排序的基本概念

1.1 什么是排序?

👉 排序是将一个无序的数据序列变成有序的过程。

例如:

原始数组:{5, 2, 9, 1, 5, 6}
排序后:{1, 2, 5, 5, 6, 9} (升序)

1.2 为什么要排序?

  • 查找更高效(如二分查找的前提是数据有序)
  • 数据可视化更清晰
  • 便于后续处理(如去重、合并等)

📊 第二章:排序算法的分类

排序算法有很多,常见的有以下几种(按时间复杂度分类):

分类 时间复杂度 常见算法
O(n²) 简单排序 冒泡排序、插入排序、选择排序
O(n log n) 高效排序 快速排序、归并排序、堆排序
O(n) 特殊排序 计数排序、桶排序、基数排序

我们不会在这篇教程里深入讲解这些算法的实现,而是先了解它们的一些重要属性。


🧩 第三章:排序的“稳定性”是什么意思?

3.1 定义

一个排序算法是稳定的(Stable),当它能够保持原本相同值元素之间的相对顺序不变。

通俗点说:

  • 如果有两个相同的数字,在排序前 A 在 B 前面;
  • 排序之后,如果 A 还在 B 前面,那么这个排序就是稳定的;
  • 如果 A 跑到了 B 后面,那就是不稳定的。

3.2 举个例子

原始数据(按分数排序):

学生 分数
小明 85
小红 90
小刚 85

排序后(目标:按分数从低到高排序):

稳定排序结果

  • 小明(85)
  • 小刚(85)
  • 小红(90)

不稳定排序结果

  • 小刚(85)
  • 小明(85)
  • 小红(90)

虽然分数排对了,但两个人的顺序变了 → 不稳定


🧱 第四章:哪些常见排序是稳定的?哪些不是?

下面是一些常见排序算法的稳定性总结:

排序算法 是否稳定 说明
冒泡排序 ✅ 是 相邻比较,只交换顺序错误的元素
插入排序 插入时不打乱原有顺序
归并排序 使用分治策略,保留顺序
快速排序 ❌ 否 比较+交换可能打乱原顺序
选择排序 每次选最小值放到前面,可能改变顺序
计数排序 ✅ 是 不涉及比较,直接统计计数
基数排序 依赖稳定排序(通常是计数排序)
堆排序 ❌ 否 建堆过程会破坏顺序

🧭 第五章:为什么稳定性很重要?

虽然大多数时候只需要数值正确排序即可,但在某些实际场景中,稳定性非常重要

场景1️⃣:多字段排序

比如你要先按班级排序,再按成绩排序。

如果你使用的是不稳定排序算法,第二次排序可能会把第一次的结果打乱。

正确的做法是:

  • 先按成绩排序(可以是非稳定)
  • 再按班级排序(必须是稳定排序),这样班级内成绩仍然保持有序

场景2️⃣:带辅助信息的排序

比如你有一个商品列表,每个商品有价格和名称两个属性:

商品名 价格
苹果 5
香蕉 3
梨子 5

你想按价格排序,但希望价格相同时仍保持原来的顺序(苹果在梨子前面),那么你需要使用稳定排序算法


🔍 第六章:如何判断一个排序是否稳定?

你可以通过观察它的实现逻辑来判断:

  • 如果排序过程中没有进行跨位置交换,或者总是从前向后移动元素,那可能是稳定的。
  • 如果排序时大量交换远距离元素,特别是根据条件随意交换,那很可能是不稳定的。

🧪 第七章:动手实验(用代码看排序是否稳定)

我们以 C++ 中的 std::stable_sortstd::sort 来演示:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct Student {
    string name;
    int score;
};

int main() {
    vector<Student> students = {
        {"小明", 85},
        {"小刚", 85},
        {"小红", 90}
    };

    // 使用 std::sort(默认不稳定)
    sort(students.begin(), students.end(), [](Student a, Student b) {
        return a.score < b.score;
    });

    cout << "使用 std::sort 排序后的顺序:" << endl;
    for (auto s : students)
        cout << s.name << " (" << s.score << ")" << endl;

    // 重置数据
    students = {{"小明", 85}, {"小刚", 85}, {"小红", 90}};

    // 使用 std::stable_sort(保证稳定性)
    stable_sort(students.begin(), students.end(), [](Student a, Student b) {
        return a.score < b.score;
    });

    cout << "\n使用 std::stable_sort 排序后的顺序:" << endl;
    for (auto s : students)
        cout << s.name << " (" << s.score << ")" << endl;

    return 0;
}

输出示例:

使用 std::sort 排序后的顺序:
小刚 (85)
小明 (85)
小红 (90)

使用 std::stable_sort 排序后的顺序:
小明 (85)
小刚 (85)
小红 (90)

可以看到,默认的 std::sort 可能会改变同分者的顺序,而 std::stable_sort 会保留原始输入顺序。


📋 第八章:总结对比表

排序类型 是否稳定 时间复杂度 适用场景
冒泡排序 O(n²) 教学、小数据排序
插入排序 类似摸牌,适合局部有序数据
归并排序 O(n log n) 大数据排序,要求稳定
快速排序 一般用途,速度快
选择排序 O(n²) 简单直观,速度慢
计数排序 O(n + k) 整数排序,范围有限
堆排序 O(n log n) 找最大/最小值
基数排序 O(d * n) 字符串/整数排序

💡 第九章:学习建议 & 练习题推荐

✅ 动手练习建议

  1. 编写冒泡排序和选择排序程序,并测试它们对相同元素的稳定性。
  2. 用结构体保存学生信息(姓名、年龄、成绩),尝试用 C++ 的 std::sortstd::stable_sort 对年龄排序,并观察稳定性差异。
  3. 思考:如果我需要先按性别排序,再按成绩排序,应该从哪个字段开始排?为什么?
  4. 查阅 STL 中 std::mapstd::multiset 的底层实现,看看它们是否使用了稳定排序特性。

📚 推荐学习资源

  • C++ STL 官方文档
  • LeetCode 题目:
      1. 最大数(排序应用)
      1. 插入排序链表
      1. 排序链表(归并排序应用)
  • 洛谷题目:
    • P1059 明明的随机数(简单排序去重)
    • P1177 【模板】快速排序

📌 结语

排序是编程中最基础也是最重要的操作之一。掌握排序的稳定性概念,可以帮助你在处理现实问题时避免很多不必要的错误。

如果你对某个具体排序算法(如归并排序、快速排序)感兴趣,或者想了解如何自己写出一个稳定的排序函数,请告诉我!我可以为你定制进阶内容 😊


📌 下一步推荐学习:

  • 快速排序详解与实现(含非递归版本)
  • 归并排序详解与应用场景分析
  • 自定义稳定排序函数编写技巧
  • C++ STL 中排序函数的扩展使用(自定义比较器)

1 条评论

  • @ 2025-5-1 15:42:12

    C++算法:排序概念和稳定性教程

    1. 排序的基本概念

    排序是计算机科学中一项基础且重要的操作,它的目的是将一组数据按照特定的顺序(通常是升序或降序)重新排列。例如,将一组整数 {5, 3, 8, 1, 2} 按照升序排列后变为 {1, 2, 3, 5, 8}。排序在很多场景中都有广泛应用,比如在数据库中对查询结果进行排序,在搜索引擎中对搜索结果进行排序等。

    2. 排序算法的稳定性

    排序算法的稳定性是指在排序过程中,相等元素的相对顺序是否保持不变。如果一个排序算法能够保证相等元素的相对顺序不变,那么这个排序算法就是稳定的;反之,则是不稳定的。

    例如,有一组学生数据,每个学生有姓名和成绩两个属性,现在要按照成绩对学生进行排序。如果有两个学生 AB 的成绩相同,在排序前 AB 前面,经过排序后 A 仍然在 B 前面,那么这个排序算法就是稳定的;如果排序后 B 到了 A 前面,那么这个排序算法就是不稳定的。

    3. 常见排序算法的稳定性分析

    3.1 冒泡排序(Bubble Sort)

    冒泡排序是一种简单的排序算法,它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

    #include <iostream>
    #include <vector>
    
    // 冒泡排序函数
    void bubbleSort(std::vector<int>& arr) {
        int n = arr.size();
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j+1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
    int main() {
        std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(arr);
        std::cout << "Sorted array: ";
        for (int num : arr) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
        return 0;
    }
    

    稳定性分析:冒泡排序是稳定的排序算法。因为在比较相邻元素时,如果它们相等,是不会进行交换的,所以相等元素的相对顺序不会改变。

    3.2 选择排序(Selection Sort)

    选择排序是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。

    #include <iostream>
    #include <vector>
    
    // 选择排序函数
    void selectionSort(std::vector<int>& arr) {
        int n = arr.size();
        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;
                }
            }
            // 交换 arr[i] 和 arr[minIndex]
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    
    int main() {
        std::vector<int> arr = {64, 25, 12, 22, 11};
        selectionSort(arr);
        std::cout << "Sorted array: ";
        for (int num : arr) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
        return 0;
    }
    

    稳定性分析:选择排序是不稳定的排序算法。例如,有一组数据 {5, 8, 5, 2},在第一次选择最小元素时,会将第一个 52 交换位置,这样就改变了两个 5 的相对顺序。

    3.3 插入排序(Insertion Sort)

    插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

    #include <iostream>
    #include <vector>
    
    // 插入排序函数
    void insertionSort(std::vector<int>& arr) {
        int n = arr.size();
        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;
        }
    }
    
    int main() {
        std::vector<int> arr = {12, 11, 13, 5, 6};
        insertionSort(arr);
        std::cout << "Sorted array: ";
        for (int num : arr) {
            std::cout << num << " ";
        }
        std::cout << std::endl;
        return 0;
    }
    

    稳定性分析:插入排序是稳定的排序算法。在插入元素时,如果遇到相等元素,会将新元素插入到相等元素的后面,不会改变相等元素的相对顺序。

    4. 总结

    排序算法的稳定性在某些场景下非常重要,比如在多级排序中,先按照一个属性排序,再按照另一个属性排序时,如果第一个排序算法是稳定的,那么第二个排序可以在第一个排序的基础上进行,而不会破坏第一个排序的结果。在实际应用中,需要根据具体的需求选择合适的排序算法。

    以上就是关于 C++ 中排序概念和稳定性的基础教程,希望对你有所帮助。你可以自己动手实现这些排序算法,并测试它们的稳定性。

    • 1