冒泡排序是交換排序的一種,其思想是從序列頭部開始逐步往後遍歷,每次遍歷比較相鄰兩個元素,如果順序不對則交換,n-1次遍歷之後序列就完成了排序。由於每次遍歷都是把最大的元素一步步讓最後移動,類似於水泡慢慢浮出水面,於是得名冒泡算法。冒泡算法的思想很簡單,實現起來也很容易,但是效率太低,所以即使是小數據量也很少推薦使用冒泡算法,更多的使用直接插入排序。
基本思想:
從待排序序列頭部開始循環遍歷n-1次,每次遍歷比較相鄰兩個元素,如果順序不對則交換,也就是說如果你想要從小到大排列,那麼如果相鄰兩個元素比較結果是前面的大於後面的,則交換就可以了,那麼通過一次遍歷最大的元素就到達了序列的尾部,n-1次遍歷之後序列就完成了排序。這裏顯然有可優化的空間,下面會提到。
實現要點:
第i次(i=0…n-1)遍歷從0開始比較到n-1-i(n序列長度),因爲每次遍歷都會將未排序部分最大的元素移動到序列尾部,所以序列尾部是有序的,故在之後的遍歷過程中無需繼續比較有序的部分。
算法改進:
該算法有兩處可優化的地方:
- 如果一次遍歷過程中未發生任何交換,即所有相鄰元素的順序都是正確的,則說明整個序列已經完成排序,故無需繼續遍歷。
- 每次遍歷過程中記錄下最後一次發生遍歷的位置,則在改位置之後的部分已經是有序的,下次遍歷時就可以提前結束。
實驗表明以上兩種改進之後的效率並未有太大的提高,第一種改進效率反而比爲改進的低,第二種改進效率稍微提高一點點。雖然這兩種改進從理論上來看是有一定的優化的,但是測試時使用的序列一般都是隨機的,即在n-1次遍歷之前完成排序以及部分有序的可能性都很小,所以這兩種改進的效果都不是很明顯,可能根本不會發生,反而由於加入了一些邏輯判斷反而導致效率降低。不過如果是真是數據,那麼之前提到的兩種情況還是很可能發生的。
Java實現:
package com.vicky.sort;
import java.util.Random;
/**
* 交換排序:冒泡排序
*
* 時間複雜度:O(n^2)
*
* 空間複雜度:O(1)
*
* 穩定性:穩定
*
* @author Vicky
*
*/
public class BubbleSort {
/**
* 未改進的冒泡排序
*
* @param <T>
* @param data
*/
public static <T extends Comparable<T>> void sort(T[] data) {
long start = System.nanoTime();
if (null == data) {
throw new NullPointerException("data");
}
if (data.length == 1) {
return;
}
// n-1趟遍歷
for (int i = 0; i < data.length - 1; i++) {
// 每次遍歷從0開始依次比較相鄰元素
for (int j = 0; j < data.length - 1 - i; j++) {
// 前面元素>後面元素則交換
if (data[j].compareTo(data[j + 1]) > 0) {
T temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
System.out.println("sort, use time:" + (System.nanoTime() - start)
/ 1000000);
}
/**
* 改進後冒泡排序
*
* 改進原理:如果一次遍歷過程未發生交換,則說明序列已經是有序的,故無需再進行遍歷。
*
* @param <T>
* @param data
*/
public static <T extends Comparable<T>> void sortImprove(T[] data) {
long start = System.nanoTime();
if (null == data) {
throw new NullPointerException("data");
}
if (data.length == 1) {
return;
}
boolean exchange = false;// 記錄一趟遍歷是否發生交換
// n-1趟遍歷
for (int i = 0; i < data.length - 1; i++) {
// 每次遍歷從0開始依次比較相鄰元素
for (int j = 0; j < data.length - 1 - i; j++) {
// 前面元素>後面元素則交換
if (data[j].compareTo(data[j + 1]) > 0) {
T temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
exchange = true;
}
}
// 如果本次遍歷未發生交換,則說明序列已是有序的,則無需繼續遍歷
if (!exchange) {
return;
}
}
System.out.println("sortImprove1, use time:"
+ (System.nanoTime() - start) / 1000000);
}
/**
* 改進後冒泡排序
*
* 改進原理:在冒泡排序的每趟掃描中,記住最後一次交換髮生的位置lastexchange也能有所幫助。因爲該位置之後的部分已經是有序的(未發生交換,
* 所以是有序),
* 故下一趟排序開始的時候,只需處理0到lastexchange部分,lastexchange到n-1是有序區。同時如果未發生交換則退出即可
*
* @param <T>
* @param data
*/
public static <T extends Comparable<T>> void sortImprove2(T[] data) {
long start = System.nanoTime();
if (null == data) {
throw new NullPointerException("data");
}
if (data.length == 1) {
return;
}
int lastChange = data.length - 1;// 記錄一趟遍歷最後一次發生交換的位置,該位置之後是有序的
// 上次遍歷發生交換則lastChange>0,繼續遍歷
while (lastChange > 0) {
// 本次遍歷從0開始到上次遍歷最後一次交換的位置結束
int end = lastChange;
lastChange = 0;
// 每次遍歷從0開始依次比較相鄰元素
for (int j = 0; j < end; j++) {
// 前面元素>後面元素則交換
if (data[j].compareTo(data[j + 1]) > 0) {
T temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
lastChange = j + 1;
}
}
}
System.out.println("sortImprove2, use time:"
+ (System.nanoTime() - start) / 1000000);
}
public static void main(String[] args) {
// 用於記錄三種冒泡排序的用時
long[] useTime1 = new long[10];
long[] useTime2 = new long[10];
long[] useTime3 = new long[10];
// 循環測試10次,取均值
for (int times = 0; times < 10; times++) {
// 構建10000個元素的序列進行排序
Random ran = new Random();
Integer[] data = new Integer[10000];
for (int i = 0; i < data.length; i++) {
data[i] = ran.nextInt(10000000);
}
// 使用System.arraycopy複製三個數組分別用於排序
Integer[] data1 = new Integer[data.length];
Integer[] data2 = new Integer[data.length];
Integer[] data3 = new Integer[data.length];
System.arraycopy(data, 0, data1, 0, data.length);
System.arraycopy(data, 0, data2, 0, data.length);
System.arraycopy(data, 0, data3, 0, data.length);
// 分別記錄三種冒泡排序的用時
long start = System.nanoTime();
BubbleSort.sort(data1);
useTime1[times] = (System.nanoTime() - start) / 1000000;
// SortUtils.printArray(data1);
start = System.nanoTime();
BubbleSort.sortImprove(data2);
useTime2[times] = (System.nanoTime() - start) / 1000000;
start = System.nanoTime();
// SortUtils.printArray(data2);
BubbleSort.sortImprove2(data3);
useTime3[times] = (System.nanoTime() - start) / 1000000;
// SortUtils.printArray(data3);
}
// 計算用時最大值,最小值,均值
long[] res1 = SortUtils.countArray(useTime1);
long[] res2 = SortUtils.countArray(useTime2);
long[] res3 = SortUtils.countArray(useTime3);
System.out.println("method\tmax\tmin\tavg\t");
System.out.println("sort" + "\t" + res1[0] + "\t" + res1[1] + "\t"
+ res1[2]);
System.out.println("sortImprove" + "\t" + res2[0] + "\t" + res2[1]
+ "\t" + res2[2]);
System.out.println("sortImprove2" + "\t" + res3[0] + "\t" + res3[1]
+ "\t" + res3[2]);
// 測試結果,第一種改進方法效率比不改進還差一些,
// 可能由於出現提前完成排序的可能性較小,每次遍歷加入了過多的賦值以及判斷操作導致效率反而降低
// 第二種改進方法還是有一些效果的
// method max min avg
// sort 1190 1073 1123
// sortImprove 1258 1097 1146
// sortImprove2 1205 1056 1099
}
}
效率分析:
(1)時間複雜度
O(n^2)
冒泡排序最好的時間複雜度是O(n),一次遍歷即可,無需交換(第一種改進)。最壞情況需要遍歷n-1次,比較且交換n-1-i次,故時間複雜度是O(n^2)。
(2)空間複雜度
O(1)
從空間來看,它只需要一個元素的輔助空間,用於元素的位置交換O(1)。
(3)穩定性
穩定
排序過程中只有相鄰兩個元素會發生交換,同時爲了減少交換次數相同的元素不會進行交換,所以兩個相同元素的相對位置不會發生改變。