排序算法之冒泡排序

一.冒泡排序

到现在为止我们已经完成了直接插入排序、二分插入排序以及简单选择排序,我们今天来看一下冒泡排序(本文默认按照从小到大的顺序对数组进行排序)。

1.原理

冒泡排序跟选择排序有一点思想上的接近,上文我们已经看过了选择排序,今天的内容就很好理解了。

这个算法的名字由来是因为越大的数组元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

现在我们想象一下,泡泡从水里升起,是不是大的泡泡上升的快最先从水里出来呢?实现冒泡排序就可以按照这个思想去考虑。
我们假设数大的是大泡泡,那么数字越大越早上浮(那么思考一下怎么实现上浮呢?)到最高处,我们假设数组的下标越大位置越高,那么我们的冒泡排序就是一个个数字往数组下标高出漂浮,大的数字再上面且越大的数据越早开始漂浮。


冒泡排序算法的原理如下:

1.首先比较相邻的元素。如果第一个比第二个大,就交换他们两个。(上浮的过程)
2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。遍历结束后,最后的元素应该会是最大的数。
3.对未排序元素重复以上的步骤,直到所有元素排序完成


ps : 冒泡排序的科学定义是这样的:
重复地访问过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。访问元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

Now,我们来举个栗子:

在这里插入图片描述

2.代码实现

//形参是待排序的整形数组,传入是的地址值,希望你还记得
private static void bubbleShort(int[] arr) {
	int temp = 0;// 临时数据,交换数据时用到
	for (int i = 1; i < arr.length; i++) {
		for (int j = 0; j < arr.length - i; j++) {
			// 每次循环寻找最大值,将最大值至于较后位置,一次完整遍历后最大值位于数组下标最大位置
			if (arr[j] > arr[j + 1]) {
				temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	// 输出排序完成后的数组数据
	for (int i = 0; i < arr.length; i++) {
		System.out.print(arr[i] + " ");
	}
}

3.时间复杂度:

根据实现代码,设数组长度为n,我们大概来估算一下:有内外两层与数组长度相关的循环,所以时间复杂度应该是平方阶。。

实际计算,内层循环数据交换次数如下:
(N-1) + (N-2) + … + 2 + 1 ,
与选择排序相同,时间复杂度是O(n2)

4.空间复杂度

由于冒泡排序算法的程序存储空间在程序运行时不随处理数组数据的过程而变化,无临时空间变化。所以其空间复杂度是O(1)

二. 算法稳定性

算法稳定性之前之了解过定义,我再实际应用中并没有注意过这个问题,但是这个问题是一直存在的。我们今天就来探索一下算法的稳定性。

1.描述

简单来讲,算法稳定性就是待排序数组,其左右相等的元素在排序过程中是否存在位置交换操作,不交换便稳定。

2.判断方法

对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法(某些特殊情况下),而稳定的算法在某种条件下也可以变为不稳定的算法(受影响于我们自己的代码实现)。

3.常见的算法稳定性

首先,记住以下结论:
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法;
基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

我们了解一下算法存在稳定性的好处。排序算法如果是稳定的,那么从一个键(排序的某个基准值)上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是在相邻的两个元素之间比较,交换也发生在这两个元素之间。所以,如果两个元素相等,其前后顺序并不需要改变,所以冒泡排序是一种可实现稳定排序的算法。
(2)选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。我们举个算法程序运行过程中会破坏稳定性的例子:序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
(3)插入排序
插入排序是在一个有序小序列的基础上,一次插入一个待排序元素,插入位置的选择比较是从有序序列的末尾开始,也就是想要插入的元素需要和有序序列的最大值开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把待插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

以下算法还未讲到,一并记录于此:
(证明过程描述目前还未阅读,可靠性不确定)

(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
(8)堆排序
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值