什么是冒泡排序?
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个是百度百科上比较官方的说法。用我们的白话来说就是,从头开始,依次比较两个相邻的元素,如果前一个元素大于后一个元素则交换位置(从小到大排序),直到这组数的最后一个元素。下面我们画图具体描述一下。
假设有如下一组数字,我们要从小到大排序
第一轮比较:
首先1和5比较,1比5小,位置不变
接着第一轮,第二次比较,5和6比较,位置依旧不变
第一轮,第三次比较,6和2比较,6比2大,所以6和2交换位置
第一轮,第四次比较,6和3比较,交换位置
第一轮,第五次比较,6和4比较,交换位置
这样,第一轮经过5次比较后,这一组数中最大的数6,就到了末尾位置。
现在这一组数的排序位置如下图,图中蓝色的,就是已经排好序的元素6。
接着我们在继续第二轮,就是重复之前的动作,只是不用再一直比到6了,只需比到4就行了,因为6已经排好位置了。
这里我用文字描述一下,就不再画图了,偷个懒。
第二轮比较:第一次比较,1和5,不交换位置:1,5,2,3,4,6
第二次比较,5和2,交换位置后:1,2,5,3,4,6
第三次比较,5和3,交换位置后:1,2,3,5,4,6
第四次比较,5和4,交换位置后:1,2,3,4,5,6
经过第二轮后,现在的顺序应该如下图
然后再第三轮、第四轮,一直到第五轮没有元素可以比较了。说明已经排好序了。
我们发现:每一个轮就会产生一个有序元素(蓝色部分)。又因为每一轮都会产生一个有序元素,那么待排序元素就少了一个(白色部分)。所以每一轮比较的次数就比前一轮少一次。那么我们可以总结出如下公式:对N个元素进行冒泡排序,需要比较N-1轮,每第K轮,需要比较N-K次。
我们用代码实现如下:
int[] array = {1,6,5,2,3,4};
for (int i = 0;i < array.length-1; i ++){
for (int k = 0;k < array.length-i-1;k++){
int temp = array[k];
if (array[k] > array[k+1]){
array[k] = array[k+1];
array[k+1] = temp;
}
}
}
上面这个代码不难看出,其时间复杂度为O(n²),空间复杂度为O(1)。
优化初体验
这个效率也太低了,那么我们可以优化它吗?当然可以。其实不难发现,我们在进行第2轮后,这一组数其实已经有序了。然而之前的排序还要继续执行后面3轮,这不是傻吊吗?白白浪费时间。
怎么判断已经有序了呢?我们之前的排序是不是依次比较两个数,如果有错序就交换位置,那么如果这一轮没有发生交换位置,不就代表在这一轮,整组数已经有序了嘛?因此我们的代码可以做如下优化
int[] array = {1,6,5,2,3,4};
for (int i = 0;i < array.length-1; i ++){
boolean flag = true;//默认这一轮没有发生交换
for (int k = 0;k < array.length-i-1;k++){
int temp = array[k];
if (array[k] > array[k+1]){
array[k] = array[k+1];
array[k+1] = temp;
flag = false; //发生了交换
}
}
if (flag){
break;
}
}
这个时间复杂度虽然还是O(n²),但是在一组数中,如果大部分元素本身是有序的,就可以少执行很多不必要的循环。
What?还能再优化?
回顾之前的排序,每经过一轮比较,就至少能产生一个有序元素。我们姑且把这块区域称为有序区吧。如下面这个第一轮排序后,蓝色的有序区就只有一个6
我为什么要用至少呢?因为假设是这样的一组数:1,5,3,2,4,6。第一轮排序:1和5,不换;5和2,交换。5和3,交换。5和4,交换,5和6,不换。结果如下图
我们可以看到,按照之前的方法,就是只产生了一个有序元数6。但我们不难发现,其实在5和6比的时候,并没有交换位置。所以实际的情况,有序区应该是这样的
所以,我们可以发现,其实在每轮最后一次发生交换的位置K,这个位置K后面的元素应该都是有序的。这样,我们下一轮比较,就不需要再比较这个位置后面的元素了。
拿上面的例子来说,按我们之前的规则,第一轮产生了有序数6后,第二轮应该从1一直比较到5,但是有了这个规律,我们只需要比较到第一轮最后一次发生交换的位置,即数4。下面我们来看一下代码的实现
int[] array = {1,5,3,2,4,6};
int changeIndex = 0; //最后一次交换的位置
int sortIndex = 5; //有序区的边界位置
for (int i = 0;i < array.length-1;i++){
boolean flag = true; //默认这一轮没有发生交换
for (int k = 0;k < sortIndex;k++){
int temp = array[k];
if (array[k] > array[k+1]){
array[k] = array[k+1];
array[k+1] = temp;
changeIndex = k; //记录最后一次发生交换的位置
flag = false; //发生了交换
}
}
sortIndex = changeIndex;
if (flag){
break;
}
}
终极优化
你想想,我们现在的排序,是不是每一轮都要从头开始,即正向排序
那我们为什么不在每一次轮比较到最后一个元素后,在反过来比较一遍呢?
这就好比,我在A点、B点各放了一堆面值100的人民币,然后你需要拿够1000,每次只能拿100。而且一个来回每个点只能拿一次。然后你就头铁,老子就只从A点拿,再从B点回到A点再拿。你需要10个来回(实际只需9个来回,最后一次出发点A拿了就不跑了。但为了规则,拿钱就得干事还是得跑一下)。但是如果你聪明点,A点出发的时候拿一次,到了B点再拿一次,就只需要5个来回。这不就节省了一半时间。
可能这个比喻不太恰当,但为什么能提高效率,我们心里应该已经有数了。其实这个排序还有一个优雅的名字,叫做“鸡尾酒排序”。下面我们来看一下代码的实现
int[] array = {1,3,2,4,6,5};
for (int i = 0;i < array.length/2; i ++){
boolean flag = true;//默认这一轮没有发生交换
//从左到右,正向排序
for (int k = 0;k < array.length-i-1;k++){
int temp = array[k];
if (array[k] > array[k+1]){
array[k] = array[k+1];
array[k+1] = temp;
flag = false; //发生了交换
}
}
if (flag){
break;
}
flag = true; //在进行反向排序前,默认没有发生交换
for (int k = array.length-i-1;k > i;k--){
int temp = array[k];
if (array[k-1] > array[k]){
array[k] = array[k-1];
array[k-1] = temp;
flag = false; //发生了交换
}
}
if (flag){
break;
}
}