作为一名对技术充满热情的学习者,我一直以来都深刻地体会到知识的广度和深度。在这个不断演变的数字时代,我远非专家,而是一位不断追求进步的旅行者。通过这篇博客,我想分享我在某个领域的学习经验,与大家共同探讨、共同成长。请大家以开放的心态阅读,相信你们也会在这段知识之旅中找到启示。
前言
冒泡排序顾名思义,想泡泡一样慢慢冒上去,今天,我们就来深刻理解冒泡排序的排序步骤,通过一些联系给大家讲解如何解决有关冒泡排序算法的一些问题。
一、什么是冒泡排序
冒泡排序(Bubble Sort)是一种基础的排序算法。它的工作原理是通过重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来
。遍历数列的工作是重复进行的,直到没有再需要交换的元素为止,这时数列就完全排序好了。
- 冒泡排序的名称来自于较小的元素会像“气泡”一样逐渐通过交换“浮”到数列的顶端,而较大的元素则沉到底部。
这里有一个简单的冒泡排序算法的例子:
假设我们有一个数组 arr[5] = {5, 2, 9, 1, 5},我们要对它进行升序排序。
冒泡排序的具体步骤如下:
1.从数组的第一个元素开始,比较相邻元素的值。
2.如果前一个元素比后一个元素大(针对升序排序),交换它们的位置。
3.对数组的每一对相邻元素重复步骤1和2,直到数组的最后一个元素,这时最大的数就“冒泡”到了数组的最后位置。
4.因为最大的数已经在正确的位置了,所以可以忽略它,对数组的剩余部分重复步骤1到3的过程。
重复上述过程,每次遍历时忽略已确定位置的元素,直到没有任何一对数字需要交换,排序就完成了。
以下是对上述数组进行冒泡排序的过程:
初始数组: [5, 2, 9, 1, 5]
- 第一趟冒泡:[2, 5, 1, 5, 9](最大的数9已到达最终位置)
- 第二趟冒泡:[2, 1, 5, 5, 9](较大数5进行了移动)
- 第三趟冒泡:[1, 2, 5, 5, 9](较小的数1移到了开头)
- 第四趟冒泡:[1, 2, 5, 5, 9](数列已经排序完成,没有任何交换)
虽然冒泡排序在理解和实现上非常简单,但它通常不适合大规模数据的排序,因为其平均情况和最坏情况的时间复杂度都是O(n^2),其中n是数组的大小。因此,在需要高效排序的应用中,通常会采用更高效的算法,如快速排序、归并排序或堆排序
二、实战练习
现在我们对下面的数组冒泡排序:
arr = [64, 34, 25, 12, 22, 11, 90]
下面是详细步骤,希望同学们可以先自己练习:
初始数组状态:
[64, 34, 25, 12, 22, 11, 90]
- 第1趟冒泡:
(64, 34) 比较,需要交换 -> [34, 64, 25, 12, 22, 11, 90] (64, 25) 比较,需要交换 -> [34, 25, 64, 12, 22, 11, 90] (64, 12) 比较,需要交换 -> [34, 25, 12, 64, 22, 11, 90] (64, 22) 比较,需要交换 -> [34, 25, 12, 22, 64, 11, 90] (64, 11) 比较,需要交换 -> [34, 25, 12, 22, 11, 64, 90] (64, 90) 比较,不需交换 -> [34, 25, 12, 22, 11, 64, 90]
- 第2趟冒泡:
(34, 25) 比较,需要交换 -> [25, 34, 12, 22, 11, 64, 90] (34, 12) 比较,需要交换 -> [25, 12, 34, 22, 11, 64, 90] (34, 22) 比较,需要交换 -> [25, 12, 22, 34, 11, 64, 90] (34, 11) 比较,需要交换 -> [25, 12, 22, 11, 34, 64, 90] (34, 64) 比较,不需交换 -> [25, 12, 22, 11, 34, 64, 90] (64, 90) 比较,不需交换 -> [25, 12, 22, 11, 34, 64, 90]
- 第3趟冒泡:
(25, 12) 比较,需要交换 -> [12, 25, 22, 11, 34, 64, 90] (25, 22) 比较,需要交换 -> [12, 22, 25, 11, 34, 64, 90] (25, 11) 比较,需要交换 -> [12, 22, 11, 25, 34, 64, 90] (25, 34) 比较,不需交换 -> [12, 22, 11, 25, 34, 64, 90] (34, 64) 比较,不需交换 -> [12, 22, 11, 25, 34, 64, 90] (64, 90) 比较,不需交换 -> [12, 22, 11, 25, 34, 64, 90]
- 第4趟冒泡:
(12, 22) 比较,不需交换 -> [12, 22, 11, 25, 34, 64, 90] (22, 11) 比较,需要交换 -> [12, 11, 22, 25, 34, 64, 90] (22, 25) 比较,不需交换 -> [12, 11, 22, 25, 34, 64, 90] (25, 34) 比较,不需交换 -> [12, 11, 22, 25, 34, 64, 90] (34, 64) 比较,不需交换 -> [12, 11, 22, 25, 34, 64, 90] (64, 90) 比较,不需交换 -> [12, 11, 22, 25, 34, 64, 90]
- 第5趟冒泡:
(12, 11) 比较,需要交换 -> [11, 12, 22, 25, 34, 64, 90] (12, 22) 比较,不需交换 -> [11, 12, 22, 25, 34, 64, 90] (22, 25) 比较,不需交换 -> [11, 12, 22, 25, 34, 64, 90] (25, 34) 比较,不需交换 -> [11, 12, 22, 25, 34, 64, 90] (34, 64) 比较,不需交换 -> [11, 12, 22, 25, 34, 64, 90] (64, 90) 比较,不需交换 -> [11, 12, 22, 25, 34, 64, 90]
此时,数组已经排序完成,没有发生任何交换。排序过程到此结束。
最终排序好的数组为:
[11, 12, 22, 25, 34, 64, 90]
这个例子通过每一趟冒泡的详细步骤,展示了冒泡排序如何一步一步将无序数组变为有序。同学们可以检查一下自己做的有没有问题。
三、时间空间复杂度
- 冒泡排序的时间复杂度取决于数组的初始排序状态以及数组中元素的数量。
- 最坏情况时间复杂度(Worst-case time complexity):是O(n^2),这发生在数组完全反序时,每次比较都需要进行交换。
- 平均时间复杂度(Average time complexity):也是O(n^2),因为平均情况下,列表可能是随机排序的,所以排序仍然可能要进行大量的比较和交换。
- 最佳情况时间复杂度(Best-case time complexity):是O(n),这发生在数组已经是完全排序的状态。冒泡排序可以通过增加一个标志位来检测某次遍历中是否发生了交换;如果没有发生交换,表示数组已经有序,直接结束排序过程。
- 冒泡排序的空间复杂度是O(1),也就是说,它在进行排序时只需要常数级别的额外空间。这个额外空间通常用于存储临时变量,用于在交换过程中暂时存储一个元素的值。
由于冒泡排序是在原地进行比较和交换的,没有需要额外存储结构,所以其额外的内存需求并不随着排序的元素数量增加,因此被视为常量空间复杂度。在实际实现时,不管你要排序多少数据,所需的额外空间都保持不变。这就是为什么冒泡排序被认为是一种空间复杂度很低的排序算法。
四、冒泡排序优缺点
冒泡排序的优缺点如下:
- 优点:
1.简单易懂:冒泡排序的算法非常直观和容易实现,对于理解排序算法的基本概念很有帮助。
2.原地排序:冒泡排序是一种原地排序算法,不需要额外的存储空间,空间复杂度为O(1)。
3.稳定排序:在排序过程中不会改变相同元素的初始相对顺序,因此是稳定的排序算法。
4.检测到有序:冒泡排序可以提前停止,在数组已经排序的情况下效率较高,因为它可以在没有必要的时候提前结束, - 缺点:
1.低效率:冒泡排序在最坏情况和平均情况下的时间复杂度都是O(n^2),这使得在处理大量数据时效率十分低下。
2.过多的交换操作:冒泡排序可能涉及许多不必要的交换操作,尤其是在数组初始状态近乎有序的情况下。
实际应用中的注意事项:
- 数据规模:由于其较高的时间复杂度,在数据量较大时不推荐使用冒泡排序。
- 实用性:对于小型数据集或教学演示来说,冒泡排序是很合适的,但在实际应用中通常会选择更有效的排序算法。
- 算法优化:可以通过设置一个标志位来标记某一趟排序过程中是否有数据交换,如果没有则提前终止排序,可以提升一定的效率。
如何适当使用冒泡排序:
- 当数据量小且不要求高性能的时候,可以使用冒泡排序。
- 在不关心算法效率,而是要演示排序过程时,使用冒泡排序是一个不错的选择,因为它的原理清晰易懂。
- 如果需要一个不占用额外内存空间的排序算法,冒泡排序是一个选项。
- 如果数据几乎已经排好序,冒泡排序可能会比其他O(n^2)时间复杂度的排序算法表现得更好,因为可以提前终止。
总体来说,冒泡排序由于算法效率较低,在实用的软件开发中较少使用,更多的是作为入门级别理解排序算法的工具,或者在数据量不大时简单使用。
五、经典面试题
面试题:“在不使用额外的数组或集合类的情况下,用Java写一个程序来移除一个有序整数数组中的重复元素,并返回新数组的长度。要求空间复杂度为O(1)。”
这个面试题要求面试者在原地进行操作,需要对数组进行修改,同时不增加额外的存储空间,以下是完成这个任务的一种方法。
面试回答(包括代码解释):
public class RemoveDuplicatesFromSortedArray { // 函数用来移除有序数组中的重复元素 public static int removeDuplicates(int[] nums) { // 如果数组为空或只有一个元素,则不存在重复项需要移除 if (nums.length == 0 || nums.length == 1) { return nums.length; } // 指针i表示不重复部分的最后一个位置 int i = 0; // 遍历数组 for (int j = 1; j < nums.length; j++) { // 如果相邻的元素不相等,则将j指向的元素复制到i+1的位置 if (nums[j] != nums[i]) { i++; nums[i] = nums[j]; } // 如果相邻元素相等,则j继续前移,直到找到下一个不同的元素 } // 返回新的数组长度,因为i是索引值,真正的长度应该是i+1 return i + 1; } // 主函数用来测试上面的函数 public static void main(String[] args) { // 示例数组 int[] sortedArray = {1, 1, 2, 2, 3, 4, 4, 5, 5, 5}; // 移除重复元素 int newLength = removeDuplicates(sortedArray); // 打印新数组长度和新数组内容 System.out.println("New length: " + newLength); for (int i = 0; i < newLength; i++) { System.out.print(sortedArray[i] + " "); } } }
在这段代码中:
- 我们有一个
removeDuplicates
函数,该函数接收一个有序整数数组作为参数。 - 我们检查数组的长度,快速返回空或单元素数组的情况。
- 我们使用两个指针
i
和j
。i
表示无重复部分的末尾,而j
用于遍历数组。 - 如果
nums[j]
与nums[i]
不同,这意味着我们发现了一个新的独特元素。因此,我们递增i
指针,然后将nums[j]
的值复制到nums[i]
,这样在前i+1
个元素中就没有重复项。 - 最后,函数返回无重复元素的新长度,这是
i+1
,因为数组索引是从0开始的。
主函数中,我们创建了一个示例数组,并使用removeDuplicates
函数来移除重复项并打印新数组的长度和元素。这个问题非常实用,因为它处理了原地数组修改,这是在进行内存优化时常常必需的。这个方法是对双指针技术的一个很好的展示,也显示了如何在不增加额外空间的情况下有效地解决问题。
总结
以上是对冒泡排序算法总结,整体看来,冒泡排序还是比较简单的,学起来也比较容易,希望同学们可以多加练习。后面我将依次给大家介绍排序算法,不论是初学者,还是考研,准备面试的同学都可可以来看看。能力有限,希望可以给大家带来帮助。
感谢大家抽出宝贵的时间来阅读博主的博客,新人博主,感谢大家关注点赞,祝大家未来的学习工作生活一帆风顺,加油!!!