三种排序算法:冒泡、选择、插入(保姆级讲解)
一、先理解排序是什么?
排序就是把一堆乱序的数字,按从小到大(或从大到小)排好
比如:[5, 2, 8, 1, 9] → 排序后 → [1, 2, 5, 8, 9]
二、冒泡排序(Bubble Sort)
形象比喻:像水泡一样往上冒
想象一排水泡,轻的(小的)往上冒,重的(大的)往下沉
排序过程动画(脑补):
初始:[5, 2, 8, 1, 9]
第1轮:比较相邻的两个数,大的往后移
[2, 5, 8, 1, 9] ← 5和2比较,交换
[2, 5, 1, 8, 9] ← 8和1比较,交换
[2, 5, 1, 8, 9] ← 8和9比较,不交换
最大的9已经"冒"到最后
第2轮:[2, 5, 1, 8, 9]
[2, 1, 5, 8, 9] ← 5和1比较,交换
[2, 1, 5, 8, 9] ← 5和8比较,不交换
次大的8到倒数第二
第3轮:[2, 1, 5, 8, 9]
[1, 2, 5, 8, 9] ← 2和1比较,交换
继续...
最终:[1, 2, 5, 8, 9]
C#代码(带详细注释):
using System;
class BubbleSortExample
{
static void Main()
{
int[] numbers = { 5, 2, 8, 1, 9 };
Console.WriteLine("排序前:" + string.Join(", ", numbers));
// 冒泡排序
for (int i = 0; i < numbers.Length - 1; i++) // 外层:轮数
{
Console.WriteLine($"\n第{i+1}轮开始:");
for (int j = 0; j < numbers.Length - 1 - i; j++) // 内层:比较
{
// 如果前面的数比后面的大,就交换
if (numbers[j] > numbers[j + 1])
{
// 交换两个数
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
Console.WriteLine($" 交换 {numbers[j+1]} 和 {numbers[j]} → " + string.Join(", ", numbers));
}
}
}
Console.WriteLine("\n排序后:" + string.Join(", ", numbers));
}
}
输出结果:
排序前:5, 2, 8, 1, 9
第1轮开始:
交换 5 和 2 → 2, 5, 8, 1, 9
交换 8 和 1 → 2, 5, 1, 8, 9
第2轮开始:
交换 5 和 1 → 2, 1, 5, 8, 9
第3轮开始:
交换 2 和 1 → 1, 2, 5, 8, 9
第4轮开始:
排序后:1, 2, 5, 8, 9
三、选择排序(Selection Sort)
形象比喻:每次选最矮的排队
想象排队,每次都找出最矮的人,让他站到前面
排序过程动画(脑补):
初始:[5, 2, 8, 1, 9]
第1轮:从所有数中找最小的
最小是1,和第一个数5交换 → [1, 2, 8, 5, 9]
第2轮:从剩下的[2,8,5,9]中找最小的
最小是2,已经在第二位,不交换 → [1, 2, 8, 5, 9]
第3轮:从剩下的[8,5,9]中找最小的
最小是5,和8交换 → [1, 2, 5, 8, 9]
第4轮:从剩下的[8,9]中找最小的
最小是8,已经在正确位置
完成!
C#代码(带详细注释):
using System;
class SelectionSortExample
{
static void Main()
{
int[] numbers = { 5, 2, 8, 1, 9 };
Console.WriteLine("排序前:" + string.Join(", ", numbers));
// 选择排序
for (int i = 0; i < numbers.Length - 1; i++) // 外层:当前位置
{
Console.WriteLine($"\n第{i+1}轮:找从第{i+1}个开始的最小值");
int minIndex = i; // 假设当前位置是最小的
// 在剩下的数中找真正的最小值
for (int j = i + 1; j < numbers.Length; j++)
{
if (numbers[j] < numbers[minIndex])
{
minIndex = j;
}
}
// 如果找到的最小值不在当前位置,就交换
if (minIndex != i)
{
Console.WriteLine($" 找到最小值 {numbers[minIndex]},与 {numbers[i]} 交换");
int temp = numbers[i];
numbers[i] = numbers[minIndex];
numbers[minIndex] = temp;
Console.WriteLine($" 交换后:" + string.Join(", ", numbers));
}
else
{
Console.WriteLine($" 最小值 {numbers[minIndex]} 已在正确位置");
}
}
Console.WriteLine("\n排序后:" + string.Join(", ", numbers));
}
}
四、插入排序(Insertion Sort)
形象比喻:像打扑克牌一样插牌
想象手里已经有一些排好序的牌,每拿到一张新牌,就找到合适位置插进去
排序过程动画(脑补):
初始:[5, 2, 8, 1, 9]
手里:空
第1张:拿到5 → 手里:[5]
第2张:拿到2,比5小,插到5前面 → 手里:[2, 5]
第3张:拿到8,比5大,插到最后 → 手里:[2, 5, 8]
第4张:拿到1,比2小,插到最前面 → 手里:[1, 2, 5, 8]
第5张:拿到9,比8大,插到最后 → 手里:[1, 2, 5, 8, 9]
C#代码(带详细注释):
using System;
class InsertionSortExample
{
static void Main()
{
int[] numbers = { 5, 2, 8, 1, 9 };
Console.WriteLine("排序前:" + string.Join(", ", numbers));
// 插入排序
for (int i = 1; i < numbers.Length; i++) // 从第二个数开始(第一个数默认已排序)
{
Console.WriteLine($"\n处理第{i+1}个数:{numbers[i]}");
int current = numbers[i]; // 当前要插入的数
int j = i - 1; // 前面已排序部分的最后一个索引
// 将比当前数大的都往后移
while (j >= 0 && numbers[j] > current)
{
numbers[j + 1] = numbers[j]; // 往后移
j--;
}
// 插入到正确位置
numbers[j + 1] = current;
Console.WriteLine($" 插入后:" + string.Join(", ", numbers));
}
Console.WriteLine("\n排序后:" + string.Join(", ", numbers));
}
}
五、三种排序对比(重要!)
| 特性 | 冒泡排序 | 选择排序 | 插入排序 |
|---|---|---|---|
| 思想 | 相邻比较,大的往后 | 每次选最小的放前面 | 像插牌一样插入 |
| 时间复杂度 | O(n²) | O(n²) | O(n²) |
| 空间复杂度 | O(1) | O(1) | O(1) |
| 优点 | 简单易懂 | 交换次数少 | 对部分有序数据快 |
| 缺点 | 效率低 | 不稳定 | 数据量大时慢 |
| 适合场景 | 教学、小数据 | 数据量小 | 数据基本有序 |
练习题(带答案)
练习题1:冒泡排序手动模拟
题目:用手动模拟冒泡排序对数组 [6, 3, 9, 2, 7] 进行排序
要求:写出每一轮比较和交换的过程
答案:
初始:[6, 3, 9, 2, 7]
第1轮:
比较6和3,交换 → [3, 6, 9, 2, 7]
比较6和9,不交换
比较9和2,交换 → [3, 6, 2, 9, 7]
比较9和7,交换 → [3, 6, 2, 7, 9]
第2轮:
比较3和6,不交换
比较6和2,交换 → [3, 2, 6, 7, 9]
比较6和7,不交换
第3轮:
比较3和2,交换 → [2, 3, 6, 7, 9]
第4轮:
比较2和3,不交换
结果:[2, 3, 6, 7, 9]
练习题2:选择排序C#代码填空
题目:补全选择排序代码中的空白
void SelectionSort(int[] array)
{
for (int i = 0; i < array.Length - 1; i++)
{
int minIndex = ______; // 填空1
for (int j = ______; j < array.Length; j++) // 填空2
{
if (array[j] < array[minIndex])
{
minIndex = ______; // 填空3
}
}
if (minIndex != i)
{
// 交换
int temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
}
答案:
// 填空1:i
// 填空2:i + 1
// 填空3:j
练习题3:插入排序逻辑判断
题目:对数组 [4, 1, 3, 2] 进行插入排序,回答以下问题:
- 处理第2个元素(1)时,需要向前比较几次?
- 处理第4个元素(2)时,它最终插入到什么位置?
- 整个排序过程中,总共进行了多少次元素移动?
答案:
处理过程:
初始:[4, 1, 3, 2]
第2个元素(1):
与4比较,4>1,4后移 → [4, 4, 3, 2]
插入1到第1位 → [1, 4, 3, 2] // 比较1次,移动1次
第3个元素(3):
与4比较,4>3,4后移 → [1, 4, 4, 2]
与1比较,1<3,停止 → 插入3到第2位 → [1, 3, 4, 2] // 比较2次,移动1次
第4个元素(2):
与4比较,4>2,4后移 → [1, 3, 4, 4]
与3比较,3>2,3后移 → [1, 3, 3, 4]
与1比较,1<2,停止 → 插入2到第2位 → [1, 2, 3, 4] // 比较3次,移动2次
答案:
1. 比较1次
2. 插入到第2位(索引1)
3. 总共移动次数:1 + 1 + 2 = 4次
进阶练习题(挑战)
挑战题1:优化冒泡排序
题目:冒泡排序可以优化:如果某一轮没有发生交换,说明已经排好序了。请修改下面的冒泡排序,添加这个优化。
void BubbleSort(int[] array)
{
for (int i = 0; i < array.Length - 1; i++)
{
for (int j = 0; j < array.Length - 1 - i; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
答案:
void BubbleSortOptimized(int[] array)
{
bool swapped;
for (int i = 0; i < array.Length - 1; i++)
{
swapped = false; // 记录本轮是否发生交换
for (int j = 0; j < array.Length - 1 - i; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
swapped = true; // 发生了交换
}
}
// 如果本轮没有交换,说明已经排好序了
if (!swapped)
{
break; // 提前结束
}
}
}
挑战题2:理解排序稳定性
题目:有一个学生数组,包含姓名和成绩。先按姓名排序,再按成绩排序。哪种排序算法能保持姓名顺序?
示例数据:
[("张三", 85), ("李四", 90), ("张三", 80)]
按成绩排序后,两个"张三"的相对顺序是否改变?
答案:
- 稳定排序:冒泡排序、插入排序(相等元素不交换,保持原顺序)
- 不稳定排序:选择排序(可能改变相等元素的顺序)
挑战题3:实际应用场景
题目:在以下场景中,应该选择哪种排序算法?为什么?
- 手机通讯录(经常添加新联系人)
- 游戏排行榜(每次得分更新都要重新排序)
- 批量导入学生成绩(一次性导入1000条)
答案:
- 插入排序:因为通讯录基本有序,新联系人很少,插入排序对基本有序数据效率高
- 冒泡排序或选择排序:数据量小(比如前100名),简单实现即可
- 都不选,用系统自带的Array.Sort():实际开发中不用自己写排序,系统库更高效
学习建议
- 先理解思想:不要急着背代码,先理解排序过程
- 手动模拟:用纸笔手动排几遍,感受过程
- 写注释:每行代码都写上注释,说明在做什么
- 打印过程:像上面的例子一样,打印每一轮的结果
- 对比学习:三种排序一起学,对比它们的不同
记住:这些基础排序算法虽然效率不高,但它们是理解更复杂算法的基础!