概述
排序算法应该算是一个比较热门的话题,在各个技术博客平台上也都有一些博文进行了一定程度的讲解。但还是希望能从自我完善的角度出发,可以更详细、全面、形象地表达这些算法的精髓。本文就先从最简单的冒泡排序开始说起,别说你已经彻底了解了冒泡排序算法(虽然一开始我也是这样以为的)。
版权说明
本文链接:http://blog.csdn.net/lemon_tree12138/article/details/50591859 – Coding-Naga
— 转载请注明出处
目录
冒泡排序
冒泡排序(Bubble Sort)是一种交换排序1,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
— 《大话数据结构》
教科书版
算法原理
一般来说,我们学习排序算法的入门都是从教科书中获得。而教科书上讲解的内容都是一些相对比较简单的,其目的是为了让人能够很好地去理解它,也可以让人能够很容易地编写出可行的代码。
教科书版冒泡排序的原理是对数组进行两层循环遍历。让数组中较小的关键字能够较快地移动到数组的顶部,从而当两层循环结束,排序即可完成。
算法实现
public void sort(int[] array) {
int arrayLength = array.length;
for (int i = 0; i < arrayLength; i++) {
for (int j = i + 1; j < arrayLength; j++) {
if (array[i] > array[j]) {
ArrayUtils.swap(array, i, j);
}
}
}
}
算法过程图
通过上面的代码实现,我们可以很容易地画出如下过程图。
图-1 冒泡排序-初级版
复杂度分析
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
平均情况 | 最坏情况 | 最好情况 | ||||
冒泡排序(教科书版) | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
标准版
算法原理
从上面的教科书版的冒泡排序过程图中,我们可以看到教科书版本的冒泡排序并没有很好地体现冒泡的思想。我可没见过有哪个泡泡可以一下子从水的最下面冒到水面的,前面说到的教科书版本的冒泡排序可以理解成最简单的交换排序算法。下面,让我们来见识一下正宗的冒泡排序算法是怎么做的吧。
算法实现
private void core(int[] array) {
int arrayLength = array.length;
for (int i = 0; i < arrayLength; i++) {
for (int j = arrayLength - 2; j >= i; j--) {
if (array[j] > array[j + 1]) {
ArrayUtils.swap(array, j, j + 1);
}
}
}
}
算法过程图
如果你说这两个算法代码差不多,那就先把代码比对清楚,你会发现不同的地方还是很多的。通过上面的代码实现,我们可以很容易地画出如下过程图。(这次就感觉像是一个小气泡,在一点一点向上冒了。^_^)
图-2 冒泡排序-标准版
复杂度分析
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
平均情况 | 最坏情况 | 最好情况 | ||||
冒泡排序(标准版) | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
改进版
优化方案
通过对上面两种冒泡排序算法的学习,我们可以看到不管是哪一种算法,都不能很好地避免对一个已经有序的数组减少比较操作(只是不用做交换处理)。
现在,我们想到一种方法,使用一个标志位,来标识当前数组是否已经有序。如果无序,则继续冒泡排序;如果已经有序,则退出排序算法。这样就可以很好地规避掉一些不必要的比较操作。
算法实现
private void core(int[] array) {
boolean status = true; // 记录是否发生交换信息
int arrayLength = array.length;
for (int i = 0; i < arrayLength && status; i++) {
status = false;
for (int j = arrayLength - 2; j >= i; j--) {
if (array[j] > array[j + 1]) {
ArrayUtils.swap(array, j, j + 1);
status = true;
}
}
}
}
复杂度分析
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
平均情况 | 最坏情况 | 最好情况 | ||||
冒泡排序(改进版) | O(n2) | O(n2) | O(1) | O(1) | 稳定 | 简单 |
算法评价
从冒泡的原理上,我们可以知道,从前向后进行循环遍历交换和从后向前进行循环遍历交换的代价和逻辑是一致的,那么我们也就可以从后向前进行冒泡排序,代码就里就不再给出了。
在单向冒泡排序算法中,存在着一个著名的“乌龟问题”2。
而在排序过程中,又主要是这个过程耗费了大量时间。关于具体的实例在下面的“双向冒泡排序”算法中体现。
双向冒泡排序
算法原理
在基于冒泡排序的基础上,我们知道,无论是从前向后遍历交换,还是从后向前遍历交换,对程序的逻辑和性能的代价都是不影响的,那么我们就可以让一部分情况下从前向后遍历交换,另一部分情况从后向前遍历交换。
图-3 双向冒泡排序
算法步骤
- 比较相邻两个元素的大小。如果前一个元素比后一个元素大,则两元素位置交换
- 对数组中所有元素的组合进行第1步的比较
- 奇数趟时从左向右进行比较和交换
- 偶数趟时从右向左进行比较和交换
- 当从左端开始遍历的指针与从右端开始遍历的指针相遇时,排序结束
算法实现
代码如下:
private void core(int[] array) {
int arrayLength = array.length;
int preIndex = 0;
int backIndex = arrayLength - 1;
while(preIndex < backIndex) {
preSort(array, arrayLength, preIndex);
preIndex++;
if (preIndex >= backIndex) {
break;
}
backSort(array, backIndex);
backIndex--;
}
}
// 从前向后排序
private void preSort(int[] array, int length, int preIndex) {
for (int i = preIndex + 1; i < length; i++) {
if (array[preIndex] > array[i]) {
ArrayUtils.swap(array, preIndex, i);
}
}
}
// 从后向前排序
private void backSort(int[] array, int backIndex) {
for (int i = backIndex - 1; i >= 0; i--) {
if (array[i] > array[backIndex]) {
ArrayUtils.swap(array, i, backIndex);
}
}
}
复杂度分析
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
平均情况 | 最坏情况 | 最好情况 | ||||
双向冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 | 简单 |
算法评价
如果单纯从时间复杂度上来讨论,双向冒泡排序与冒泡排序算法复杂度是一致的。不过在双向冒泡排序算法中,我们引入了一些变量,以控制程序流程,在空间复杂度上虽然都O(1),不过双向冒泡排序还是会大一些(至少有多了两个位置指针)。从代码的复杂度上,双向冒泡排序算法会大一些。
不过在上面的冒泡排序算法中,我们了解到冒泡排序算法有一个“乌龟问题”。正是因为这个原因,我们引入了双向冒泡排序算法。这里我们可通过一个实例更加象形地了解它。
假设我们现在有一个待排序序列{6, 5, 4, 3, 2, 1}。分别使用单向和双向冒泡排序对其进行排序,两种排序算法的过程如下(左图为单向冒泡,右图为双向冒泡):
步骤 | 单向冒泡排序 | 双向冒泡排序 |
---|---|---|
原始状态 | [6, 5, 4, 3, 2, 1] | [6, 5, 4, 3, 2, 1] |
第 1 趟 | [1, 6, 5, 4, 3, 2] | [1, 6, 5, 4, 3, 2] |
第 2 趟 | [1, 2, 6, 5, 4, 3] | [1, 5, 4, 3, 2, 6] |
第 3 趟 | [1, 2, 3, 6, 5, 4] | [1, 2, 5, 4, 3, 6] |
第 4 趟 | [1, 2, 3, 4, 6, 5] | [1, 2, 4, 3, 5, 6] |
第 5 趟 | [1, 2, 3, 4, 5, 6] | [1, 2, 3, 4, 5, 6] |
Ref
- 《大话数据结构》
- 《算法导论》
- [ 白话经典算法系列之一 冒泡排序的三种实现 ]