數據結構與算法的重溫之旅(九)——三個簡單的排序算法

前面的幾篇文章講了一些基礎的數據結構類型,此次咱們就深刻算法,先從簡單的排序算法提及。在排序算法中,入門必學的三個算法分別是冒泡排序、插入排序和選擇排序。下面就具體講一下這三個算法的原理和代碼實現算法

1、冒泡排序(Bubble Sort)

冒泡排序只會操做相鄰的兩個數據。每次冒泡操做都會對相鄰的兩個元素進行比較,看是否知足大小關係要求。若是不知足就讓它倆互換。一次冒泡會讓至少一個元素移動到它應該在的位置,重複 n 次,就完成了 n 個數據的排序工做。數組

打個比方,好比[6, 5, 4, 3, 2, 1],先從下標爲0的元素開始,第一個位元素比第二位大,因此第一位與第二位的位置互相交互,而後第二位與第三位的互相比較,第二位的比第三位的大,這兩位互相交互,以此類推。當完成一輪比較後就會從下標位1的元素開始重複上述過程,直到下標爲n-1時則中止。因此能夠得出時間複雜度是O(n)=n^{2}。下面是具體的代碼實現:bash

var arr = [5,3,6,2,8,1,9,4,7,10,11,34,12]
var index = 0
while (index < arr.length - 1) {
	for (var i = 0; i < arr.length - index; i++) {
		var temp = ''
		if (arr[i] > arr[i+1]) {
			temp = arr[i+1]
			arr[i+1] = arr[i]
			arr[i] = temp
        }
    }
	++index
}複製代碼

在這裏提一個知識點,若是涉及到兩數交換的話除了用一個臨時變量來暫存這種方法外,還有用異或運算來實現兩數交換。上述的代碼能夠改寫以下:數據結構

var arr = [5,3,6,2,8,1,9,4,7,10,11,34,12]
var index = 0
while (index < arr.length - 1) {
	for (var i = 0; i < arr.length - index; i++) {
		var temp = ''
		if (arr[i] > arr[i+1]) {
			arr[i] = arr[i] ^ arr[i+1]
			arr[i+1] = arr[i] ^ arr[i+1]
			arr[i] = arr[i] ^ arr[i+1]
        }
    }
	++index
}複製代碼

異或運算是位運算,也就是將數字轉換成二進制來運算。異或運算是兩個數相同位上的數互相比較,只要是相同則返回0,不一樣則爲1。拿個簡單的例子:post

var a = 1, b = 2
a = a ^ b
b = a ^ b
a = a ^ b複製代碼

第一步異或運算裏,a爲1,對應的二進制是01,b爲2對應的二進制是10,因此運算後a爲11,也就是3。第二步的異或運算裏,a爲11,b爲10,運算後可得b爲01,也就是1。到第三步裏,a爲11,b爲01,可得a爲10,也就是2。這就不利用臨時變量來實現兩數交換。位運算在處理數據量比較大的狀況下十分的高效,可是因爲冒泡排序算法的時間複雜度過高,因此在大數據的狀況下還不如換另外一種時間複雜度低的算法。性能

冒泡排序除了上述利用位運算來優化外還能夠經過判斷後面的元素是否有交換來提早結束冒泡,優化改進以下:大數據

var arr = [5,3,6,2,8,1,9,4,7,10,11,34,12]
var index = 0
while (index < arr.length - 1) {
    var state = false
	for (var i = 0; i < arr.length - index; i++) {
		var temp = ''
		if (arr[i] > arr[i+1]) {
			temp = arr[i+1]
			arr[i+1] = arr[i]
			arr[i] = temp
            state = true
        }
    }
    if (!state) break
	++index
}複製代碼

在這裏,若是判斷到後面沒有發生交換,則能夠判斷後面的元素已經有序,則不須要再次遍歷數組,提升了算法的性能。優化

2、插入排序(Insertion Sort)

插入排序的思想比上面的冒泡排序的思想要複雜一點。插入排序是將數組分紅兩個區間,一個是有序區間,一個是無序區間。通常的會將數組第一個元素默認爲有序區間,而後將無序區間中的元素插入到有序區間相應的位置中,直到無序區間爲0爲止,時間複雜度是O(n)=n^{2}。代碼以下:ui

for (var i = 1; i < n; ++i) {
  var value = a[i];
  // 查找插入的位置
  for (var j = i - 1; j >= 0; --j) {
    if (a[j] > value) {
      a[j+1] = a[j];  // 數據移動
    } else {
      break;
    }
  }
  a[j+1] = value; // 插入數據
}

