概念:
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,就像水中的气泡一样。
算法步骤:
以下是冒泡排序的一般步骤:
-
开始比较:从数列的第一个元素开始,比较相邻的两个元素,如果第一个元素比第二个元素大,则交换它们的位置。
-
遍历数列:对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这将使得最大的元素“冒泡”到数列的末端。
-
重复过程:由于最后的元素现在已经是最大的,所以下一次遍历时可以忽略它,只需要考虑剩下的元素。
-
继续遍历:重复步骤2和3,直到没有元素需要交换,这意味着数列已经完全排序。
冒泡排序的时间复杂度是O(n^2),其中n是数列的长度。当数列已经部分或完全排序时,冒泡排序的效率会比较高。
对于冒泡排序,我们以实际例子来进行解释:
我们有一串无规则数
EG:1 3 2 6 8 5 7 9 4
下面默认要求从小到大排序
给了一串数字之后,我们该这么用冒泡排序呢?
首先,我们比较
第一个元素和第二个元素:
1 3 2 6 8 5 7 9 4
1 3 2 6 8 5 7 9 4
两个元素是从小到大排序,不需要修改
第二个元素和第三个元素:
1 3 2 6 8 5 7 9 4
1 2 3 6 8 5 7 9 4
两个元素不是从小到大排序,需要修改
第三个元素和第四个元素:
1 2 3 6 8 5 7 9 4
1 2 3 6 8 5 7 9 4
两个元素是从小到大排序,不需要修改
第四个元素和第五个元素:
1 2 3 6 8 5 7 9 4
1 2 3 6 8 5 7 9 4
两个元素是从小到大排序,不需要修改
第五个元素和第六个元素:
1 2 3 6 8 5 7 9 4
1 2 3 6 5 8 7 9 4
两个元素不是从小到大排序,需要修改
第六个元素和第七个元素:
1 2 3 6 5 8 7 9 4
1 2 3 6 5 7 8 9 4
两个元素不是从小到大排序,需要修改
第七个元素和第八个元素:
1 2 3 6 5 7 8 9 4
1 2 3 6 5 7 8 9 4
两个元素是从小到大排序,不需要修改
第八个元素和第九个元素:
1 2 3 6 5 7 8 9 4
1 2 3 6 5 7 8 4 9
两个元素不是从小到大排序,需要修改
通过一次的排序,我们将最大的数排到了最后一位
..............
以此类推,在前一次排好的基础上,再通过一次排序,我们可以将第二大的数排到倒数第二位
..............
所以,我们排序应该有总的9-1次也就是(n-1)次,因为我们把第二小的排好后,最小的自然也就排放好了
理解思路后,我们就该开始写代码了:
注意:冒泡排序用的是双层循环
外层循环:用来表明我们要排多少次,是我们排序的次数:n-1
内层循环:我们要比较的数据
在内层循环中,为何是n-1-i的解释:
//-1表示下标,然后每次减去一个i,表明我们最后一个元素就不需要进行比较了,因为就比如第一次排序将最大的排放到了最后一位,下一次排序就不用再去理他了。
代码实现:
以下是冒泡排序的C++代码:
#include <iostream>
using namespace std;
// 冒泡排序函数
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 设置一个标志位,用于优化冒泡排序
bool swapped = false;
// 最后i个元素已经是排序好的了,所以不需要再比较
for (int j = 0; j < n - i - 1; j++) {
// 相邻元素两两比较
if (arr[j] > arr[j + 1]) {
// 如果前一个元素大于后一个元素,则交换它们
swap(arr[j], arr[j + 1]);
swapped = true;
}
}
// 如果在这一轮排序中没有发生交换,说明数组已经排序完成
if (!swapped)
break;
}
}
// 交换两个元素的函数
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
效果展示:
// 打印数组的函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << "\n";
}
// 主函数
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
printArray(arr, n);
// 调用冒泡排序函数
bubbleSort(arr, n);
cout << "Sorted array: ";
printArray(arr, n);
return 0;
}
Original array: 64 34 25 12 22 11 90
Sorted array: 11 12 22 25 34 64 90
D:\2024C语言\data-structure\bubbleSort\x64\Debug\bubbleSort.exe (进程 24128)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
在这个示例中,bubbleSort
函数实现了冒泡排序算法。它通过两层循环来遍历数组,内层循环负责比较和交换相邻的元素。如果在某一轮遍历中没有发生任何交换,那么swapped
标志位将保持为false
,这表明数组已经排序完成,可以提前结束排序过程。swap
函数用于交换两个元素的值。printArray
函数用于打印数组,让我们可以看到排序前后的对比。main
函数中定义了一个未排序的数组,并调用bubbleSort
函数对其进行排序。
优化:
冒泡排序算法虽然简单,但效率并不是特别高,特别是在数据量大且数据初始状态较为混乱的情况下。然而,我们可以通过以下几种方式来优化冒泡排序算法:
-
设置标志位:在每一轮遍历中,如果发现没有发生任何交换,说明数组已经有序,可以提前结束排序过程。这可以避免不必要的比较。
-
记录最后一次交换的位置:在每一轮遍历中,记录最后一次发生交换的位置。这个位置之后的元素在下一轮遍历中就不需要再比较了,因为它们已经是有序的。
-
双向冒泡排序(鸡尾酒排序):在每一轮遍历中,先从左到右比较并交换元素,然后从右到左比较并交换元素。这样,每一轮遍历可以同时将最大的元素移动到右边和最小的元素移动到左边。
-
使用更高效的排序算法:如果可能的话,可以考虑使用更高效的排序算法,如快速排序、归并排序或堆排序,这些算法在大多数情况下的性能都优于冒泡排序。
下面是使用第二种方法优化冒泡排序的C++代码:
#include <iostream>
using namespace std;
// 优化的冒泡排序函数
void optimizedBubbleSort(int arr[], int n) {
int start = 0, end = n - 1;
int newEnd = 0; // 记录最后一次交换的位置
while (start < end) {
// 从左到右遍历
for (int i = start; i < end; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr[i], arr[i + 1]);
newEnd = i; // 更新最后一次交换的位置
}
}
end = newEnd; // 更新end的值,因为end之后的元素已经是有序的
// 从右到左遍历
for (int i = end; i > start; i--) {
if (arr[i] < arr[i - 1]) {
swap(arr[i], arr[i - 1]);
start = i; // 更新start的值,因为start之前的元素已经是有序的
}
}
}
}
// 交换两个元素的函数
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
// 打印数组的函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << "\n";
}
// 主函数
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
printArray(arr, n);
// 调用优化的冒泡排序函数
optimizedBubbleSort(arr, n);
cout << "Sorted array: ";
printArray(arr, n);
return 0;
}
在这个示例中,optimizedBubbleSort
函数实现了双向冒泡排序,即鸡尾酒排序。它在每一轮遍历中同时将最大的元素移动到数组的一端和最小的元素移动到数组的另一端。这种方法可以减少遍历的次数,从而提高排序效率。
即使我们对冒泡排序进行了优化,其时间复杂度在最坏的情况下仍然是O(n^2),其中n是数组的长度。这是因为在最坏的情况下,我们需要进行n-1轮遍历,每轮遍历都需要比较n-k个元素,其中k是已经排序好的元素的数量。即使我们使用了优化措施,比如设置标志位来避免不必要的遍历,或者记录最后一次交换的位置来减少后续遍历的范围,这些优化措施也只能提高算法的效率,但并不能改变最坏情况下的时间复杂度。
然而,在最好的情况下(即输入数组已经是完全有序的),冒泡排序的时间复杂度可以降低到O(n)。这是因为只需要进行一轮遍历,就可以确定数组已经有序,从而不需要进一步的比较和交换。
优化措施可以减少实际的执行时间,尤其是在部分有序的数据集上,但它们不会改变算法在最坏情况下的理论时间复杂度。如果需要处理大量数据或者对性能有较高要求,通常会选择时间复杂度更低的排序算法,如快速排序、归并排序或堆排序,这些算法在平均和最坏情况下的时间复杂度通常为O(n log n)。
特性:
- 时间复杂度为 O(n2)、自适应排序:各轮“冒泡”遍历的数组长度依次为 n−1、n−2、…、2、1 ,总和为 (n−1)n/2 。在引入
flag
优化后,最佳时间复杂度可达到 O(n) 。 - 空间复杂度为 O(1)、原地排序:指针 i 和 j 使用常数大小的额外空间。
- 稳定排序:由于在“冒泡”中遇到相等元素不交换。