學寫提升性能的代碼之流程控制-4

這是我參與8月更文挑戰的第10天,活動詳情查看: 8月更文挑戰算法

前言

業務代碼開發多了,其實就是在寫 if-else數組

簡單來講,遞歸就是在函數執行體內部調用自身的行爲,這種方式有時可讓複雜的算法實現變得簡單,如計算斐波那契數或階乘。
但使用遞歸也有一些潛在的問題須要注意:好比缺乏或不明確遞歸的終止條件會很容易形成用戶界面的卡頓,同時因爲遞歸是一種經過空間換時間的算法,其執行過程當中會入棧保存大量的中間運算結果,它對內存的開銷將與遞歸次數成正比,因爲瀏覽器都會限制JavaScript的調用棧大小,超出限制遞歸執行便會失敗。瀏覽器


使用迭代

任何遞歸函數均可以改寫成迭代的循環形式,雖然循環會引入自身的一些性能問題,但相比於長時間執行的遞歸函數,其性能開銷仍是要小不少的。以歸併排序爲例:緩存

// 遞歸方式實現歸併排序
function merge(left, right){
  const result = []
  while(left.length > 0 && right.length > 0) {
    // 把最小的先取出來放到結果中
    if(left[0] < left[0]){
      result.push(left.shift())
    } else {
      result.push(right.shift())
    }
  }
  // 合併
  return result.concat(left).concat(right)
}

// 遞歸函數
function mergeSort(array){
  if(array.length == 1) return array
  // 計算數組中點
  const middle = Math.floor(array.length / 2)
  // 分割數組
  const left = array.slice(0, middle)
  const right = array.slice(middle)
  // 進行遞歸合併與排序
  return merge(mergeSort(left), mergeSort(right))
 }
複製代碼

能夠看出這段歸併排序中,mergeSort()函數會被頻繁調用,對於包含n個元素的數組來講,mergeSort()函數會被調用2n-1次,隨着所處理數組元素的增多,這對瀏覽器的調用棧是一個嚴峻的考驗。改成迭代方式以下:markdown

// 用迭代方式改寫遞歸函數
function mergeSort(array){
  if(array.length == 1 ) return array
  const len = array.length
  const work = []
  for(let i = 0 ; i < len; i ++){
    work.push([array[i]])
  }
  // 確保總數組長度爲偶數
  if(len & 1) work.push([])
  // 迭代兩兩歸併
  for(let lim = len; lim > 1; lim = (lim+1)/2) {
    for(let j = 0,k = 0; k < lim; j += 1, k += 2){
      work[j] = merge(work[k], work[k+1])
    }
    // 數組長度爲奇數時,補一個空數組
    if(lim & 1) work[j] = []
  }
  return work[0]
}
複製代碼

此處經過迭代實現的mergeSort()函數,其功能上與遞歸方式相同,雖然在執行時間閃過來看可能要慢一些,但它不會收到瀏覽器對JavaScript調用棧的限制。閉包

避免重複工做

若是在遞歸過程當中,前一次的計算結果能被後一次計算使用,那麼緩存前一次的計算結果就能有效避免許多重複工做,這樣就能帶來很好的性能提高。好比遞歸常常會處理的階乘操做以下:app

// 計算n的階乘
function factorial(n){
  if(n === 0) {
    return 1
  } else {
    return n * fatorial(n-1)
  }
}
複製代碼

當咱們要計算多個數的階乘(如二、三、4)時,若是分別計算這三個數的階乘,則函數factorial()總共要被調用12次,其中在計算4的階乘時,會把3的階乘從新計算一遍,計算3的階乘時又會把2的階乘從新計算一遍,能夠看出若是在計算4 的階乘以前,將3的階乘數緩存下來,那麼在計算4的階乘時,遞歸僅須要再執行一次。如此經過緩存階乘計算結果,避免多餘計算過程,本來12次的遞歸調用,能夠減小到5次。
根據這樣的訴求,提供一個有效利用緩存來減小沒必要要計算的解決方案:ide

// 利用緩存避免重複計算 
function memoize(func, cache){
  const cache = cache || {}
  return function(args){  
    if(!cache.hasOwnProperty(args)){
      cache[args]=func(args);  
    }
    return cache[args];  
  }
}
複製代碼

該方法利用函數閉包有效避免了相似計算屢次階乘時的重複操做,確保只有當一個計算在以前從未發生過期,才產生新的計算值,這樣前面的階乘函數即可改寫爲:函數

// 緩存結果的階乘方案
const memorizeFactorial = memorize(factorial, {'0': 1, '1': 1})
複製代碼

這種方式也存在性能問題,好比函數閉包延長了局部變量的存活期,若是數據量過大又不能有效回收,則容易致使內存溢出。這種方案也只有在程序中有相同參數屢次調用纔會比較省時,因此綜合而言,優化方案還需根據具體使用場景具體考慮。post

相關文章
相關標籤/搜索