JavaScript各類排序算法的實現及其速度性能分析

今天咱們來討論的問題有兩個:面試

  1. 如何用JavaScript實現選擇排序、冒泡排序、插入排序、快速排序、歸併排序、堆排序;算法

  2. 對生成的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,中國工信出版集團,人民郵電出版社

相關文章
相關標籤/搜索