複製代碼

3、選擇排序(Selection Sort)

選擇排序和上面的插入排序相似,也是分有一個有序區間和無序區間,與插入排序不一樣的是,選擇排序裏插入到有序區間的元素是無序區間裏的最小值,時間複雜度是O(n)=n^{2}。代碼以下:spa

var arr = [5,3,6,2,8,1,9,4,7,10,11,34,12]
for (var i = 0; i < arr.length - 1; i++) {
	var min = arr[i]
	var index = i
	for (var j = i; j < arr.length; j++) {
		if (min > arr[j]) {
			min = arr[j]
			index = j
		}
    }
	var temp = arr[i]
	arr[i] = min
	arr[index] = temp
}複製代碼

4、排序算法的分析

在用算法的時候,不止要懂原理,也要懂如何根據它的性能來使用到不一樣的場景中,下面就以三個點來講一下算法性能的分析。

1.執行效率

咱們在分析算法的時間複雜度的時候,要分別的列出最好狀況、最壞狀況合評價狀況的時間複雜度。爲何要作區分呢,首先爲了算法之間更好的對比性能,其次是有些極端狀況,好比說在高度有序或者雜亂無章的狀況下執行的時間會各有不一樣,因此咱們要知道排序算法在不一樣數據下的性能表現。

除此之外,以前所要忽略的係數、常數、低階也要考慮進來。咱們知道,時間複雜度反應的是數據規模 n 很大的時候的一個增加趨勢,因此它表示的時候會忽略係數、常數、低階。可是實際的軟件開發中,咱們排序的多是 10 個、100 個、1000 個這樣規模很小的數據,因此,在對同一階時間複雜度的排序算法性能對比的時候,咱們就要把係數、常數、低階也考慮進來。

還有一點,在上面說提到的三個基於比較的排序算法,都涉及到比較和替換。因此,若是咱們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。

2.內存消耗

算法的內存消耗其實就是算法所額外佔用空間的多少,能夠經過空間複雜度來衡量。針對排序算法的空間複雜度,這裏引入了一個新概念原地排序(Sorted in place)。原地排序算法,就是特指空間複雜度是 O(1) 的排序算法。咱們上面講的三種排序算法,都是原地排序算法。

3.算法的穩定型

僅僅用執行效率和內存消耗來衡量排序算法的好壞是不夠的。針對排序算法,咱們還有一個重要的度量指標,穩定性。這個概念是說,若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變。

好比一組數據5,2,3,2,6,1。經排序後可得1,2,2,3,5,6。這組數據裏有兩個 2。通過某種排序算法排序以後,若是兩個 2 的先後順序沒有改變,那咱們就把這種排序算法叫做穩定的排序算法;若是先後順序發生變化,那對應的排序算法就叫做不穩定的排序算法。

可能看着這個例子看不出什麼起來,可是若是要排序的是對象元素,則很容易看出算法的穩定性。好比說,咱們如今要給電商交易系統中的「訂單」排序。訂單有兩個屬性,一個是下單時間,另外一個是訂單金額。若是咱們如今有 10 萬條訂單數據,咱們但願按照金額從小到大對訂單數據排序。對於金額相同的訂單,咱們但願按照下單時間從早到晚有序。對於這樣一個排序需求,咱們怎麼來作呢?藉助穩定排序算法,這個問題能夠很是簡潔地解決。解決思路是這樣的:咱們先按照下單時間給訂單排序,注意是按照下單時間,不是金額。排序完成以後,咱們用穩定排序算法,按照訂單金額從新排序。兩遍排序以後,咱們獲得的訂單數據就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。爲何呢?

穩定排序算法能夠保持金額相同的兩個對象,在排序以後的先後順序不變。第一次排序以後,全部的訂單按照下單時間從早到晚有序了。在第二次排序中,咱們用的是穩定的排序算法,因此通過第二次排序以後,相同金額的訂單仍然保持下單時間從早到晚有序。若是是先排金額再排時間的話,排時間的時候可能某個很前的時間端有個很大的金額,這個時候大金額可能排到前面致使不能知足先金錢有序的前提。下面就以上面的三個算法來更加詳細的說明。

5、三個排序算法之間的比較

首先以是不是原地排序算法爲例。冒泡排序算法涉及到相鄰的交換和比較操做,只須要常量級的臨時空間,因此空間複雜度是O(1), 是原地排序算法。插入排序和冒泡排序同樣也執行了交換和比較操做,因此也是原地排序,空間複雜度爲1。同理,因爲選擇排序和插入排序相似,因此也是原地排序算法。

