冒泡排序是一种简单的排序算法,它重复地遍历要排序的列表,比较相邻的元素,如果顺序错误就交换它们。
基本思想
从列表的第一个元素开始,比较相邻的两个元素
如果顺序错误(前一个比后一个大),就交换它们
对每一对相邻元素重复这个过程,直到列表末尾
重复上述步骤,每次遍历都会将最大的元素"冒泡"到正确位置
int[] arr = { 6, 3, 4, 1, 2 };
for (int i = 0; i < arr.Length - 1; i++)
{
for (int j = 0; j < arr.Length - i- 1; j++)
{
if (arr[j] > arr[j+1])
{
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
Console.WriteLine(string.Join(",",arr));
}
为什么是n-1?
有n个元素,需要确定n个位置
但每轮冒泡可以确定1个最大元素的位置
当确定了n-1个元素的位置后,最后1个元素自然就在正确位置
所以只需要
n-1
轮
原始数组: [5, 3, 8, 1, 2] (n=5)
第1轮: 将最大的8冒泡到最后 → [3, 5, 1, 2, 8]
第2轮: 将第二大的5冒泡到最后 → [3, 1, 2, 5, 8]
第3轮: 将第三大的3冒泡到最后 → [1, 2, 3, 5, 8]
第4轮: 将第四大的2冒泡到最后 → [1, 2, 3, 5, 8]
此时只剩下1个元素[1],它已经在正确位置,不需要第5轮!
为什么是n-i-1
因为每一轮排序后,最大的元素会“冒泡”到当前未排序序列的末尾。因此,下一轮就不需要再比较已经排好序的部分。 第0轮(i=0)时,未排序部分包括所有n个元素,我们需要比较n-1次(因为比较是相邻两个元素,所以从0到n-2)。 第1轮(i=1)时,最后一个元素已经排好序,所以只需要比较前n-1个元素,即比较n-2次。 所以,内层循环的边界是j < n - i - 1。
比如:内层循环中我们比较的是 arr[j]
和 arr[j+1]
:
当
j = n-2
时,比较的是arr[n-2]
和arr[n-1]
如果
j
达到n-1
,那么j+1 = n
就会数组越界!
初始: [5, 3, 8, 1, 2]
↑ ↑ ↑ ↑ ↑
索引: 0 1 2 3 4
第1轮(i=0): j < 4 → 比较: 0-1, 1-2, 2-3, 3-4
第2轮(i=1): j < 3 → 比较: 0-1, 1-2, 2-3 (跳过索引4)
第3轮(i=2): j < 2 → 比较: 0-1, 1-2 (跳过索引3-4)
第4轮(i=3): j < 1 → 比较: 0-1 (跳过索引2-4)
冒泡排序 vs 选择排序
冒泡排序虽然效率不高,但它的实现简单,是理解排序算法原理的好起点!
1. 用生活中的比喻开始
想象一下你在整理一排水杯,从左到右按从矮到高的顺序排列:
初始顺序:5, 3, 8, 1, 2
冒泡排序就像这样工作:
从左边开始,比较相邻的两个水杯
如果左边的比右边的高,就交换它们的位置
一轮比较下来,最高的水杯就会"冒泡"到最右边
重复这个过程,直到所有水杯都排好序
2. 简单直观的解释
// 初始:5, 3, 8, 1, 2
// 第1轮冒泡:
// 比较5和3 → 交换 → 3,5,8,1,2
// 比较5和8 → 不交换 → 3,5,8,1,2
// 比较8和1 → 交换 → 3,5,1,8,2
// 比较8和2 → 交换 → 3,5,1,2,8 (8冒泡到最后)
// 第2轮冒泡:
// 比较3和5 → 不交换 → 3,5,1,2,8
// 比较5和1 → 交换 → 3,1,5,2,8
// 比较5和2 → 交换 → 3,1,2,5,8 (5冒泡到倒数第二)
// 继续这个过程...
3. 基础代码演示(带详细注释)
using System;
class Program
{
static void Main()
{
// 我们要排序的数字数组
int[] numbers = { 5, 3, 8, 1, 2 };
Console.WriteLine("=== 冒泡排序演示 ===");
Console.WriteLine("原始数组: " + string.Join(", ", numbers));
// 调用冒泡排序方法
BubbleSort(numbers);
Console.WriteLine("\n最终结果: " + string.Join(", ", numbers));
}
static void BubbleSort(int[] arr)
{
int n = arr.Length;
// 外层循环:控制需要多少轮冒泡
// 每轮会让一个最大的数"冒泡"到正确位置
for (int i = 0; i < n - 1; i++)
{
Console.WriteLine($"\n🎯 第{i+1}轮冒泡开始:");
Console.WriteLine("当前数组: " + string.Join(", ", arr));
// 内层循环:进行相邻元素的比较和交换
for (int j = 0; j < n - i - 1; j++)
{
// 显示比较的两个数字
Console.Write($" 比较 [{arr[j]}] 和 [{arr[j+1]}]");
// 如果左边的数比右边大,就交换它们
if (arr[j] > arr[j + 1])
{
// 交换两个数字
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
Console.WriteLine(" → 🔄 交换!");
}
else
{
Console.WriteLine(" → ✅ 顺序正确,不交换");
}
}
// 显示本轮结束后的结果
Console.WriteLine($"第{i+1}轮结束: " + string.Join(", ", arr));
// 显示哪个数字"冒泡"到了右边
if (n - i - 1 > 0)
{
Console.WriteLine($"数字 {arr[n - i - 1]} 冒泡到了正确位置");
}
}
}
}
4. 可视化执行过程
用箭头和颜色展示每一轮的变化:
初始: [5, 3, 8, 1, 2]
第1轮:
5↔3 → 交换 → [3, 5, 8, 1, 2]
5↔8 → 不换 → [3, 5, 8, 1, 2]
8↔1 → 交换 → [3, 5, 1, 8, 2]
8↔2 → 交换 → [3, 5, 1, 2, 8] ← 8冒泡到最后
第2轮:
3↔5 → 不换 → [3, 5, 1, 2, 8]
5↔1 → 交换 → [3, 1, 5, 2, 8]
5↔2 → 交换 → [3, 1, 2, 5, 8] ← 5冒泡到倒数第二
第3轮:
3↔1 → 交换 → [1, 3, 2, 5, 8]
3↔2 → 交换 → [1, 2, 3, 5, 8] ← 3冒泡到倒数第三
第4轮:
1↔2 → 不换 → [1, 2, 3, 5, 8] ← 排序完成!
5. 关键概念解释
为什么叫"冒泡"排序?
因为大的数字会像气泡一样慢慢向上浮(向右移动)到正确的位置。
相邻比较的重要性
// 我们只比较相邻的两个元素
if (arr[j] > arr[j + 1]) // j和j+1是相邻的
{
// 交换它们
}
为什么内层循环条件是 j < n - i - 1
?
n - i
:排除已经排好序的元素(每轮结束后,右边的i个元素已经有序)-1
:防止数组越界(因为我们要比较j和j+1)
6. 优化版本:提前结束
如果一轮中没有发生任何交换,说明数组已经有序,可以提前结束:
static void OptimizedBubbleSort(int[] arr)
{
int n = arr.Length;
bool swapped; // 标记是否发生了交换
for (int i = 0; i < n - 1; i++)
{
swapped = false; // 每轮开始时重置标记
Console.WriteLine($"\n第{i+1}轮开始:");
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
// 交换代码...
swapped = true; // 发生了交换
}
}
// 如果一轮中没有发生交换,说明已经有序
if (!swapped)
{
Console.WriteLine("🌟 没有发生交换,数组已经有序!");
break; // 提前结束排序
}
}
}
7. 动手练习
练习1: 修改代码实现从大到小排序
// 提示:只需要修改比较符号
// 把 > 改为 <
练习2: 对字符串数组按字母顺序排序
string[] words = {"banana", "apple", "cherry", "date"};
// 提示:使用 string.Compare(str1, str2)
8. 常见问题解答
Q: 冒泡排序和选择排序有什么区别? A:
选择排序:每轮找到最小的放到前面
冒泡排序:每轮通过相邻比较让最大的冒泡到最后
Q: 冒泡排序快吗? A: 对于小数据量可以,大数据量比较慢。但实现简单,容易理解。
Q: 为什么内层循环每次都要从头开始? A: 因为我们需要确保每个大的数字都能一步步"冒泡"到正确位置。
9. 记忆口诀
"相邻比较,大的右移, 一轮结束,最大沉底, 重复进行,直到有序。"
10. 总结
冒泡排序的核心思想:
比较:比较相邻的两个元素
交换:如果顺序错误就交换
重复:多次遍历直到没有交换发生