排序,我想大家一定經歷過或者正在經歷着。或許你不懂算法,對排序算法一無所知,但是你一定用過一些第三方庫的api來一鍵排序,那么,在你享受便捷的同時,你是否想過它的底層是如何實現的?這樣的算法實現方式是不是最好的?還有沒有其它的可能性來實現更快速的排序?那么,希望這一篇文章過后。對於排序算法,你不會再覺得陌生和迷惑。
這篇文章會介紹一些簡單常用的排序算法,比如我們耳熟能詳的冒泡排序,以及選擇排序、插入排序、歸並排序等等等等。當然,你一旦學會了這些算法在js中的實現方式,其實你也就弄懂了這種算法。就算以后要用其它語言來實現這些算法,也不過就是一些語言特性上的差別罷了。
我們會專門寫一個數組類,並在其中加入各種排序算法。那么,我們先開始搭一個簡單的架子。
function ArrayList() { var array = []; this.insert = function (item) { array.push(item); }; this.toString = function() { return array.join(); }; }
我們構建一個數組類,並且只有一個insert和toString方法,以便我們輸入數組元素和打印數組元素。
下面我們為這個數組類添加各種排序方法。我們先從最簡單的開始。
1、冒泡排序
冒泡排序十分簡單,就是比較數組中任何兩個相鄰的元素,如果第一個比第二個大,那么就交換兩個元素的位置。這樣,較大值得元素就會一點一點移動到正確的位置,就像氣泡升至表面一樣。我們先來看一下代碼。
//交換數組中兩個相鄰元素的方法,傳入的是相關的數組,以及相鄰兩個元素的下標。 var swap = function (array,index1,index2) { // 這里,是最“普通”的方式,通過一個中間量來存儲index1元素,因為要把index1的值設置為index2,然后再把index2的值設置為剛才存儲index1的aux變量。 // 希望我上面說的足夠清楚。 var aux = array[index1]; array[index1] = array[index2]; array[index2] = aux; // 但是我們可以有更為簡便的方式來做到替換兩個數組元素位置的方式。 // 一個是下面的ES6新增的方法。數組的解構賦值。不多說,大家可以自行去查看。 //[array[index1],array[index2]] = [array[index2],array[index1]]; // 另外一個方法是利用數組的splice方法,刪除從index1開始的兩個元素,並且在刪除的位置插入index2,和index1以達到替換元素的目的。
// 如果你對數組方法還不是很清楚,請看這里用js來實現那些數據結構02(數組篇02-數組方法)
//array.splice(index1,2,array[index2],array[index1]); }; // 這是最簡單,當然也是最慢的排序方法,我們需要兩層循環來判斷值得大小,以此來一步一步確定每一個值得位置。 // 這里我要刨根問底一下,為什么外層循環(i)循環的是數組元素的長度,而內層(j)是比數組長度少1的循環次數? // 因為這是可以保證每兩個元素都進行過比較的最小的循環次數,不信你把兩次循環的次數增加(length +100000);來試一下,發現結果仍舊是我們想要的。(當然,這更耗費時間。) // 但是你把次數減得更少就不行了,排序的結果就不對了(其實這里可以合理的減少內層循環的次數,后面說)。你還可以這么理解,外層循環控制我們有多少個數需要比較,內層循環去具體的操作兩個數的比較。 this.bubbleSort = function () { var length = array.length; for(var i = 0; i < length; i++) { //i=0,1,2,3,4...length-1; //console.log(i,"i"); for(var j = 0; j < length-1;j++) {//j = 0,1,2,3,4...length - 1 - 1; //console.log(j,"j"); if(array[j] > array[j+1]){ swap(array,j,j+1) } } } }; //我們來寫一個簡單的方法生成一個未排序的arraylist。//這個方法是在構造函數外的。你不用也可以。只是為了方便生成一個數組罷了。 function creatArrayList (size) { var array = new ArrayList(); for(var i = size; i > 0; i--) { array.insert(i); } return array; } var arraylist = creatArrayList(5); console.log(arraylist.toString());//5,4,3,2,1 arraylist.bubbleSort(); console.log(arraylist.toString());//1,2,3,4,5
上面的解釋我相信已經夠詳細了,我們下面接着看看是否可以改進一下這垃圾的耗時間的效率低的冒泡排序,讓我們在簡單實現的基礎上提高一點點性能?
//咱們看看modifiedBubbleSort和bubbleSort的區別,唯一不同的地方就在於內層循環的時候在for循環的第二個條件中多減了一個i。這么做的用意是什么呢? // 我們一點一點來捋一下這點點代碼。假設我們的數組是【5,4,3,2,1】;當i = 0的時候(第一次外循環),我們拿5去依次和4,3,2,1來比較,最后數組漸變成了【4,3,2,1,5】; // 那么此時,5就是最大的,當i=1的時候(第二次外循環)。此時我們的內循環就不再需要去拿當前的j去與5(也就是數組中確定了的最大的)比較。 // 外層每循環一次,內層中的每個元素就相互交換了一下位置(如果符合條件的話),最終每一次內層循環完畢,都會確定一個當前輪數最大的值。 // 那么既然我們已經知道最大的值是什么,就無需在后面的循環輪數中再去和已經確定了位置的值去做比較了。這樣就可以提高一點我們的執行效率。 // 這里再多句嘴,當i = 0時,j = 0,j < length - 1 - 0;當i = 1時,j = 0,j < length - 1 - 1;(要理解這句話) this.modifiedBubbleSort = function () { var length = array.length; for(var i = 0; i < length; i++) { for(var j = 0; j < length - 1 - i;j++) { if(array[j] > array[j+1]) { swap(array,j,j+1); } } } };
改進后的冒泡排序我們也學會了,但是也就只能這樣了。沒辦法再進一步的進行優化和效率的提升。冒泡排序,是最基礎的,最不推薦的排序方式。因為它的時間復雜度是O(n2),大O表示法,我們會在后面的內容中詳細的講解什么是大O表示法。這里可以暫時的理解為,兩層循環形成了i*j的計算結果,也就是length*length的循環總次數。也就是n2.。(事實上就是這么回事)。如果是三層循環,那很有可能就是O(n3)的復雜度了。
2、選擇排序
選擇排序的思想是,找到數據結構中最小值並將其放在第一位,接着找到第二小的值,放到第二位,以此類推。(當然你也可以找最大的值放在最后一位)。我們還是來看代碼。
//其實選擇排序也並不復雜,我們來一起看一看。 this.selectionSort = function () { //首先,我們聲明一個存儲數組長度的變量,以及一個存儲當前最小值的變量,哦不對,存儲當前最小值的對應下標的變量。 var length = array.length,indexMin; // 在外層循環,我們循環次數是整個數組的長度。 for(var i = 0; i < length - 1; i++) { // 這里,最開始的循環我們也不知道誰是最小的,所以我們就把第一個值得下標作為最小值得下標。 indexMin = i; // 內層循環,我們會依次比較當前最小值(也就是indexMin對應的值)和數組中的其它值。 // 這里,為什么是j=i呢? // 我們每一次外層循環,都會確定一個最小值並把最小值放置在相應的位置(從下標0開始每次外循環都會往后加1)。 // 那么我們就不需要再去比較開始循環過比較過的下標了,所以我們每次外層循環過后,內層循環都從i的位置開始就可以了。 // 有點類似於modifiedBubbleSort的j<length - 1 - i; for(var j = i; j < length; j++) { // 這樣,我們就可以判斷出最小值是什么,如果indexMin所對應的值比j所對應的值還要大,說明最小值對應的下標應該為j。 if(array[indexMin] > array[j]) { indexMin = j; } } // 在外層循環一次結束后,如果i(最開始我們確定的最小值)不等於indexMin。這說明i並不是最小值。我們就交換兩個值的位置。 // 如果相等,說明當前的indexMin就是最小值,無需交換位置。 console.log(i,indexMin) if(i !== indexMin) { swap(array,i,indexMin) } } };
不過選擇排序的復雜度也是O(n2),效率並不是很好。那么我們繼續往下看。
3、插入排序
插入排序,怎么說呢....就是假設數組中的第一個元素是已經排序過的了(不假設不行,或者說它就是排過序的了,因為就一個元素嘛),那么我們和第二個元素比較,第二個元素是應該在第一個元素之前,還是在原位置不動呢?也就是說,第一個元素和第二個元素比較大小來確定這兩個元素的位置。那么這樣,單純就數組元素的前兩項來說,他們是排好序的了。那么我們再以第三個元素跟前兩個元素進行比較,來確定第三個元素應該插入在前兩項元素的什么位置。
簡單來說,我們可以認為在排序數組中有一個已排序的子數組,我們依次用后面的元素與子數組中的元素進行比較,以確定后面的元素應該插入到子數組的什么位置。最后,我們就會得到一個完全排序的數組了。
我們繼續來看代碼。
this.insertionSort = function () { //j和temp分別用來存儲當前的下標和當前下標所對應的值。 var length = array.length,j,temp; // 為什么i要從1開始呢?因為index為0的元素我們視為已經排序過的了。 for(var i = 1; i < length; i++) { // 我們用j來存儲當前要比較的值的下標也就是i,因為0上的元素已經排序過了(我們默認這樣做的)。 j = i; // 同樣,我們要比較當前索引的值得大小來確定是否需要換位,所以我們還要有一個臨時存儲當前下標所對應的值的變量。 temp = array[i]; // 如果j>0說明是數組中的元素, // 並且,如果當前j(i)的前一個元素(j-1)比當前的變量大,那么就把j(i)的值設置為j-1的值,也就是把j(i)的位置往后挪了一個。 // 直到array[j - 1] > temp為false為止。為什么不說j>0這個條件呢?因為這是保證數組正確對比的一個防護層,當然,它是很重要的。 // 這里有一個十分必要且需要注意的point,就是我們的變量j的值的問題。 // 我們在循環直到條件不成立跳出循環的時候,此時的j就是需要把臨時存儲array[i]的值(也就是temp)插入的地方。 // 因為,我們在while循環中,每次循環都會用j--來使下標的位置一點點往前移動,直到條件不成立后,我們得到了一個應該插入temp的位置的j。 while (j > 0 && array[j - 1] > temp) { array[j] = array[j - 1]; j--; } array[j] = temp; } };
上面的代碼就是插入排序了,其中要注意的點就在while循環這一塊,需要花費一點心思去理解一下。我在簡單的啰嗦兩句,其實在代碼中,我們聲明了j和temp變量用來存儲當前的下標和其所對應的值。那么temp的作用是我們可以在找到該插入的位置的時候,可以知道應該插入的值是什么,而j的存在的意義是確定這個位置是哪里。所以,我們在while循環中會拿遞減的j所對應的值的前一個去和temp比較,如果條件成立,那么我就往后挪,直到挪不動為止(while循環的條件不匹配),我們就找到了應該插入temp位置的j。這時候在該位置上插入temp就可以了。
那么,簡單的排序方法就介紹到這里了,下一章我們來看看復雜一點的,但是效率更高的排序算法。
其實我在寫這篇文章的時候,一直在糾結要不要去畫畫圖,讓大家可以更容易的去理解這些代碼和這些排序算法的實現方式,但是,我在網上搜了一下。一大堆!!所以,我就覺得算了吧。不過文中我已經附上了相關的鏈接地址,其中有對該算法的概念的更為詳細的解釋。
最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!