今天咱們來討論的問題有兩個:面試
如何用JavaScript實現選擇排序、冒泡排序、插入排序、快速排序、歸併排序、堆排序;算法
對生成的10萬個隨機數進行排序,各個排序算法的性能分析。數組
這裏咱們所有用數組來存儲數據,首先建立一個類ArrayList。
其中屬性的說明以下:數據結構
array空數組--->用以存放數據dom
insert()方法--->往array中插入數據函數
swapItemInArray(n,m)方法--->將array中第n個元素和第m個元素交換位置性能
toString()方法--->將array數組轉換爲字符串學習
originSort()方法--->JavaScript原生排序算法實現,在以後的性能比較中,咱們會用到它ui
function ArrayList(){ var array = [] this.insert = function(item){ array.push(item) } this.swapItemInArray = function(n,m){ let temp = array[n] array[n] = array[m] array[m] = temp } this.toString = function(){ return array.join() } this.originSort = function(){ array.sort(function(a,b){ return a-b }) } }
先來實現最簡單的選擇排序。
其思路是:對於有N個數字的數組,進行N輪排序,在每一輪中,將最大的數找出,放到末尾。下一輪的時候再找出次大的數放到倒數第二位。
咱們來爲ArrayList類添加以下方法this
this.selectSort = function(){ var self = this var len = array.length var maxIndex for (var i = 0; i < len; i++) { maxIndex = 0 //初始化最大數的位置 for (var j = 0; j < len - i; j++) { if (array[maxIndex] < array[j]) { //每一次都和以前的最大數比較 maxIndex = j //若是大於以前的最大數,則紀錄當前數爲最大數 } } //第i輪結束後,將最大數放到數組倒數第i個 self.swapItemInArray(maxIndex,len-i-1) } }
選擇排序是否是太簡單了?接下來咱們就來實現冒泡排序。
思路:對於有N個數字的數組,進行N輪排序,在每一輪中,從前日後以此比較兩兩相鄰的數字,每次比較後,都把大的日後放,一輪下來,最大的數會被推到數組最後。
this.bubbleSort = function(){ var self = this var len = array.length for (var i = 0; i < len; i++) { //數組長度要遍歷的趟數 //第i趟以後,後面i個元素都不用比較 for (var j = 0; j < len - 1 - i; j++) { if (array[j]>array[j+1]) { //兩兩相鄰進行比較 self.swapItemInArray(j,j+1) //將較大的數字放到後面 } } } }
插入排序的實現思路以下:
對於有N個數字的數組,進行N輪排序
第一輪 將第2個數字與第1個數字比較,若是第2個數字小,則與1交換
第二輪 將第3個數字與第2個數字比較
若是第3個數字小,則與第2個數字交換,再用第2個數字與第1個數字比較,將小的放前面。
第i輪 第1個到第i-1個數字已經全是從小到大排列了,第i個數字與前面的數字依次比較並交換位置,使得第1個數字,到第i個數字也是從小到達排列。
代碼以下
this.insertSort = function(){ var self = this var len = array.length for (var i = 0; i < len; i++) { for (var j = i; j>0; j--) { if (array[j]<array[j-1]) { self.swapItemInArray(j,j-1) } } } }
上面幾種排序的實現是否是很小兒科呢?下面的就要稍微複雜點了。
快速排序算法基本上是面試必考排序算法,也是傳聞最好用的算法。
不過實現起來可一點都不容易,至少對我來講是這樣。
算法思想
本質上快速排排序是一種分治算法的實際應用。
按照下圖所示,對於左邊的原始數集合,(隨便地)取一個數(稱其爲主元),好比取65爲主元,則65則將原來的集合劃分爲了A集合和B集合,A中全部的數字都小於65,B中全部的數字都大於65。
而後。
以後再對A集合和B集合採起相同方式的劃分。
最後就分爲了從小到大排列的衆多小集合。
實現思路
對於有N個數字的數組,進行大約logN輪的排序。
若每次都能劃分爲兩等份,則效率最高。若是選擇的那個數將數組劃分爲了一、N-1長度的兩個數組則效率會很是低。
所以,主元的選擇很是關鍵。不能用JavaScript中所提供的Math.random()得到主元,由該函數生成隨機數代價昂貴。
根據相關資料,一個比較好的方法爲:取首項、中間項、末尾項中的中值做爲劃分基準。
主元的具體實現函數以下
它傳入三個參數,數組自己、首項索引、尾項索引。
在查找中值的時候,順便將三個值分別排列成,首項最小、中位數中等、尾項最大。
爲了方便後續的劃分,將主元和倒數第二個數進行交換(因爲尾項已經大於中值,所以沒必要對其進行操做,故主元放到倒數第二個)
function findPivot(arr,left,right){//主元取中位數 var center = Math.floor((left+right)/2) if (arr[left] > arr[center]) { self.swapItemInArray(left,center) } if (arr[left] > arr[right]) { self.swapItemInArray(left,right) } if (arr[center] > arr[right]) { self.swapItemInArray(center,right) } self.swapItemInArray(center,right-1)//主元被藏在倒數第二個 return arr[right-1] }
每趟如何劃分?
如下圖爲例,「9」爲主元,咱們把它放在最後一個。
這裏設置i和j兩個指針(JavaScript中則是數組下標),i指向首項「2」,j指向倒數第2個數字「7」。
讓i指針往右邊移動,遇到>=主元「9」的項時停下來
讓j指針往左邊移動,遇到<=主元「9」的項時停下來
交換i和j所指的值,而且i右移一位,j左移一位
i指針和j指針繼續移動比較交換
當i與j發生交錯時,本趟劃分結束,把主元與i所指的「6」進行交換(即把主元放回原位)。此時數組被劃分爲了兩個[0,...,j]、[i+1,...,last]。[0,...,j]中的全部元素都小於主元,[i+1,...,last]中全部元素都大於主元。
對劃分出來的兩個子數組繼續進行下一步劃分。
具體函數實現以下,因爲數組長度過小時採用快速排序效率較低,於是當數組長度過小時,咱們改用插入排序。
function partition(arr,left,right){ //分割操做 var pivot = findPivot(arr,left,right) //找到主元 var length = right - left if (length>cutoff) { //當劃分組沒有小於閾值時,繼續採用快速排序 var i = left var j = right - 2 while(i<=j){ //i和j沒有交錯 while(arr[i]<pivot){ i++ } while(arr[j]>pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) //結束後將主元放回原位 if (left<i-1) { //對主元左側的子數組展開快排 partition(arr,left,i-1) } if (i+1<right) { //對主元右側的子數組展開快排 partition(arr,i+1,right) } }else{ //若是數組長度小於閾值,採用插入排序 insertSort(left,right) } }
快速排序的完整代碼
this.quickSort = function(){ var self = this var cutoff = 3 function partition(arr,left,right){ //分割操做 var pivot = findPivot(arr,left,right) var length = right - left if (length>cutoff) { var i = left var j = right - 2 while(i<=j){ while(arr[i]<pivot){ i++ } while(arr[j]>pivot){ j-- } if (i<=j) { self.swapItemInArray(i,j) i++ j-- } } self.swapItemInArray(i,right-1) if (left<i-1) { partition(arr,left,i-1) } if (i+1<right) { partition(arr,i+1,right) } }else{ insertSort(left,right) } } function findPivot(arr,left,right){//主元取中位數 var center = Math.floor((left+right)/2) if (arr[left] > arr[center]) { self.swapItemInArray(left,center) } if (arr[left] > arr[right]) { self.swapItemInArray(left,right) } if (arr[center] > arr[right]) { self.swapItemInArray(center,right) } self.swapItemInArray(center,right-1)//主元被藏在倒數第二個 return arr[right-1] } function insertSort(left,right){ //當分塊足夠小的時候,用插入排序 var len = right - left for (var i = 0; i <= len; i++) { for (var j = i; j > 0; j--) { if (array[j]<array[j-1]) { self.swapItemInArray(j,j-1) } } } } partition(array,0,array.length-1) }
與快速排序的「在劃分中排序」不一樣,歸併排序的基本思想是先將長度爲N的數組劃分爲N個長度爲1的數組,而後兩兩合併,在合併的時候排序。
如何在兩個子數組歸併的時候排序
以下圖,對於A數組和B數組,設置指針Aptr和指針Bptr,它們的初始位置都在倆數組的首部。
將Aptr和Bptr所指的數對比,將小的數放到C數組中。
好比Aptr所指「1」,Bptr所指「2」,Aptr所指的「1」小,則將「1」放入到C中,Aptr後移,Bptr不動。
再對比Aptr所指的「13」和Bptr所指的「2」,「2」較小,將其推入到C中,Bptr右移,Aptr不動。
反覆重複上述操做,若是最後一個數組A空,另外一個數組B還有剩餘元素,則依次將數組B的剩餘元素所有放到C中。
至此完成依次歸併操做。
代碼實現爲
function merge(arr1,arr2){ var i = 0 var j = 0 var tempArr = [] while(i<arr1.length && j<arr2.length){ if(arr1[i]<=arr2[j]){ tempArr.push(arr1[i]) i++ }else{ tempArr.push(arr2[j]) j++ } } while(i<arr1.length){ //若是arr1還有剩餘元素,則所有放到tempArr中 tempArr.push(arr1[i]) i++ } while(j<arr2.length){ //若是arr2還有剩餘元素,則所有放到tempArr中 tempArr.push(arr2[j]) j++ } return tempArr }
歸併排序的完整代碼以下,咱們這裏採用遞歸來實現劃分
this.mergeSort = function(){ function merge(arr1,arr2){ var i = 0 var j = 0 var tempArr = [] while(i<arr1.length && j<arr2.length){ if(arr1[i]<=arr2[j]){ tempArr.push(arr1[i]) i++ }else{ tempArr.push(arr2[j]) j++ } } while(i<arr1.length){ tempArr.push(arr1[i]) i++ } while(j<arr2.length){ tempArr.push(arr2[j]) j++ } return tempArr } function sliceArr(array){ var len = array.length if (len === 1) { return array } var middle = Math.floor(len/2) var left = array.slice(0,middle) var right = array.slice(middle,len) return merge(sliceArr(left),sliceArr(right)) } array = sliceArr(array) }
最大堆是什麼
最大堆是一個徹底二叉樹。
但它還知足,任意一個節點,它的值大於左子樹中的任意元素的值,也大於右子樹中的任意元素值。
該節點的左子樹元素的值和右子樹元素的值的大小沒有要求。
堆的數組表示
當咱們用數組(從0開始)來表示一個堆的時候,第i個元素的左子元素爲第(2*i+1)個元素,右子元素爲第(2i*2)個元素。
堆排序的大體思想
將數組按最大堆的方式排列,好比排列爲[a,b,c,d]
將一個最大堆的根(a)和最後一個元素(d)交換,
把數組中除了最後一個數(a)之外的元素[d,b,c]從新調整爲最大堆。
對[d,b,c]重複上述操做
如何建立最大堆
把全部元素插入到數組array(設長度爲N)中後,從索引爲(Math.floor(N/2) - 1)的元素開始,依次向前地將它和它的子樹調整爲最大堆。
以下圖,先將子樹①調整爲最大堆 ----> 調整子樹②爲最大堆 ----> 調整整個樹③爲最大堆
最大堆建立的代碼以下
function createMaxHeap(){ //建立最大堆 var len = array.length var startIndex = Math.floor(len/2) - 1 //從這個節點開始,將其子樹調整爲最大堆 for (var i = startIndex; i >= 0; i--) { compareChildAndAdjust(i) } } function compareChildAndAdjust(i,lastIndex){ var bigChildIndex = findBigInChildren(i,lastIndex) if (bigChildIndex==false) { //當找到的子節點返回爲false時,表示沒有子節點應當結束 return } var parent = array[i] var bigChild = array[bigChildIndex] if (parent >= bigChild) { return }else{ self.swapItemInArray(i,bigChildIndex) compareChildAndAdjust(bigChildIndex,lastIndex)//調整後要對子樹調整 } } function findBigInChildren(i,lastIndex){ var leftChild = array[2*i+1] //i節點的左子節點 var rightChild = array[2*i+2] //i節點的右子節點 if (lastIndex) { if (2*i+1 >= lastIndex) { return false } if (!(2*i+1 >= lastIndex) && (2*i+2 >= lastIndex)) { return 2*i+1 } } if (!leftChild) { return false } if ( leftChild && !rightChild) { return 2*i+1 } if (leftChild>rightChild) { return 2*i+1 }else{ return 2*i+2 } }
堆排序的完整代碼以下
this.heapSort = function(){ var self = this createMaxHeap() swapMaxWithLast() function swapMaxWithLast(){ var lastIndex = array.length - 1 for (var i = lastIndex; i > 0; i--) { self.swapItemInArray(0,i) //將根節點與最後一個節點交換 //從根節點開始,與其子節點比較並從新造成最大堆 //傳入的第二個參數表示,向下比較的時候,比到第i個節點以前停下來 compareChildAndAdjust(0,i) } } function createMaxHeap(){ //建立最大堆 var len = array.length var startIndex = Math.floor(len/2) - 1 //從這個節點開始,將其子樹調整爲最大堆 for (var i = startIndex; i >= 0; i--) { compareChildAndAdjust(i) } } function compareChildAndAdjust(i,lastIndex){ var bigChildIndex = findBigInChildren(i,lastIndex) if (bigChildIndex==false) { //當找到的子節點返回爲false時,表示沒有子節點應當結束 return } var parent = array[i] var bigChild = array[bigChildIndex] if (parent >= bigChild) { return }else{ self.swapItemInArray(i,bigChildIndex) compareChildAndAdjust(bigChildIndex,lastIndex)//調整後要對子樹調整 } } function findBigInChildren(i,lastIndex){ var leftChild = array[2*i+1] //i節點的左子節點 var rightChild = array[2*i+2] //i節點的右子節點 if (lastIndex) { if (2*i+1 >= lastIndex) { return false } if (!(2*i+1 >= lastIndex) && (2*i+2 >= lastIndex)) { return 2*i+1 } } if (!leftChild) { return false } if ( leftChild && !rightChild) { return 2*i+1 } if (leftChild>rightChild) { return 2*i+1 }else{ return 2*i+2 } } }
首先用一個函數來隨機生成10萬個數
function createNonSortedArray(size){ var array = new ArrayList() for (var i = size; i > 0; i--) { let num = Math.floor(1 + Math.random()*99) array.insert(num) } return array } var arr = createNonSortedArray(100000) //console.log(arr.toString()) //打印查看生成結果
接下來採用以下函數來計算排序時間
var start = (new Date).getTime() //在這裏調用arr的各類排序方法 //如 arr.quickSort() var end = (new Date).getTime() console.log(end-start) //打印查看生成結果 //console.log(arr.toString()) //打印查看排序結果
數據結果以下
冒泡排序耗時26000ms左右
選擇排序耗時5800ms左右
插入排序耗時10600ms左右
歸併排序耗時80-100ms
快速排序
cutoff==5--->30-50ms
cutoff==10 --->30-60ms
cutoff==50 ---->40-50ms
cutoff==3效果不錯--->30-50ms,30ms出現的機會不少
cutoff==0時(即不在分割長度短的時候轉爲插入排序),效果依然不錯,30-50ms,30ms出現的不少
堆排序耗時120-140ms
JavaScript提供的原生排序耗時55-70ms
結論
快速排序效率最高,cutoff取3效果最好(沒有懸念)
原生排序居然是第二快的排序算法!諸位同窗參加筆試的時候,在沒有指明必需要用哪一種排序算法的狀況下,若是須要排個序,仍是用原生的yourArr.sort(function(a,b){return a-b})吧,畢竟不易錯還特別快!
若是想了解數據結構和排序算法的基礎理論知識,推薦中國大學mooc浙江大學陳越老師主講的《數據結構》。該課程採用C語言講解,但仍然能夠系統地學習到數據結構的實現思路。
你要是以爲本文文字描述難以理解,去聽或看該課程的動態圖片講解應該會豁然開朗。
《數據結構》(第2版),陳越,高等教育出版社《學習JavaScript數據結構與算法》[巴西]Loiane Groner,中國工信出版集團,人民郵電出版社