再來講一下是不是穩定排序算法。在冒泡排序中,只有交換才能夠改變兩個元素的先後順序。爲了保證冒泡排序算法的穩定性,當有相鄰的兩個元素大小相等的時候,咱們不作交換,相同大小的數據在排序先後不會改變順序,因此冒泡排序是穩定的排序算法。

在插入排序中,對於值相同的元素,咱們能夠選擇將後面出現的元素,插入到前面出現元素的後面,這樣就能夠保持原有的先後順序不變,因此插入排序是穩定的排序算法。

那選擇排序是穩定排序算法嗎?答案是否認的,選擇排序是一種不穩定的排序算法。選擇排序的定義裏,每次都要找剩餘未排序元素中的最小值,並和前面的元素交換位置,這樣破壞了穩定性。

6、有序度和無序度

在進行時間複雜度的比較以前,咱們先來過一下有序度和無序度。咱們先以冒泡排序爲例。若是一組數據已經排好序了,那麼在用冒泡排序進行排序的時候,只需遍歷一層循環則能夠了得出結果,因此最好時間複雜度是O(n)。可是若是數據是徹底倒序,則要進行n次冒泡操做,則最壞條件下時間複雜度是O(n^{2})。這個時候求平均時間複雜度的時候就用到標題上寫的有序度和無序度了。

有序度是數組中具備有序關係的元素對的個數。有序元素對用數學表達式表示就是這樣:

有序元素對:a[i] <= a[j], 若是 i < j。
複製代碼

好比3,4,6,5,2,1這個數據裏,有序度爲5,有序元素的對數分別是(3,4), (3,6), (3,5), (4,6), (4,5)。對於有序度是n*(n-1)/2,咱們能夠把它稱做爲滿有序度,而逆序度的計算公式則是滿有序度減有序度。

咱們從上面知道,冒泡排序包含兩個操做原子,比較和交換。每交換一次,有序度就加 1。無論算法怎麼改進,交換次數老是肯定的,即爲逆序度,也就是n*(n-1)/2–初始有序度。如上面的3,4,6,5,2,1這個例子,則要進行10次交換操做。

對於包含 n 個數據的數組進行冒泡排序,平均交換次數是多少呢?最壞狀況下,初始狀態的有序度是 0,因此要進行 n*(n-1)/2 次交換。最好狀況下,初始狀態的有序度是 n*(n-1)/2,就不須要進行交換。咱們能夠取箇中間值 n*(n-1)/4,來表示初始有序度既不是很高也不是很低的平均狀況。換句話說,平均狀況下,須要 n*(n-1)/4 次交換操做,比較操做確定要比交換操做多,而複雜度的上限是 O(n^{2}),因此平均狀況下的時間複雜度就是O(n^{2})

在插入排序中,若是要排序的數據已是有序的,咱們並不須要搬移任何數據。若是咱們從尾到頭在有序數據組裏面查找插入位置,每次只須要比較一個數據就能肯定插入的位置。因此這種狀況下,最好是時間複雜度爲 O(n)。注意,這裏是從尾到頭遍歷已經有序的數據。若是數組是倒序的,每次插入都至關於在數組的第一個位置插入新的數據,因此須要移動大量的數據,因此最壞狀況時間複雜度爲 O(n^{2})。在前面的文章中咱們得知,在數組中插入一個數據的平均時間複雜度是多少嗎?沒錯,是 O(n)。因此,對於插入排序來講,每次插入操做都至關於在數組中插入一個數據,循環執行 n 次插入操做,因此平均時間複雜度爲 O(n^{2})

同理,咱們能夠經過上面的兩個算法能夠得出選擇排序的最快時間複雜度是O(n),最慢時間複雜度是O(n^{2}),平均時間複雜度是O(n^{2})

在這裏咱們能夠得出一個結論,因爲選擇排序不是一個穩定性排序算法,即便和冒泡和插入排序同樣是原地排序算法和時間複雜度同樣,但因爲這個缺點,因此就選擇插入排序或冒泡排序。而在冒泡排序和插入排序中對比,咱們能夠發現冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序須要 3 個賦值操做,而插入排序只須要 1 個。插入排序比冒泡排序少了兩個步驟使得它的性能比冒泡排序要好一點。可能在小數據下看不出來,可是涉及到大數據的狀況下這點細微的差異就會被放大出來了。


上一篇文章: 數據結構與算法的重溫之旅(八)——遞歸

下一篇文章:數據結構與算法的重溫之旅(十)——歸併排序和快速排序​​​​​​​

相關文章
相關標籤/搜索