NOI C++ 课程体系 / 算法篇 / 第11讲
CSP-J 入门级 · 排序算法 · m4-11

排序基础:冒泡排序 与 选择排序

从排队到排列 —— 掌握排序算法的基本概念,深入理解冒泡排序和选择排序的原理、实现与优化,为后续高级排序打下坚实基础。

6 大模块 2 种经典排序 3 个核心概念 5 大易错点 12+20 道练习
本讲 · 第 35/56 讲冒泡排序与选择排序
模块 01

情景导航

从生活中的排队场景开始,直觉理解"排序"的意义。

故事体育课排队

体育课上,老师让 6 个同学按身高从矮到高站成一排。可是大家站得乱七八糟:

165, 152, 170, 158, 163, 155(cm)

老师说:"你们两两比较,如果左边比右边高,就交换位置!"

同学们开始从左到右依次比较:

① 165 vs 152 → 165>152,交换 → 152, 165, 170, 158, 163, 155

② 165 vs 170 → 165<170,不换 → 152, 165, 170, 158, 163, 155

③ 170 vs 158 → 170>158,交换 → 152, 165, 158, 170, 163, 155

④ 170 vs 163 → 170>163,交换 → 152, 165, 158, 163, 170, 155

⑤ 170 vs 155 → 170>155,交换 → 152, 165, 158, 163, 155, 170

第一轮结束后,最高的人(170)已经"冒泡"到了最后!🫧

再来第二轮、第三轮……每轮都会把当前最高的"冒"到正确位置。这就是冒泡排序!

💡 另一种排法

如果老师换一种方式:"每轮从剩余的人中,找出最矮的,放到队伍最前面。"

第1轮:找最矮 152,放第1位 → 152, 165, 170, 158, 163, 155

第2轮:剩余中找最矮 155,放第2位 → 152, 155, 170, 158, 163, 165

这就是选择排序 —— 每轮选择最小的放到前面!

①生活类比:排序无处不在
🃏

扑克牌排序

拿到牌后从小到大排列:每次找最小的放到左边。

→ 选择排序
📚

书架整理

相邻两本比较,高的换到后面,一遍一遍扫。

→ 冒泡排序
🏆

成绩排名

全班成绩按从高到低排列,方便查找名次。

→ 排序的核心价值
🔍

二分查找前提

只有排好序的数据才能用二分查找!

→ 排序是搜索的基础
②编程定义

排序(Sorting):将一组数据按照某种规则(通常是数值大小或字典序)重新排列为有序序列的过程。

排序是计算机科学中最基本、最重要的操作之一。据统计,商业程序中约 25% 的 CPU 时间花在排序上!

📌 本讲核心知识地图

板块核心内容对应大纲
排序概念定义、分类、稳定性、时间/空间复杂度排序的基本概念
冒泡排序相邻比较交换、优化(提前终止)冒泡排序
选择排序每轮选最小/最大放到前面选择排序
稳定性相等元素的相对顺序是否改变排序稳定性分析
③核心口诀
🔑 冒泡排序

相邻两两比较,大的往后冒泡;
每轮冒一个,n-1 轮就排好;
如果一轮没交换,提前结束更高效!

🔑 选择排序

每轮选最小,放到最前面;
n-1 轮选完,数组就有序;
简单直观,但不稳定要记牢!

模块 02

知识探究

深入理解排序的基本概念,掌握冒泡排序和选择排序的原理与实现。

1排序的基本概念 核心
升序 vs 降序
  • 升序:从小到大 (1,2,3...)
  • 降序:从大到小 (3,2,1...)
  • 竞赛中通常要求升序
  • 降序只需改变比较符号
内部排序 vs 外部排序
  • 内部排序:数据全部在内存中
  • 外部排序:数据太大,需借助外存
  • CSP-J 只考内部排序
  • 冒泡/选择/插入/快排都是内部排序
稳定性
  • 稳定:相等元素排序后相对顺序不变
  • 不稳定:相等元素可能交换位置
  • 冒泡排序是稳定的
  • 选择排序是不稳定的
排序算法最好最坏平均空间稳定性
冒泡排序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(nlogn)O(n²)O(nlogn)O(logn)❌ 不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)✅ 稳定
2冒泡排序(Bubble Sort)必会

核心思想:从前往后,依次比较相邻的两个元素,如果前面的比后面的大,就交换它们的位置。每一轮结束后,当前未排序部分的最大值会"冒泡"到末尾。

📊 排序过程可视化:[5, 3, 8, 1, 2]

▶ 播放看全程 · ⏭ 单步研究每一次比较与交换 · 黄=正在比较,粉=发生交换,绿=已就位

