面試必備:深刻了解冒泡、選擇和插入排序的優缺點

前言

相信排序對於每個程序員來講都不會陌生,極可能你學的第一個算法就是排序,尤爲冒泡排序你們可能都是信手拈來,可是當從學校走入了職場以後,這些經典的排序已經慢慢淡出了咱們的視線,由於在平常開發中,高級語言已經幫咱們封裝好了排序算法,好比 C 語言中 qsort(),C++ STL 中的 sort()、stable_sort(),還有 Java 語言中的 Collections.sort(),這些算法無疑是寫得很是好的,可是咱們做爲有追求的程序員,對這些經典的排序算法仍是有必要了解得更透徹一點。程序員

本章,咱們一塊兒來探討一下三個經典排序算法:冒泡、選擇和插入排序。算法

思考

咱們都知道,在分析一個算法的好壞的時候,咱們第一反應就是分析它們的時間複雜度,好的算法時間複雜度天然會低,此外,空間複雜度也是衡量它們好壞的標準,好的算法的確也會在空間複雜度上作的比較好。數組

誠如上述,時間複雜度、空間複雜度基本是衡量算法的標準,可是對於排序算法來講,咱們須要考慮更多,那麼還有什麼因素會影響排序算法的好壞呢?緩存

影響因素

帶着問題,下面咱們一塊兒從如下方面探討排序算法的優點和劣勢。bash

1.時間複雜度

最好、最壞、平均時間複雜度性能

通常咱們所說的時間複雜度都是平均時間複雜度,可是若是須要細分,時間複雜度還分最好狀況、最壞狀況和平均狀況的時間複雜度,因此咱們須要經過這三種狀況來分析排序算法的執行效率分別是什麼。優化

時間複雜度的係數、常數和低階ui

咱們知道(平均)時間複雜度反應的是當n很是大的時候的一個增加趨勢,這時候它的係數、常數、低階每每都會被咱們忽略掉。可是當咱們排序的數據只有100、1000或者10000時,它們就不能被咱們忽略掉了。spa

比較次數和交換次數3d

進行排序時,咱們每每須要進行比較大小和交換位置,當咱們比較執行效率的時候,這些數據也須要考慮進去。

2.空間複雜度

算法的內存消耗能夠經過空間複雜度來表示,這裏有個概念,咱們稱空間複雜度爲O(1)的排序算法爲原地排序算法

3.排序算法的穩定性

排序算法的穩定性是指,在排序過程當中,值相同的元素間的相對位置跟排序前的相對位置是同樣的。舉個例子,排序前一個數組爲{3, 2, 1, 2', 4},咱們用2'來區分第二個2和第一個2,假如是穩定的排序算法,它的結果必定是這樣{1, 2, 2', 3, 4},而若是不穩定的算法,它的結果有多是這樣{1, 2', 2, 3, 4}。

爲何咱們要強調穩定性呢?舉個例子,假如咱們須要排序一個訂單,須要按照時間和價格進行升序排序,首先會先將全部訂單按時間升序排序,而後再進行價格的升序排序,假如價格排序不是一個穩定的排序,那麼訂單的時間就有可能不會按升序排列,因此在特定狀況下,排序算法的穩定性是一個比較重要的考慮因素。

冒泡排序

咱們先從冒泡排序開始分析。

冒泡排序原理是讓相鄰的兩個元素進行比較,看是否知足大小關係,若是不知足則交換位置,每一次冒泡會讓一個元素放到屬於它的位置,而後進行n輪冒泡,即完成冒泡排序。

舉個例子,假如咱們須要對數組{5,4,3,1,8,2}進行升序排序,那麼第一輪冒泡後,8就冒泡到了最後一個位置,由於它是最大值。

image

如此重複6輪,以下圖所示,便可將數組的每個元素冒泡到本身的位置,從而完成排序。

image

固然,這是沒有通過優化的冒泡排序,事實上當數組再也不進行數據交換時,咱們就能夠直接退出,由於此時已是有序數組。具體代碼以下:

public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 退出冒泡的標誌
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { 
        // 交換
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        // 表示有數據交換    
        flag = true;   
      }
    }
    // 沒有數據交換,提早退出
    if (!flag) 
        break; 
  }
}
複製代碼

結合上述代碼,咱們總結冒泡排序的特性。

1.時間複雜度

當數組是有序時,那麼它只要遍歷它一次便可,因此最好時間複雜度是O(n);若是數據是逆序時,這時就是最壞時間複雜度O(n^2),那麼平均時間複雜度怎麼算呢?

計算平均時間複雜度會比較麻煩,由於涉及到機率論定量分析,因此能夠用如下方法來計算,以上面排序數組爲例子,先來理解一下下面的概念:

  • 有序度:有序度是指一個數組有序數對的個數,例如上面數組爲排序前有序數對爲(4,5),(4,8),(3,5),(3,8),(1,5),(1,2),(1,8),(5,8),(2,8),因此有序度是9。
  • 滿有序度:滿有序度是指一個數組處於有序狀態時的有序數對,仍是上面數組排序完成後的有序數對有15個,用等差數列求和,得出求滿有序度的通用公式爲n(n-1)/2。
  • 逆序度:逆序度正好和有序度相反,因此上面數組對應的值是15-9=6,即逆序度=滿有序度-有序度。

理解了這三個概念後,咱們就能夠知道,其實將數組排序就是有序度增長,逆序度減少的過程,因此咱們排序的過程其實也是求逆序度大小的過程。

