1. 程式人生 > >排序(上)——為什麼插入排序比氣泡排序更受歡迎?

排序(上)——為什麼插入排序比氣泡排序更受歡迎?

本文是學習演算法的筆記,《資料結構與演算法之美》,極客時間的課程

排序對於每個程式設計師來說,可能都不會陌生。平常的專案中,也經常遇到排序。按時間複雜度,分成三類,分三節來說在這裡插入圖片描述

這裡,先丟擲一個問題。插入排序和氣泡排序的時間複雜度都是O(n2),可實際開發中,為什麼我們更傾向於使用插入排序演算法呢?

評價一個排序演算法,

從效率角度說,要考慮各種時間複雜度;資料量小時,時間複復雜度的係數、常數也要考慮;元素比較和交換的次數。

從記憶體消耗角度說,要考慮演算法的空間複雜度。空間複雜度是o(1)的排序演算法,叫原地排序演算法(Sorted in place)。今天說的三種演算法都是原地排序演算法。

從演算法穩定性來說,排序演算法又分為穩定性演算法和非穩定性演算法。穩定性,指相等元素,在排序後,原來的先後順序不改變。

穩定性演算法相對於不穩定性演算法,有什麼特別的用麼,反正是等值,誰先誰後,不都一個樣子麼?實際業務中,可能會按多個屬性排序。比如這麼一個場景:有一批訂單,按日期降序排列,如果日期一樣的,按訂單金額降序。

這個需要很好理解,實現起來,可能就沒那麼容易,按日期排好了,再把日期一樣的按訂單金額排一次。這個就不好實現,先把日期一樣的分別取出來排下,再對應放回去,很麻煩。

有了穩定性排序演算法,就很容易實現。第一步,先按訂單金額降序排列,第二步,再把第一步得到的結果,按日期降序重新排列一次,就可以實現需求了。為什麼自己琢磨一下吧。

接著,我們詳細說下這三種排序演算法,我會分別給出程式碼實現(java),簡要說下複雜度,還有演算法的評價等等。

氣泡排序演算法

基本的邏輯是,取第一個元素與後一個比較,如果大於後者,就與後者互換位置,不大於,就保持位置不變。再拿第二個元素與後者比較,如果大於後者,就與後者互換位置。一輪比較之後,最大的元素就移動到末尾。相當於最大的就冒出來了。再進行第二輪,第三輪,直到排序完畢。
在這裡插入圖片描述
程式碼實現,我們會做一些優化,如果是n個元素,第一輪比較(n-1)次,第二輪,比較(n-2)次就可以了。直到剩餘兩個元素,比較1次,排序操作就結束了。再進一步優化,如果,在某一輪操作中,只有比較操作,而沒有資料移動操作,那說明已經完全有序,不需要再進行下一輪了,直接結束即可。

	/**
	 * 氣泡排序
	 * 
	 * @param a
	 * @return
	 */
	public int[] bubbleSort(int[] a) {
		int n = a.length;
		if (n <= 1) {
			return a;
		}
		for (int i = 1; i < n; i++) {
			boolean flag = false; // 開關,當某次內層迴圈,沒有資料交換時,已排好順序,直接跳出迴圈。
			for (int j = 0; j < n - i; j++) {
				if (a[j] > a[j + 1]) {
					int temp = a[j];
					a[j] = a[j + 1];
					a[j + 1] = temp;
					flag = true;
				}
			}
			if (!flag) {
				break;
			}
		}
		return a;
	}

分析氣泡排序的時間複雜度,最好情況時間複雜度是O(n)。最好的情況本身就是有序的,比較了一輪,一次冒泡,不需要移動元素,排序完成。

最壞情況時間複雜度是O(n2)。最壞情況剛好是反序的(結合本例,就是倒序),要進行(n-1)輪的比較,每輪比較都要進行(n-1)次的位置移動。

平均情況時間複雜度也是為O(n2)。這個用加權平均算概率有點複雜。理論是,n個元素,排列方式就在有 n! 種,每種情況下,比較多少輪,每次多少資料移動都不一樣。有一點是確定的,上限是O(n2),下限是o(n)。
這裡,我們引入一個簡化的比較方式。
在這裡插入圖片描述
有序度:滿足 a[m] > a[n],且 m > n 的一對數,
逆序度:滿足 a[m] > a[n],且 m < n 的一對數,
滿序度:有序度的最大值(或逆序度的最大值)。潢序度 = 有序度 + 逆序度

對於氣泡排序,冒泡一次,有序度至少增加一,當達到滿度時,排序就結束。在 n! 種排列中,有序度最小值是0,最大值是(n-1)*n/2,平均值就是(n-1)*n/4。有序度可以評價氣泡排序的比較操作,移動元素的操作肯定比比較的操作少,那氣泡排序的平均情況時間複雜度是 (n-1)*n/4 至 最壞情況時間複雜度之間的值,不考慮係數,低階,得O(n2)。

氣泡排序的空間複雜度是O(1),資料移動需要一個臨時變數,屬於常量級別。