C++bubble_sort.cpp — 基础版
#include <iostream>
using namespace std;

void bubbleSort(int a[], int n) {
    for (int i = 0; i < n - 1; i++) {        // 外层:n-1 轮
        for (int j = 0; j < n - 1 - i; j++) { // 内层:比较相邻元素
            if (a[j] > a[j + 1]) {            // 前面 > 后面
                // 交换(冒泡)
                int temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

int main() {
    int a[] = {5, 3, 8, 1, 2};
    int n = 5;
    bubbleSort(a, n);
    for (int i = 0; i < n; i++)
        cout << a[i] << " ";
    return 0;
}
运行结果
1 2 3 5 8

🔍 关键细节分析

外层循环 i:控制轮数。第 i 轮(从0开始)结束时,后 i 个元素已经排好,不需要再比较。

内层循环 j:范围是 0 到 n-1-i。因为每轮结束后最后 i 个已经有序,不需要再比较它们。

交换操作:需要三步(temp = a, a = b, b = temp),不能用 a=b, b=a(会丢失数据)。

时间复杂度:两层循环,外层 n-1 次,内层最多 n-1 次 → O(n²)

空间复杂度:只用了一个 temp 变量 → O(1)(原地排序)

⚡冒泡排序优化 提效

基础版冒泡排序即使数组已经有序,仍会继续执行所有轮次。我们可以通过一个 flag 标记来优化:

C++bubble_sort_optimized.cpp — 优化版
void bubbleSortOpt(int a[], int n) {
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;  // 本轮是否发生交换
        for (int j = 0; j < n - 1 - i; j++) {
            if (a[j] > a[j + 1]) {
                int temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
                swapped = true;  // 发生了交换
            }
        }
        if (!swapped) break;  // 本轮没交换 → 已有序,提前结束!
    }
}
✅ 优化效果
  • 最好情况(已有序):只需 1 轮,O(n)
  • 平均/最坏:仍为 O(n²)
  • 实际竞赛中几乎不会遇到最好情况
  • 但这是一个好习惯的体现
📊 复杂度总结
  • 最好时间:O(n)(已有序+优化)
  • 最坏时间:O(n²)(逆序)
  • 平均时间:O(n²)
  • 空间:O(1)(原地排序)
  • 稳定性:稳定(相等时不交换)

💡 为什么冒泡排序是"稳定"的?

考虑两个相等的元素 A=5 和 B=5(A 在 B 前面)。

当比较到 A 和 B 时,条件是 a[j] > a[j+1],即 5 > 5 为 false,不会交换。

所以 A 始终在 B 前面,相对顺序不变 → 稳定!

如果把条件改成 >=,就变成不稳定了!注意这个细节。

3选择排序(Selection Sort)必会

核心思想:每一轮从未排序部分中选出最小的元素,与未排序部分的第一个元素交换位置。n-1 轮后数组有序。

📊 排序过程可视化:[5, 3, 8, 1, 2]

注意观察:选择排序每轮只在最后交换一次,黄色的“当前最小值”会不断更新

C++selection_sort.cpp
#include <iostream>
using namespace std;

void selectionSort(int a[], int n) {
    for (int i = 0; i < n - 1; i++) {         // 外层:n-1 轮
        int minIdx = i;                         // 假设当前位置最小
        for (int j = i + 1; j < n; j++) {       // 在剩余部分找最小
            if (a[j] < a[minIdx]) {
                minIdx = j;                     // 更新最小值位置
            }
        }
        if (minIdx != i) {                      // 如果最小不在当前位置
            int temp = a[i];
            a[i] = a[minIdx];
            a[minIdx] = temp;                   // 交换到前面
        }
    }
}

int main() {
    int a[] = {5, 3, 8, 1, 2};
    int n = 5;
    selectionSort(a, n);
    for (int i = 0; i < n; i++)
        cout << a[i] << " ";
    return 0;
}
运行结果
1 2 3 5 8

🔍 关键细节分析

外层循环 i:表示当前要放置第 i 小的元素到位置 i。

minIdx:记录未排序部分中最小元素的下标。初始假设为 i。

内层循环 j:从 i+1 到 n-1,遍历未排序部分,找最小。

交换:只在找到更小的元素时才交换,每轮最多交换 1 次(比冒泡少!)。

时间复杂度:无论什么数据,都要两层循环 → 固定 O(n²)(没有最好情况优化)

空间复杂度:O(1)(原地排序)

📊冒泡排序 vs 选择排序
对比项冒泡排序选择排序
核心操作相邻比较+交换选最小+放到前面
每轮交换次数可能多次(最坏 n-1 次)最多 1 次
最好时间O(n)(优化后,已有序)O(n²)(无优化空间)
最坏时间O(n²)O(n²)
稳定性✅ 稳定❌ 不稳定
空间O(1)O(1)
实际效率交换多,常数略大交换少,常数略小
竞赛使用理解原理,很少直接用理解原理,很少直接用
🫧 冒泡排序

怎么动:相邻比较,边比边换,像气泡上浮

交换次数:取决于逆序对个数(最多 n(n-1)/2 次)

稳定性:✅ 稳定(相等不交换)

看家本领:一轮无交换可提前结束 → 最好 O(n)

🎯 选择排序

怎么动:先扫完全场找最小,再一步换到位

交换次数:每轮至多 1 次,最多 n-1 次(交换最少)

稳定性:❌ 不稳定(长距离交换会跨越相等元素)

看家本领:比较次数固定 n(n-1)/2,与输入无关

🧠 一句话记忆

冒泡是“边走边换”,选择是“看完再换”。

要交换次数最少(如写磁盘昂贵时)→ 选选择排序;

要稳定(如成绩同名次保序)→ 选冒泡排序。

🔒排序稳定性深入分析 必考

稳定性定义:如果排序前 a[i] = a[j] 且 i < j,排序后 a[i] 仍在 a[j] 前面,则排序算法是稳定的。

📖 选择排序为什么不稳定?

考虑数组:[5, 5, 2](两个5分别标记为 5ₐ 和 5ᵦ)

第1轮:在 [5ₐ, 5ᵦ, 2] 中找最小 → min=2(位置2),与位置0交换

结果:[2, 5ᵦ, 5ₐ]

注意:原来 5ₐ 在 5ᵦ 前面,现在 5ᵦ 在 5ₐ 前面!相对顺序变了!

→ 选择排序是不稳定的

💡 稳定性为什么重要?

场景:按学生成绩排名,成绩相同时希望保持原来的学号顺序。

多关键字排序:先按第二关键字排序,再按第一关键字排序。如果第二次排序是稳定的,则第一关键字相同的元素仍保持第二关键字的顺序。

例:先按身高排序(稳定),再按体重排序(稳定),最终结果:体重相同的人仍按身高排。

竞赛技巧:当需要保持相对顺序时,必须选择稳定排序(冒泡、插入、归并)。

模块 03

实践应用

通过详细分析题,巩固对冒泡排序和选择排序的理解。

题1冒泡排序过程追踪

题目:对数组 [4, 2, 7, 1, 5] 使用冒泡排序(升序),请写出每一轮结束后的数组状态,并统计总共进行了多少次比较和多少次交换。

提示:注意内层循环的范围每轮减少1。
查看参考答案

详细过程追踪:

第1轮(j 从 0 到 3,共4次比较):

j=0: a[0]=4 vs a[1]=2 → 4>2,交换 → [2, 4, 7, 1, 5]

j=1: a[1]=4 vs a[2]=7 → 4<7,不换 → [2, 4, 7, 1, 5]

j=2: a[2]=7 vs a[3]=1 → 7>1,交换 → [2, 4, 1, 7, 5]

j=3: a[3]=7 vs a[4]=5 → 7>5,交换 → [2, 4, 1, 5, 7]

第1轮结束:[2, 4, 1, 5, 7],比较4次,交换3次

第2轮(j 从 0 到 2,共3次比较):

j=0: 2 vs 4 → 不换 → [2, 4, 1, 5, 7]

j=1: 4 vs 1 → 交换 → [2, 1, 4, 5, 7]

j=2: 4 vs 5 → 不换 → [2, 1, 4, 5, 7]

第2轮结束:[2, 1, 4, 5, 7],比较3次,交换1次

第3轮(j 从 0 到 1,共2次比较):

j=0: 2 vs 1 → 交换 → [1, 2, 4, 5, 7]

j=1: 2 vs 4 → 不换 → [1, 2, 4, 5, 7]

第3轮结束:[1, 2, 4, 5, 7],比较2次,交换1次

第4轮(j=0,共1次比较):

j=0: 1 vs 2 → 不换 → [1, 2, 4, 5, 7]

第4轮结束:[1, 2, 4, 5, 7],比较1次,交换0次

统计:总比较次数 = 4+3+2+1 = 10 次,总交换次数 = 3+1+1+0 = 5 次

公式:n个元素的冒泡排序,比较次数 = n(n-1)/2 = 5×4/2 = 10

题2选择排序分析

题目:对数组 [3, 1, 4, 1, 5] 使用选择排序(升序)。请写出每轮选出的最小值及其交换过程。排序后两个1的相对顺序是否改变?

查看参考答案

详细过程(标记两个1为 1ₐ(位置1) 和 1ᵦ(位置3)):

初始:[3, 1ₐ, 4, 1ᵦ, 5]

第1轮:在 [3, 1ₐ, 4, 1ᵦ, 5] 中找最小 → minIdx=1(值1ₐ)

交换 a[0] 和 a[1]:3 ↔ 1ₐ → [1ₐ, 3, 4, 1ᵦ, 5]

第2轮:在 [3, 4, 1ᵦ, 5] 中找最小 → minIdx=3(值1ᵦ)

交换 a[1] 和 a[3]:3 ↔ 1ᵦ → [1ₐ, 1ᵦ, 4, 3, 5]

第3轮:在 [4, 3, 5] 中找最小 → minIdx=3(值3)

交换 a[2] 和 a[3]:4 ↔ 3 → [1ₐ, 1ᵦ, 3, 4, 5]

第4轮:在 [4, 5] 中找最小 → minIdx=3(值4)

minIdx == i,不交换 → [1ₐ, 1ᵦ, 3, 4, 5]

最终结果:[1ₐ, 1ᵦ, 3, 4, 5]

稳定性分析:排序前 1ₐ 在 1ᵦ 前面(位置1 < 位置3),排序后 1ₐ 仍在 1ᵦ 前面(位置0 < 位置1)。

这组数据恰好保持了顺序,但选择排序本质上是不稳定的。

反例:[5ₐ, 3, 5ᵦ, 2] → 第1轮 min=2 与 a[0]=5ₐ 交换 → [2, 3, 5ᵦ, 5ₐ],5ᵦ 跑到了 5ₐ 前面!

题3综合编程:逆序对计数

题目:给定一个数组,"逆序对"是指满足 i<j 且 a[i]>a[j] 的数对 (i,j)。编程计算数组 [5, 3, 8, 1, 2] 中有多少个逆序对。

提示:冒泡排序的交换次数恰好等于逆序对数!也可以用双重循环直接统计。
查看参考答案

方法一:双重循环枚举 O(n²)

#include <iostream>
using namespace std;
int main() {
    int a[] = {5,3,8,1,2};
    int n = 5, cnt = 0;
    for(int i=0; i<n; i++)
        for(int j=i+1; j<n; j++)
            if(a[i] > a[j]) cnt++;
    cout << cnt << endl;
    return 0;
}
运行结果
7

手动验证所有逆序对:

(5,3), (5,1), (5,2) → 3个

(3,1), (3,2) → 2个

(8,1), (8,2) → 2个 → 等等不对

重新枚举:(5,3)✓ (5,1)✓ (5,2)✓ (3,1)✓ (3,2)✓ (8,1)✓ (8,2)✓ → 共7个?

再仔细数:i=0时 j=1(5>3✓) j=2(5>8✗) j=3(5>1✓) j=4(5>2✓) → 3个

i=1时 j=2(3>8✗) j=3(3>1✓) j=4(3>2✓) → 2个

i=2时 j=3(8>1✓) j=4(8>2✓) → 2个

i=3时 j=4(1>2✗) → 0个

总计:3+2+2+0 = 7 个逆序对(修正答案)

方法二:利用冒泡排序 —— 冒泡排序的总交换次数就等于逆序对数。因为每次交换恰好消除一个逆序对。

进阶:当 n 很大时(n≤10⁵),O(n²) 会超时,需要用归并排序或树状数组在 O(nlogn) 内计算。

模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
排序定义将无序数据按规则排列为有序序列
冒泡排序相邻比较交换,每轮冒一个最大值到末尾
冒泡优化加 flag,一轮没交换就 break → 最好 O(n)
选择排序每轮选最小值,与未排序首位交换
时间复杂度冒泡/选择都是 O(n²)(冒泡优化后最好O(n))
稳定性冒泡 稳定(>才交换),选择 不稳定(长距离交换)
竞赛实际了解原理即可,实际用 sort()(快排/IntroSort)
2拓展:插入排序与 std::sort

💡 插入排序(Insertion Sort)—— 下一个要学的排序

思想:像整理扑克牌一样,每次拿一张牌插入到已排好的牌中的正确位置。

时间:最好 O(n),最坏 O(n²),平均 O(n²)

优势:对近乎有序的数据特别快!在实际中,很多排序算法(如 Timsort、IntroSort)在子数组很小时会切换到插入排序。

C++ 的 std::sort:底层使用 IntroSort(快排 + 堆排 + 插入排序的混合),平均 O(nlogn),实际竞赛中直接用 sort() 即可!

sort(a, a+n); — 一行搞定,又快又稳。

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能手写冒泡排序和选择排序的完整代码。

2. 能说出冒泡排序的优化方法(flag 提前终止)。

3. 能手推数组的排序过程(每轮状态变化)。

4. 能分析排序算法的时间复杂度和稳定性。

5. 能解释为什么选择排序不稳定(长距离交换打破相对顺序)。

6. 知道竞赛中应该用 std::sort() 而非手写 O(n²) 排序。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

冒泡排序内层循环范围写错

❌ 错误
for(j=0; j<n-1; j++) — 每轮都比较到最后,做了多余比较。
✅ 正确
for(j=0; j<n-1-i; j++) — 每轮减少 i 个已排好的元素。
口诀:内层 n-1-i,每轮少比一个!
易错2

交换操作写错

❌ 错误
a[j]=a[j+1]; a[j+1]=a[j]; — a[j] 已被覆盖,第二步赋的是新值!
✅ 正确
int temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; — 必须用临时变量。
口诀:交换三步走,temp 不能丢!
易错3

选择排序 minIdx 初始化错误

❌ 错误
int minIdx = 0; — 每轮都从0开始找,会找到已排序部分。
✅ 正确
int minIdx = i; — 从当前未排序部分的起始位置开始。
口诀:minIdx 等于 i,从当前开始找!
易错4

混淆稳定和不稳定

❌ 错误
"选择排序是稳定的,因为它每次选最小的。"
✅ 正确
选择排序是不稳定的!因为最小元素可能与当前位置相距很远,交换时跳过了中间相等的元素。
口诀:冒泡稳定相邻换,选择不稳跳着换!
易错5

竞赛中手写冒泡/选择排序

❌ 错误
竞赛中手写 O(n²) 的冒泡排序,n=10⁵ 时超时得 0 分。
✅ 正确
竞赛中直接用 #include <algorithm> + sort(a, a+n);,O(nlogn) 又快又稳。
口诀:学习手写懂原理,竞赛用 sort 拿高分!
速查排序基础速查表
知识点关键结论
冒泡排序相邻比较交换,O(n²),稳定
冒泡优化flag 标记,没交换就 break,最好 O(n)
选择排序每轮选最小,O(n²),不稳定
比较次数冒泡 = n(n-1)/2,选择 = n(n-1)/2
交换次数冒泡可能多,选择每轮最多1次
稳定性冒泡✅ 选择❌ 插入✅ 快排❌
竞赛实践用 std::sort(),O(nlogn)
📌知识点笔记卡

🫧 冒泡排序

相邻比较交换

大的往后冒泡

O(n²) 稳定

🎯 选择排序

每轮选最小

放到最前面

O(n²) 不稳定

⚡ 冒泡优化

加 flag 标记

没交换就 break

最好 O(n)

🔒 稳定性

相等元素顺序不变=稳定

冒泡✅ 选择❌

关键看交换方式

📊 复杂度

冒泡/选择 = O(n²)

快排/归并 = O(nlogn)

竞赛用 sort()

🏆 竞赛技巧

手写排序理解原理

实战用 std::sort

逆序对 = 冒泡交换次数

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

冒泡排序的核心操作是()

A. 每轮选出最小元素放到前面
B. 比较相邻元素,如果逆序就交换
C. 将数组分成两半分别排序
D. 随机选一个元素作为基准

查看参考答案

答案:B

冒泡排序的核心是比较相邻元素,如果 a[j] > a[j+1](逆序)就交换。A 是选择排序,C 是归并排序,D 是快速排序。

💡 解析思路:抓关键词「相邻」。冒泡 = 相邻比较 + 逆序交换。A 是选择排序(每轮选最小放前面),C 是归并排序,D 是快速排序。
⚠️ 易错:误选 A。选择排序也是“选最小”,但它是扫完全场再一步换到位;冒泡是每一步都和相邻元素比——“相邻”二字是分水岭。
A2选择题

对 n 个元素进行冒泡排序,最坏情况下的时间复杂度是()

A. O(n)   B. O(n log n)   C. O(n²)   D. O(2ⁿ)

查看参考答案

答案:C. O(n²)

最坏情况(逆序)下,外层 n-1 轮,内层最多 n-1 次比较,总比较次数约 n²/2 = O(n²)。

💡 解析思路:最坏 = 完全逆序,每轮都要把最大值一路换到末尾。比较次数 (n-1)+(n-2)+…+1 = n(n-1)/2,是 n² 级别。
⚠️ 易错:误选 B。O(n log n) 是快速/归并排序的水平,基于“相邻交换”的冒泡永远达不到——相邻交换每次最多消除 1 个逆序对。
A3选择题

下列排序算法中,哪个是稳定的?

A. 选择排序   B. 冒泡排序   C. 快速排序   D. 堆排序

查看参考答案

答案:B. 冒泡排序

冒泡排序只在 a[j] > a[j+1] 时交换(严格大于),相等元素不会交换位置,所以是稳定的。选择排序、快速排序、堆排序都是不稳定的。

💡 解析思路:判断稳定性的万能套路:看相等元素会不会被交换或跨越。冒泡只在 a[j] > a[j+1](严格大于)时交换,相等元素永远不换位置。
⚠️ 易错:误选 A。选择排序的“长距离交换”会跨过相等元素——这正是它不稳定的根源(详见 B 组反例题)。
A4编程题

用冒泡排序对数组 [64, 34, 25, 12, 22, 11, 90] 进行升序排列,输出每轮结束后的数组状态。

查看参考答案
#include <iostream>
using namespace std;
int main() {
    int a[] = {64,34,25,12,22,11,90};
    int n = 7;
    for(int i=0; i<n-1; i++){
        bool sw = false;
        for(int j=0; j<n-1-i; j++)
            if(a[j]>a[j+1]){
                int t=a[j]; a[j]=a[j+1]; a[j+1]=t;
                sw=true;
            }
        cout<<"Round "<<i+1<<": ";
        for(int k=0;k<n;k++) cout<<a[k]<<" ";
        cout<<endl;
        if(!sw) break;
    }
    return 0;
}
运行结果
Round 1: 34 25 12 22 11 64 90
Round 2: 25 12 22 11 34 64 90
Round 3: 12 22 11 25 34 64 90
Round 4: 12 11 22 25 34 64 90
Round 5: 11 12 22 25 34 64 90
Round 6: 11 12 22 25 34 64 90

注意:第6轮没有发生交换(flag=false),提前终止。实际上第5轮后已经有序。

思路:每轮把相邻的逆序元素交换,第 i 轮结束后末尾 i 个元素已经就位,共需 n−1 轮。

🟡 B组 · 提高(4题)

B1分析题

对数组 [9, 8, 7, 6, 5] 进行冒泡排序(升序),总共需要多少次比较和多少次交换?这属于最好还是最坏情况?

查看参考答案

这是最坏情况(数组完全逆序)。

比较次数:第1轮4次 + 第2轮3次 + 第3轮2次 + 第4轮1次 = 10 次

公式:n(n-1)/2 = 5×4/2 = 10

交换次数:每次比较都需要交换(因为完全逆序)= 10 次

每轮过程:

第1轮:[8,7,6,5,9](4次交换)

第2轮:[7,6,5,8,9](3次交换)

第3轮:[6,5,7,8,9](2次交换)

第4轮:[5,6,7,8,9](1次交换)

💡 解析思路:比较次数由循环结构写死 = n(n-1)/2 = 10,与输入无关;交换次数 = 逆序对个数,完全逆序时恰为 10,每次比较都要交换。
⚠️ 易错:以为交换次数也要“分情况算”——只有比较次数是固定的,交换次数才取决于数据。
B2编程题

编写选择排序程序,对输入的 n 个整数进行升序排列,并输出每轮选出的最小值及其原始位置。

查看参考答案
#include <iostream>
using namespace std;
int main() {
    int n; cin >> n;
    int a[1005];
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<n-1;i++){
        int minIdx = i;
        for(int j=i+1;j<n;j++)
            if(a[j]<a[minIdx]) minIdx=j;
        cout<<"Round "<<i+1<<": min="<<a[minIdx]<<" at pos "<<minIdx<<endl;
        if(minIdx!=i){
            int t=a[i]; a[i]=a[minIdx]; a[minIdx]=t;
        }
    }
    for(int i=0;i<n;i++) cout<<a[i]<<" ";
    cout<<endl;
    return 0;
}

思路:第 i 轮在 [i, n−1] 范围内找最小值的下标,与 a[i] 交换,并记录本轮选出的最小值及其原始位置。

易错:内层循环起点写成 0 而不是 i+1(重复比较已经排好的部分);或 minIdx 没有在每轮开头重新初始化为 i。

B3分析题

举一个具体例子,证明选择排序是不稳定的。要求:数组中至少有两个相等的元素,排序后它们的相对顺序发生了改变。

查看参考答案

反例:[4ₐ, 3, 4ᵦ, 2]

初始:4ₐ(位置0), 3(位置1), 4ᵦ(位置2), 2(位置3)

第1轮:在 [4ₐ, 3, 4ᵦ, 2] 中找最小 → minIdx=3(值2)

交换 a[0] 和 a[3]:4ₐ ↔ 2 → [2, 3, 4ᵦ, 4ₐ]

第2轮:在 [3, 4ᵦ, 4ₐ] 中找最小 → minIdx=1(值3)

minIdx==i,不交换 → [2, 3, 4ᵦ, 4ₐ]

第3轮:在 [4ᵦ, 4ₐ] 中找最小 → minIdx=2(值4ᵦ)

minIdx==i,不交换 → [2, 3, 4ᵦ, 4ₐ]

结果:[2, 3, 4ᵦ, 4ₐ] — 4ᵦ 在 4ₐ 前面!

排序前 4ₐ 在 4ᵦ 前面(位置0 < 位置2),排序后 4ᵦ 跑到了 4ₐ 前面(位置2 < 位置3)。

原因:第1轮将最小的 2 与 a[0]=4ₐ 交换,4ₐ 被"跳"到了位置3,越过了 4ᵦ。

💡 解析思路:构造反例三步:① 放两个相等元素(4a、4b);② 让其中一个被长距离交换跨过去;③ 观察相对顺序翻转。第一轮 4a 就被换到了最后。
⚠️ 易错:反例必须给相等元素标上下标(4a/4b),不标注就无法证明“相对顺序改变了”——这是证明稳定性题目的规范写法。
B4编程题

分别用冒泡排序和选择排序对同一个数组排序,统计各自的比较次数和交换次数,对比结果。

查看参考答案
#include <iostream>
using namespace std;
void bubble(int a[],int n,int&cmp,int&swp){
    cmp=swp=0;
    for(int i=0;i<n-1;i++){
        bool sw=false;
        for(int j=0;j<n-1-i;j++){
            cmp++;
            if(a[j]>a[j+1]){
                int t=a[j];a[j]=a[j+1];a[j+1]=t;
                swp++; sw=true;
            }
        }
        if(!sw) break;
    }
}
void selection(int a[],int n,int&cmp,int&swp){
    cmp=swp=0;
    for(int i=0;i<n-1;i++){
        int mi=i;
        for(int j=i+1;j<n;j++){
            cmp++;
            if(a[j]<a[mi]) mi=j;
        }
        if(mi!=i){
            int t=a[i];a[i]=a[mi];a[mi]=t;
            swp++;
        }
    }
}
int main(){
    int a[]={5,3,8,1,2}, b[]={5,3,8,1,2};
    int n=5, bc,bs,sc,ss;
    bubble(a,n,bc,bs);
    selection(b,n,sc,ss);
    cout<<"Bubble: cmp="<<bc<<" swp="<<bs<<endl;
    cout<<"Selection: cmp="<<sc<<" swp="<<ss<<endl;
    return 0;
}
运行结果
Bubble: cmp=10 swp=7
Selection: cmp=10 swp=3

结论:比较次数相同(都是 n(n-1)/2 = 10),但选择排序的交换次数更少(每轮最多1次 vs 冒泡可能多次)。

思路:给两种排序各自加计数器:冒泡排序固定比较 n(n−1)/2 次,选择排序的比较次数也固定,但交换次数最多 n−1 次。

易错:把“交换次数”与“比较次数”混为一谈——逆序数组上冒泡排序两者都是 n(n−1)/2,而选择排序的比较次数固定、交换次数却很少。

🔴 C组 · 挑战(4题)

C1初赛真题

对 n 个元素的数组进行冒泡排序,在最好情况下的时间复杂度为()

A. O(1)   B. O(n)   C. O(n log n)   D. O(n²)

查看参考答案

答案:B. O(n)

最好情况:数组已经有序。使用优化版冒泡排序,第1轮比较 n-1 次,没有发生任何交换 → flag=false → break。只需 1 轮,比较 n-1 次 → O(n)。

注意:如果不用 flag 优化,即使已有序也要跑完 n-1 轮,仍然是 O(n²)。

💡 解析思路:“最好 O(n)”必须配合 swapped 优化才成立:已有序时第 1 轮比较 n-1 次、0 次交换 → 直接 break。
⚠️ 易错:不加优化的基础版冒泡即使已有序也要跑满 n-1 轮 = O(n²)。教材说“冒泡最好 O(n)”时,默认指优化版。
C2初赛真题

以下排序算法中,不稳定的有()个。

① 冒泡排序   ② 选择排序   ③ 插入排序   ④ 快速排序   ⑤ 归并排序

A. 1   B. 2   C. 3   D. 4

查看参考答案

答案:B(2个)

① 冒泡排序 → 稳定(>才交换,相等不交换)

② 选择排序 → 不稳定(长距离交换可能跳过相等元素)

③ 插入排序 → 稳定(向后插入,相等时不移动)

④ 快速排序 → 不稳定(partition 时长距离交换)

⑤ 归并排序 → 稳定(合并时优先取前面的)

不稳定的有:②④,共 2 个。

💡 解析思路:记忆口诀「快选堆希不稳定,冒插归并稳定」(快排、选择、堆排、希尔不稳定;冒泡、插入、归并稳定)。②④ 共 2 个。
⚠️ 易错:误以为选择排序稳定——每轮“一步到位”的长距离交换是它的死穴。
C3综合编程

冒泡排序的"双向冒泡"优化(鸡尾酒排序):奇数轮从左到右冒泡(最大值到末尾),偶数轮从右到左冒泡(最小值到开头)。编程实现并测试。

查看参考答案
#include <iostream>
using namespace std;
void cocktailSort(int a[], int n) {
    int left = 0, right = n - 1;
    while (left < right) {
        // 正向:最大值冒到右边
        bool sw = false;
        for (int j = left; j < right; j++)
            if (a[j] > a[j+1]) {
                int t=a[j]; a[j]=a[j+1]; a[j+1]=t;
                sw = true;
            }
        right--;
        if (!sw) break;
        // 反向:最小值冒到左边
        sw = false;
        for (int j = right; j > left; j--)
            if (a[j] < a[j-1]) {
                int t=a[j]; a[j]=a[j-1]; a[j-1]=t;
                sw = true;
            }
        left++;
        if (!sw) break;
    }
}
int main() {
    int a[] = {5,1,4,2,8,0,3};
    cocktailSort(a, 7);
    for(int i=0;i<7;i++) cout << a[i] << " ";
    cout << endl;
    return 0;
}
运行结果
0 1 2 3 4 5 8

鸡尾酒排序优势:对于"乌龟"元素(小值在末尾),双向冒泡可以更快将其移到前面。平均比较次数略少于单向冒泡。

思路:鸡尾酒排序在一趟“从左到右”和一趟“从右到左”中分别确定最大值与最小值的位置,把有效区间两端同时收缩。

易错:收缩边界时忘记写 left++ 或 right--;或“本轮无交换就提前结束”的判断只在其中一个方向写了。

C4综合编程

输入 n 名学生的成绩和姓名,按成绩从高到低排序。成绩相同时,按姓名的字典序排列。要求使用稳定排序。

查看参考答案
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
struct Student {
    string name;
    int score;
};
// 自定义比较:成绩降序,成绩相同按姓名升序
bool cmp(Student a, Student b) {
    if (a.score != b.score)
        return a.score > b.score;  // 成绩高在前
    return a.name < b.name;        // 姓名字典序
}
int main() {
    int n; cin >> n;
    Student s[1005];
    for(int i=0;i<n;i++)
        cin >> s[i].name >> s[i].score;
    stable_sort(s, s+n, cmp);  // 稳定排序!
    for(int i=0;i<n;i++)
        cout << s[i].name << " " << s[i].score << endl;
    return 0;
}

关键点:使用 stable_sort 而非 sort,保证成绩和姓名都相同时保持原始输入顺序。这在多关键字排序中很重要。

思路:编写多关键字比较函数:先比成绩降序,成绩相同再按姓名字典序升序;稳定排序保证同分同名时维持原顺序。

易错:比较函数里漏写 return 语句(行为未定义,可能运行错误);或使用不稳定排序却依赖它保持原始顺序。

🏆洛谷原题精选(20道)

以下题目均与排序相关,按难度从易到难排列。建议在洛谷官网提交代码评测。

1P1177排序入门
2P1059明明的随机数入门
3P1068分数线划定入门
4P1093奖学金入门
5P1094纪念品分组普及
6P1090合并果子普及
7P1138第 k 小整数入门
8P1051谁拿了最多奖学金入门
9P1908逆序对普及
10P1116车厢重组入门
11P1080国王游戏提高
12P1223排队接水入门
13P1803凌乱的yyy/线段覆盖普及
14P1012拼数普及
15P2676超级书架入门
16P1091合唱队形普及
17P1083借教室提高
18P1966火柴排队提高
19P1250种树普及
20P1064金明的预算方案提高
🔑 刷题建议

① 先做入门题(P1177、P1059、P1138),熟悉 sort() 的基本用法

② 再做普及题(P1908逆序对、P1012拼数),掌握自定义排序和归并排序

③ 最后挑战提高题(P1080国王游戏、P1966火柴排队),需要排序+贪心/逆序对的综合能力