那麼咱們就能夠計算冒泡排序的平均時間複雜度了,最好狀況下,逆序度是0,最壞狀況下,逆序度是n(n-1)/2,那麼平均時間複雜度就取中間值,即n(n-1)/4,因此簡化掉係數、常數和低階後,平均時間複雜度爲O(n^2)。

2.空間複雜度

因爲冒泡排序只須要常量級的臨時空間,因此空間複雜度爲O(1),是一個原地排序算法。

3.穩定性

在冒泡排序中,只有當兩個元素不知足條件的時候纔會須要交換,因此只有後一個元素大於前一個元素時才進行交換,這時的冒泡排序是一個穩定的排序算法。

選擇排序

選擇排序的原理是從未排序部分選擇最小的一個元素放到已排序部分的後一個位置,最後使得數組有序的過程。

跟冒泡排序同樣,咱們分析選擇排序時也是對數組{5,4,3,1,8,2}進行排序,整個排序過程以下圖所示。

image

通過6次循環,完成了數組排序,具體實現代碼以下:

public static int[] SelectSort(int[] array) {
	for(int i = 0;i < array.length; i++) {
		int index = i;
		for(int j = i; j < array.length ;j++) {
		    // 找出每一輪的最小值
			if(array[j] < array[index]) {
				index = j;
			}
		}
		// 和已排序部分的後一個位置進行交換
		int temp = array[i];
		array[i] = array[index];
		array[index] = temp;
	}
    return array;
}
複製代碼

經過上面的排序過程圖和代碼對選擇排序進行分析。

1.時間複雜度

選擇排序的最好狀況時間複雜度、最壞狀況和平均狀況時間複雜度都是O(n^2)。由於原數組不管是否有序,進行排序時都是須要每個元素對進行比較,當比較完成後還要進行一次交換,因此每一種狀況的時間複雜度都是O(n^2)。

2.空間複雜度

由於只須要用到臨時空間,因此是一個原地排序算法,空間複雜度爲O(1)。

3.穩定性

以下圖所示,咱們排序{5,5',1},得知排序後兩個5的位置已經交換,因此不是穩定排序。

image

插入排序

先想一下,當咱們往有序數組裏面插入一個元素,而且使得它插入後保持數組的有序性,咱們應該如何操做呢?以下圖。

image

插入排序就是上面圖示的操做,從無序數組中拿到一個元素,而後往有序數組裏面尋找它的位置,而後插入,有序數組元素加一,無序數組減一,直至無序數組的長度爲0。

同上兩個排序算法同樣,咱們使用插入排序對數組{5,4,3,1,8,2}進行排序,流程以下圖所示。

image

如上述流程能夠知道,每一次從無序數組中抽第一個元素,緩存起來,而後在從已排序的最後一個元素開始比較,當該元素大於已排序元素,已排序元素後移一位,直到遇到比他小的元素,而後在比他小的元素的後一個位置插入緩存起來的元素,這樣已排序數組增長一個元素,未排序數組減小一個元素,直至結束。

綜上,能夠得知算法以下:

public void insertSort(int[] arr, int n) {
	if (n <= 0) {
		return;
	}

	for (int i = 0; i < n; i++) {
		int temp = arr[i];
		// 從有序數組的最後一個元素開始往前找
		int j=i-1;
		while(j>=0) {
			if (arr[j] > temp) {
			    // 若是當前元素大於temp,則後移
				arr[j+1] = arr[j];
				j--;	
			} else {
			    // 若是當前元素小於temp,說明temp比前面全部都要大
				break;
			}
		} 
		// 插入
		arr[j+1] = temp;
	}
}
複製代碼

經過上面的排序過程圖和代碼對插入排序進行分析。

1.時間複雜度

最好時間複雜度爲當數組爲有序時,爲O(n);最壞時間複雜度爲當數組逆序時,爲O(n^2)。已知,往一個有序數組插入一個元素的平均時間複雜度爲O(n),那麼進行了n次操做,因此平均時間複雜度爲O(n^2)。

2.空間複雜度

插入排序爲原地排序,因此空間複雜度爲O(1)。

3.穩定性

插入排序每一次插入的位置都會大於或者等於前一個元素,因此爲穩定排序。

三者比較

通過上述對三個時間複雜度均爲O(n^2)的算法進行分析,能夠知道它們的差別以下表所示:

image

經過對比能夠看到,冒泡和插入排序都是優於選擇排序的,可是插入排序每每會比冒泡排序更容易被選擇使用,這是爲何呢?

雖說冒泡和插入在時間複雜度、空間複雜度、穩定上表現都是同樣的,可是咱們別忽略了一個事情,咱們求出來的時間複雜度是忽略了係數、常數和低階參數的,回看代碼咱們知道,冒泡和插入在進行交換元素的時候分別是以下這樣的,假如一次賦值操做耗時爲K,那麼冒泡排序執行一次交換元素就須要3K時間,而插入排序只須要K。

// 冒泡排序交換元素
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
複製代碼
// 插入排序交換元素
arr[j+1] = arr[j];
複製代碼

固然,這只是理論分析,當我用我本身的機器,分別用冒泡和插入算法對10000個一樣的數組(每一個數組200個元素),進行排序,冒泡排序耗時490ms,插入排序耗時8ms,因此可見插入排序因爲每次比較的耗時比較短,因此總體來講耗時也會比冒泡要少。

總結

雖然這三種排序的複雜度較高,達到O(n^2),冒泡和選擇排序也不可能使用到應用實踐中去,都只能停留在理論研究上,而插入排序仍是以其原地排序和穩定性可以在某些特定的場景中使用。

相關文章
相關標籤/搜索