氣泡排序是穩定性演算法,從實現的程式碼,可以推知,當相同元素比對時,不會進行資料移動。

插入排序演算法

基本邏輯是,把元素分為已排序的和未排序的。每次從未排序的元素取出第一個,與已排序的元素從尾到頭逐一比較,找到插入點,將之後的元素都往後移一位,騰出位置給該元素。
在這裡插入圖片描述

	/**
	 * 插入排序
	 * 
	 * @param a
	 * @return
	 */
	public int[] insertSort(int[] a) {
		int n = a.length;
		if (n <= 1) {
			return a;
		}
		for (int i = 1; i < n; i++) {
			int temp = a[i];
			int j = i - 1;
			for (; j >= 0; j--) {
				if (a[j] > temp) {
					a[j + 1] = a[j]; // 比temp 大的已排序資料後移一位
				} else {
					break;
				}
			}
			a[j + 1] = temp; // 空出來的位置,把temp放進去
		}
		return a;
	}

插入排序,最好情況時間複雜度,如果已經是一個有序陣列了,就不需要移動資料。只要查詢到插入位置即可,每次只需要一次比較就可以找到插入位置,所以,這種情況下,最好情況時間複雜度為O(n)。

如果陣列是倒序的,每次插入都相當於在陣列的第一個位置插入資料,有大量移動資料的操作,所以,最壞情況時間複雜度是O(n2)。

陣列中插入一個數據平均時間複雜度是O(n),要進行n次操作,所以,平均情況時間複雜度是O(n2)。

空間複雜度是O(1)。也是一種穩定性演算法。

選擇排序演算法

基本邏輯是:把所有資料分為已排序區間和未排序區間。每次從未排序區間中,選出最小值,之後將該值與未排序區間第一個元素互換位置,此時已排序區間元素個數多了一個,未排序區間內的元素少了一個。如此迴圈直到未排序區間沒有元素為止。
在這裡插入圖片描述

	/**
	 * 選擇排序
	 * 
	 * @param a
	 * @return
	 */
	public int[] selectionSort(int[] a) {
		int n = a.length;
		if (n <= 1) {
			return a;
		}
		for (int i =1; i < n; i++) {
			int j = i-1;
			int min = a[j]; // 最小的數值
			int index = j; // 最小值對應的下標
			for (; j < n-1 ; j++) {
				if (min > a[j+1]) {
					min = a[j+1];
					index = j+1;
				}
			}
			//最小值a[index]與放未排序的首位a[i-1]互換位置
			if (index != i-1) {
				int temp = a[i-1];
				a[i-1] = a [index];
				a [index] = temp;
			}
		}
		return a;
	}

選擇排序演算法,最好情況時間複雜度與最壞情況時間複雜度,都是O(n2),空間複雜度是O(1),它是不穩定的演算法。相同元素,排序後,相對位置可能發生改變。

比較下三種演算法的效率,我在測試類中,分別取800,8000,80000個元素進行排序,結果如下

	@Test
	public void testParseResult() {

		Random rand = new Random();
		int length = 800;
		int[] a = new int[length];
		int[] b = new int[length];
		int[] c = new int[length];
		for (int i = 0; i < length; i++) {
			a[i] = rand.nextInt(100);
			b[i] = a[i];
			c[i] = a[i];
			// System.out.print(" " + b[i]);
			// if (i % 50 == 49) {
			// System.out.println();
			// }
		}
		long one = System.currentTimeMillis();
		insertSort(a);
		long two = System.currentTimeMillis();

		bubbleSort(b);
		long three = System.currentTimeMillis();
		
		selectionSort(c);
		long four = System.currentTimeMillis();

		System.out.println(" ");
		System.out.println("陣列長度值為:" + length);
		System.out.println("插入排序用時(單位毫秒):" + (two - one));
		System.out.println("氣泡排序用時(單位毫秒):" + (three - two));
		System.out.println("選擇排序用時(單位毫秒):" + (four - three));
		
//		System.out.println();
//		for (int i = 0; i < length; i++) {
//			 System.out.print(" " + a[i]);
//			 if (i % 10 == 9) {
//			 System.out.println();
//			 }
//		}
	}
陣列長度值為:800
插入排序用時(單位毫秒):3
氣泡排序用時(單位毫秒):5
選擇排序用時(單位毫秒):2

陣列長度值為:8000
插入排序用時(單位毫秒):56
氣泡排序用時(單位毫秒):148
選擇排序用時(單位毫秒):24

陣列長度值為:80000
插入排序用時(單位毫秒):2385
氣泡排序用時(單位毫秒):11067
選擇排序用時(單位毫秒):1163

現在說說文章開頭的問題,同為穩定性排序演算法,時間複雜度也一樣,插入排序的效率比氣泡排序要好,尤其是資料量大的時候,差距更明顯。為什麼?

氣泡排序移動資料的操作更多,只要是小於後一個元素,就移動一次。所以它的效率低。看測試結果,八萬的時候,就是數量級的差距了。