算法學習筆記:排序算法(二)

上一篇中已經介紹了幾個簡單的排序算法,這一篇文章我將繼續向你們介紹排序算法相關的內容,本篇的會介紹希爾排序、快速排序、歸併排序以及分治算法的思想,但願經過本文章可以加深你們對排序算法的理解。算法

希爾排序

希爾排序又叫縮小增量排序,希爾排序的主要思想是使數組中任意相隔h的元素都是有序的,h就是希爾增量,實現的希爾排序的方法就是:對相隔h的元素進行分組,對每組進行使用插入排序,使子序列變成有序序列,增量逐漸遞減一直到1爲止,在例子中我將h增量設爲array.length/2,遞減的過程就是不斷除以2,是否是被個人解釋弄的有點暈,讓咱們先來看一個演示過程理解一下:shell

clipboard.png

如圖所示,一共15個元素,增量就是15/2爲7,第一輪的分組即爲[2, 26, 48],[44, 26],[38, 2],[5, 46],[47, 4],[15, 19],[36, 50],而後進行插入排序,獲得新的序列[ 3, 27, 2, 5, 4, 15, 36, 26, 44, 38, 46, 47, 19, 50, 48 ],而後在進行分組,增量爲7/2爲3:數組

clipboard.png

第二輪分組[3, 5, 36, 2, 19],[44, 47, 26, 46, 50], [38, 15, 27, 4, 48],而後進行插入排序,獲得序列[ 3, 4, 2, 5, 26, 15, 19, 27, 44, 36, 46, 47, 38, 50, 48 ],而後再進行分組,增量爲3/2爲1,再插入排序獲得的就是一個有序序列了,最好讓咱們來看具體的代碼實現:spa

function shellSort(arr) {
  var n = arr.length
  for (var gap = parseInt(n/2); gap > 0; gap=parseInt(gap/2)) {
    for (var i=gap; i<arr.length; i++) {
      var j = i - gap, temp = arr[i]
      while (arr[j] > temp) {
        arr[j+gap] = arr[j]
        j = j - gap
      }
      arr[j+gap] = temp
    }
  }
  console.log(arr)
}
shellSort([3,44,38,5,47,15,36,26,27,2,46,4,19,50,48])

其實希爾排序也不難,只要按照上面的分解圖示一步一步的理解去編寫,相信你們都能寫得出來,上面這種形式的增量設置就是二分的形式設置,而後插入排序,還有一種希爾排序的寫法就是自定義增量,而後子序列裏的元素兩兩比較,來看具體代碼:設計

function shellSort(arr) {
  var n = arr.length, gap = 1
  while (gap < n / 3) {
    gap = gap * 3 + 1
  }
  for (gap; gap > 0; gap=parseInt(gap/3)) {
    for (var i=gap; i<arr.length; i++) {
      for (var j = i - gap;j >= 0; j-=gap) {
        if (arr[j] > arr[j+gap]) {
          var temp = arr[j+gap]
          arr[j+gap] = arr[j]
          arr[j] = temp
        }
      }
    }
  }
  console.log(arr)
}
shellSort([3,44,38,5,47,15,36,26,27,2,46,4,19,50,48])

希爾排序更高效的緣由在於它權衡了子數組的規模和有序性,各個子數組都很短,排序以後的子數組都是部分有序的,所以在希爾排序的效率要比插入排序高。code

分治法

在介紹歸併排序和快速排序有必要先介紹一下分治法相關的內容,爲何呢?由於歸併排序和快速排序都是分治法的典型應用。
分治法的設計思路就是:將一個大問題分解成若干個規模嬌小的相同子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解決這些子問題,而後將各個子問題的解合併獲得原問題的解
分治法所能解決的問題通常有以下幾個特色:blog

  1. 把該問題縮小到必定規模就能夠容易解決
  2. 該問題能夠被分解爲若干個規模較小的相同問題
  3. 利用該問題的子問題的解向上合併能夠獲得該問題的解
  4. 該問題分解出的子問題是相互獨立的

歸併排序

歸併排序就是分治法的典型應用之一,歸併排序的實現有兩種,一種是自頂向下的歸併排序,另外一種就是自底向上的歸併排序。排序

自頂向下的歸併排序

向來看第一種,自頂向下的歸併排序的實現思路是不斷二分,而後對二分後的最小序列分別進行排序後,再將排序後的結果向上合併獲得最終的有序數組,讓咱們先經過一個樹結構來理解歸併排序的過程:
圖片描述遞歸

從圖中能夠看到將一個數組0-14的元素不斷二分,分到最後一層,而後互相比較,獲得新的有序序列,而後向上合併,在進行比較,不斷反覆,合併出最終的有序序列,這就是自頂向下的歸併排序的思路,經過這個思路你是否能本身寫出排序的方法呢?索引

好了,接下來就讓咱們看看具體的代碼實現:

function sort(arr) {
  var n = arr.length
  if (n < 2) return arr
  var mid = Math.ceil(n / 2)
  var left = arr.slice(0, mid)
  var right = arr.slice(mid)
  return merge(sort(left), sort(right));
}

