排序算法——冒泡排序系列及性能测试

初识冒泡排序

排序算法常常作为学习算法的入门实践,而冒泡排序又是其中最简单的一种,我们今天的主题就是冒泡排序。它的基本思想就像鱼吐泡泡一样简单。


想象有一条鱼在数组的最底端,每一轮,它就吐个泡泡,泡泡会从数组一端漂到另一端,在漂浮过程中,泡泡会捕获数组中待排序的部分中最大的元素,将其移动到最顶端,然后把这个元素从数组待排序部分中剔除。

下一轮又从剩余的待排序元素中选一个最大的,用泡泡再次将其上浮到最顶端。如此一来,经过n轮吐泡泡。从数组最顶端往下,依次就是最大的元素,第二大的元素,第三大的元素…整个数组就是有序的了,当然,若想要逆序的话,每一轮就挑最小的元素冒泡上去就好了。

一轮冒泡的过程,是通过对相邻两个元素不断地比较和交换,来找到该轮冒泡中最大的元素,并将其移动到顶端的。


具体过程是,想象一个指针,指针指向的元素称为当前元素。指针先指向第一个元素,比较当前元素和下一个元素之间的大小,若当前元素比下一个元素大,则交换当前元素和下一个元素。并把指针后移一个位置,继续和下一个元素比较,一直比到最后一个元素。这样,就完成了第一轮冒泡。第一轮冒泡完毕后,最右侧的元素就是最大的。第二轮冒泡,又从第一个元素开始,依次进行比较和交换,一直比到倒数第二个元素。第二轮结束,右侧倒数第二个元素就是第二大的…N轮冒泡后,整个数组就是从小到大有序的了

图解如下

假设准备对如下数组进行冒泡排序

第一轮冒泡

指针先指向第一个元素,比较当前元素与下一个元素,发现9比5大,那么交换二者

指针后移一位

继续比较当前元素和下一个元素的大小,发现9比7大,交换之

指针继续后移一位,继续比较与交换,最终9被冒泡到最右端,第一轮冒泡结束

第二轮冒泡开始,将指针放在第一个元素,开始重复第一轮的过程(注意第二轮只需要冒泡到倒数第二个位置即可,因为每一轮结束后,最右侧的有序序列的长度都会变长1位)

发现5比7小,则不交换。指针后移

发现7比3大,交换之

指针后移

发现7比1大,交换之…最后,8被冒泡到最右端

继续第三轮,7被冒泡到最右边

第四轮,6到了最右边

最后,整个数组有序

根据这个思路,写出代码如下

	public void bubbleSort(int[] a) {
		for(int i = a.length - 1; i >= 0; i--) {
			for(int j = 0; j < i; j++) {
				if(a[j] > a[j + 1]) swap(a, j, j + 1);
			}
		}
	}
	
	public static void swap(int[] a, int i, int j) {
		int t = a[i];
		a[i] = a[j];
		a[j] = t;
	}

外层循环从数组最后一个位置开始,表示的是每一轮的终结位置(第一轮冒泡到最后一个位置停止,第二轮冒泡到倒数第二个位置停止,以此类推)。内层循环是一轮冒泡的过程,每轮冒泡,从第一个位置开始,依次与后一个位置进行比较和交换,一直进行到该轮冒泡的停止位置。当然,上述代码,只写出了核心部分的实现,并不完善。比如bubbleSort方法内,应该先判断array变量是否为nullswap方法内应该判断array是否为null,并检测ij是否越界。

优化思路

思路一

上面的冒泡排序的实现,实际还有可优化的空间。比如,若某一轮冒泡的过程中,没有发生任何元素交换。这就说明数组已经整体有序,则无需再进行后续的冒泡轮数。修改代码,只需要在每一轮冒泡开始前,新增一个布尔类型的标志变量boolean sorted = true,只要该轮冒泡发生过元素交换,就将sorted变量置为false。如此以来,在每轮冒泡结束后,新增一个判断逻辑,判断若sortedtrue,则提前结束排序,写成代码如下。

	public static void bubbleSortV1(int[] a) {
		for(int i = a.length - 1; i >= 0; i--) {
			boolean sorted = true;
			for(int j = 0; j < i; j++) {
				if(a[j] > a[j + 1]) {
					swap(a, j, j + 1);
					sorted = false;
				}
			}
			if (sorted) break;
		}
	}
思路二

如果说上面是粒度较大的优化(针对外层循坏的全局优化,满足条件时整个流程提前结束),则在更小粒度上还存在着优化空间(每轮冒泡过程中的优化)。原先的实现,每轮冒泡之后,最右侧有序序列的长度是依次加一的。即,第一轮冒泡,需要交换比较,直到最后一个位置;第二轮冒泡,需要交换比较,直到倒数第二个位置…然而,每轮冒泡只需要将泡泡上浮到有序序列的边界即可终止,我们可以根据每一轮的实际情况,来决定下一轮冒泡的停止位置。而不是每一轮都必须达到固定的停止位置。

