冒泡排序的思路很简单——从头至尾遍历数组元素,若前一项大于(或小于)后一项,则交换相邻两项。单次遍历整个数组可将某一个元素排列到正确位置,因此需要遍历元素数量n次。在代码中体现也就是内外两层循环,内层循环负责遍历中两两元素的交换操作,外层负责遍历次数控制。
首先看版本1:
/**
* 冒泡排序效率最低写法,没有任何优化步骤
* @param arrayToSort
*/
public static void sortAlpha(int[] arrayToSort) {
for (int i = 0; i < arrayToSort.length; i++) {
for (int j = 0; j < (arrayToSort.length - 1); j++) {
int tmp;
if (arrayToSort[j] > arrayToSort[j + 1]) {
tmp = arrayToSort[j];
arrayToSort[j] = arrayToSort[j+1];
arrayToSort[j+1] = tmp;
}
}
}
}
很明显的两层循环,这里按从小到大排序,因此弱发现后一元素比当前元素大,则交换两个元素的位置,直到比较到最后一个元素为止。因为是j
与j+1
比较,为避免数组越界,内层for循环需以arrayToSort.length - 1
做边界。
这个版本中没有做任何优化,因为遍历n次后,数组中后n个元素实际已经是排好序的了,因此不需要再进行比较了。所以内层循环还可以进一步优化,得到版本2:
/**
* 冒泡排序优化版本1,减少内层循环遍历个数,已经排序好的元素无需遍历。
* @param arrayToSort
*/
public static void sortBeta(int[] arrayToSort) {
for (int i = 0; i < arrayToSort.length; i++) {
for (int j = 0; j < (arrayToSort.length - 1 - i); j ++) {
int tmp;
if (arrayToSort[j] > arrayToSort[j + 1]) {
tmp = arrayToSort[j];
arrayToSort[j] = arrayToSort[j+1];
arrayToSort[j+1] = tmp;
}
}
}
}
由于遍历n次后,后n项已经排好序了,因此内层循环上界再减去遍历次数即可,即arrayToSort.length - 1 - i
这样每次内层遍历便可减少n次比较操作,提升了效率。
虽然这时内层循环已经优化操作次数,但如果给一个部分有序的数列,如
{1, 2, 3, 4, 8, 7, 6, 5}
遍历前四次后,数组实际已经排好序了,这时就不需要再进行比较操作了,因此除了前四次排序,仅在需要一次遍历就可检验出数组有序,也就是公共遍历5次数组就可完成排序了。在当前的逻辑下,会导致出现3次无意义的遍历。
解决这个问题的方案也很简单,若数组已经有序,则不会出现交换操作。因此仅需设定一个标志变量,当有交换时置成需要排序(检验)的状态,当所有元素已经有序,检验过程没有交换操作,那么再下次遍历数组直接退出即可,便是版本3:
/**
* 冒泡排序优化版2,减少内部排序遍历个数,并且添加了数据是否有序的检验,
* 若数组已经有序,无需再进行遍历,直接退出。
* @param arrayToSort
*/
public static void sortGamma(int[] arrayToSort) {
boolean needSort = true;
for (int i = 0; i < arrayToSort.length; i++) {
if (!needSort) {
break;
}
for (int j = 0; j < (arrayToSort.length - 1 - i); j ++) {
needSort = false;
int tmp;
if (arrayToSort[j] > arrayToSort[j + 1]) {
tmp = arrayToSort[j];
arrayToSort[j] = arrayToSort[j+1];
arrayToSort[j+1] = tmp;
needSort = true;
}
}
}
}
在每次进入内部循环时重置标志变量,若进行了交换操作则相应改变,进入内层循环前进行判断,若不需要再遍历,直接退出。
接下来写一些测试代码,来验证三种方案的效率。生成两个数组:一个完全逆序,长度为9999;另一个完全正序,长度同样为9999(不用再排序了),然后分别使用三种排序,统计排序时间,以下是一种方法的调用:
public static void main(String[] args) {
final int len = 9999;
int test[] = new int[len];
for (int i = 0; i < test.length; i++) {
test[i] = len - 1 - i;
}
int testNoNeedSort[] = new int[len];
for (int i = 0; i < testNoNeedSort.length; i ++) {
testNoNeedSort[i] = i;
}
long startTimeA = System.currentTimeMillis();
BubbleSort.sortGamma(test);
long endTimeA = System.currentTimeMillis();
System.out.println("Time use A:" + (endTimeA - startTimeA));
long startTimeB = System.currentTimeMillis();
BubbleSort.sortGamma(testNoNeedSort);
long endTimeB = System.currentTimeMillis();
System.out.println("Time use A:" + (endTimeB - startTimeB));
}
运行三种排序,统计到的时间:
版本 | 逆序(最差情况)ms | 正序(最好情况)ms |
---|---|---|
版本1 | 107 | 66 |
版本2 | 62 | 42 |
版本3 | 60 | 1 |
最好情况下由于仅需比较,不需要交换,因此三种版本使用时间均比最差情况短,但由于第三种增加了交换情况的判断,仅需遍历一遍就可完成。
最差情况下由于版本2,版本3对内层排序进行了操作数优化,因此显著优于完全没有优化的版本1。