function merge(left, right){
  var result = []
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      result.push(left.shift())
    } else {
      result.push(right.shift())
    }
  }
  while (left.length) {
    result.push(left.shift())
  }
  while (right.length) {
    result.push(right.shift())
  }
  return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(sort(arr))

代碼實現是否是很容易理解呢?相信你們通過仔細的思考事後都能看得懂,爲了方便更好的理解,來看一下動圖的排序過程:
圖片描述

自底向上的歸併排序

自底向上的歸併排序思路是將長度爲n的數組當作n個長度爲1的數組,而後兩兩向上歸併排序,獲得新的數組,不斷向上歸併排序,直到獲得長度爲n的數組,這樣的排序比以前自頂向下的排序代碼要少,下面來看具體的代碼實現:

function merge(arr, start, mid, end){
  var i = start, j= mid + 1, copy = []
  for (var k = start; k <= end; k++) {
    copy[k] = arr[k]
  }
  for (var k = start; k <= end; k++) {
    if (i > mid) { // 左邊取完取右邊的元素
      arr[k] = copy[j]
      j++
    } else if (j > end) { // 右邊取完取左邊的元素
      arr[k] = copy[i]
      i++
    } else if (copy[j] < copy[i]) { // 右邊的元素小於左邊的元素,取右邊的元素
      arr[k] = copy[j]
      j++ 
    } else { // 左邊的元素小於右邊的元素,取左邊的元素
      arr[k] = copy[i]
      i++
    }
  }
  console.log(arr)
}

function sort(arr) {
  var n = arr.length
  for (var i = 1; i < n; i = i + i) { // i子數組的大小
    for (var j = 0; j < n - i; j = j + i + i) { // j數組的索引
      var start = j
      var end = Math.min(j + i + i - 1, n-1)
      var mid = parseInt((start + end) / 2)
      merge(arr, start, mid, end)
    }
  }
}

var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
sort(arr)

爲了方便理解,已經在代碼中加了必要的註釋,可能這段代碼比以前的要難理解一些,須要你們耐心多花一些時間去理解

快速排序

快速排序也是一種分治的排序算法,因爲它實現簡單而且效率比通常的排序算法高,所以,它的應用範圍很是普遍,接下來讓咱們來看快速排序的排序過程:

clipboard.png

將數組的第一個元素作爲基準,從數組末尾開始找比基準小的數,找到就停下來,記下索引j,而後從基準右邊開始找比基準大的數找到停下來,記下索引i,而後交換i和j上的元素,獲得數組:

clipboard.png

而後繼續從44的位置開始找比基準3小的一直移動到2,此時索引i和索引j相等,將基準3和i、j對應的值交換位置獲得:

clipboard.png

此時基準數3前面的元素都是比它小的數,後面元素都是比它大的數,而後從基準數先後拆成兩個數組,在遞歸執行前面一樣的操做。
看來上面的排序過程,你是否是有代碼的實現了呢?能夠先試着寫一下實現的代碼,這樣更容易理解,接下來就讓我來看一下具體代碼:

function sort(arr, left, right) {
  var temp = arr[left],i = left, j = right, t;
  if (left < right) {
    while (i != j) {
      while (arr[j] >= temp && i < j) {
        j--
      }
      while (arr[i] <= temp && i < j) {
        i++
      }
      if (i < j) {
        t = arr[i]
        arr[i] = arr[j]
        arr[j] = t
      }
    }
    arr[left] = arr[i]
    arr[i] = temp
    sort(arr, left, i - 1)
    sort(arr, i + 1, right)
  }
  return arr
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(sort(arr, 0, arr.length - 1))

看了代碼以後是否是以爲並不難呢?這種快速排序的實現其實有一個問題不知道你們注意到沒有,當數組中有多個重複元素的時候,重複的元素只要排了一個就不須要重複排了,可是這中快速排序的實現並無考慮這種狀況,即使重複的元素仍是會執行排序,所以有人提出了更優的快速排序方法「三向切分的快速排序」,讓咱們先來看代碼實現有什麼不一樣:

function sort(arr, left, right) {
  var temp = arr[left],i = left, j = right,k = left + 1, t;
  if (left < right) {
    while (k <= j) {
      if (arr[k] < temp) {
        t = arr[k]
        arr[k] = arr[i]
        arr[i] = t
        i++;
        k++;
      } else if (arr[k] > temp) {
        t = arr[k]
        arr[k] = arr[j]
        arr[j] = t
        j--;
      } else {
        k++;
      }
    }
    sort(arr, left, i - 1)
    sort(arr, j + 1, right)
  }
  return arr
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48,44,38];
console.log(sort(arr, 0, arr.length - 1))

整體思路和以前的同樣保證基準值前面的比它小後面的比它大,惟一不一樣的地方就是用了一個臨時下標k去做比較,把小的丟到基準值前面,大的值就和末尾的值交換,獲得新值再與基準值比較,當與基準值相等的時候,就只須要將臨時下標的值+1而不須要排序了

總結

這篇文章詳細介紹了希爾排序、歸併排序、快速排序這三種排序的思想和實現方式,但願你們看完以後都能本身去多實踐,多思考,算法仍是須要本身多動手,不然光看做用不大。若是有錯誤或不嚴謹的地方,歡迎批評指正,若是喜歡,歡迎點贊收藏

相關文章
相關標籤/搜索