比如有如下数组

第一轮冒泡后,数组变为

最后一次发生交换的是3和4的位置,之后都只是进行了比较而并未发生交换,说明4以后的都是有序序列了。那么,第二轮冒泡的终止位置可以不必到倒数第二个位置了,而可以只到上一轮冒泡发生的最后一次交换的位置(发生最后一次交换的位置的右侧,即是有序序列)。即,对于每一轮冒泡,新增了用于记录发生最后一次交换的位置的变量,用于指示下一轮冒泡的终止位置。写成代码如下

	public static void bubbleSortV2(int[] a) {
		for(int i = a.length - 1; i >= 0;) {
			boolean sorted = true;
			int lastSwap = i - 1;
			for(int j = 0; j < i; j++) {
				if(a[j] > a[j + 1]) {
					swap(a, j, j + 1);
					sorted = false;
					lastSwap = j;
				}
			}
			i = lastSwap;
			if (sorted) break;
		}
	}

注意,无论如何优化,冒泡排序的元素交换次数是不变的,优化措施减少的只是不必要的比较次数,给出如下示例,可自行验证

性能测试

理论上讲,由于后两种引入了优化措施,应该其效率要高于未优化的版本,即效率大小应为

bubbleSortV2 > bubbleSortV1 > bubbleSort

然而实际测试的时候发现,有时并不是如此,因为优化措施在每轮冒泡过程中加入了额外的判断,需要消耗一定性能的,并且如果待排序的数组,使用优化措施减少的无用比较的性能开销,小于引入额外判断造成的性能开销,即收益小于成本时,V1和V2的性能就不一定要高于未优化版本(并且还需要考虑到运行时机器的实际情况)。然而当数组规模较大时,优化措施能降低的比较次数较多,收益较高,则性能会优于未优化版本。

下面针对3个冒泡排序的版本,进行性能测试。测试使用随机生成的数组,数组规模从100,400,700…一直到14800(数组规模从100开始,规模依次递增300,共测试50组数组)。为了避免运行时机器的影响因素,对每组数组,重复测试20次,取性能平均值。绘制得到的性能折线图如下

可知,随着数组规模的增大,优化后的版本性能要明显好于未优化的版本,且V2版本性能略优于V1

加餐(鸡尾酒排序)

另,还有一种冒泡排序的变种,名为鸡尾酒排序,它的思想是双向冒泡,奇数轮从左到右,将最大的元素冒泡到最右侧,偶数轮从右到左,将最小的元素冒泡到最左侧。鸡尾酒排序在数组中间部分有序,而两端乱序时,能够取得较好的性能提升。下面是它的代码实现,和冒泡排序非常类似

	public static void cockTailSort(int[] a) {
		boolean moveL = true;
		int l = 0, r = a.length - 1;
		while (l < r) {
			if (moveL) {
				for (int i = l; i < r; i++) {
					if (a[i] > a[i + 1]) swap(a, i, i + 1);
				}
				r--;
			} else {
				for (int i = r; i > l ; i--) {
					if (a[i] < a[i - 1]) swap(a, i, i - 1);
				}
				l++;
			}
			moveL = !moveL;
		}
	}

将鸡尾酒排序纳入性能测试,看看它和普通冒泡排序的对比

可见鸡尾酒排序的性能要略优于普通冒泡排序

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
冒泡排序是一种简单的排序算法,它重复地遍历待排序的元素,比较相邻元素的大小,如果顺序错误就交换它们,直到没有需要交换的元素为止。下面是冒泡排序的C语言实现: ```c void bubbleSort(int arr[], int n) { for(int i = 0; i < n-1; i++) { int flag = 0; //是否冒泡 for(int j = 0; j < n-1; j++) { if(arr[j > arr[j+1]) { swap(&arr[j], &arr[j+1]); flag = 1; } } if(flag == 0) { //如果没有发生交换,说明数组已经有序,可以提前结束循环 break; } } } ``` 在这个实现中,冒泡排序函数`bubbleSort`接受一个整型数组`arr`和数组长度`n`作为参数。它使用两层循环来遍历数组,并通过比较相邻元素的大小来进行交换。内层循环每次将最大的元素冒泡到数组的末尾。外层循环控制了冒泡的轮数,每一轮都会将当前未排序的最大元素放在正确的位置上。如果在某一轮的冒泡过程中没有发生交换,说明数组已经有序,可以提前结束循环,从而提高算法的效率。 需要注意的是,上述代码中使用了一个`swap`函数来交换两个元素的值,你可以根据需要自行实现该函数。此外,为了减少冒泡排序的时间复杂度,可以在内层循环中添加一个标志位`flag`,用于标记是否发生了交换。如果某一轮的冒泡过程中没有发生交换,说明数组已经有序,可以提前结束循环。这样可以避免不必要的比较和交换操作,提高排序的效率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值