1. 程式人生 > 實用技巧 >前端基礎--演算法

前端基礎--演算法

排序

js本身陣列的sort方法,可以滿足日常很多需求。基本會寫快速排序就夠了

基本排序演算法

基本排序的思想都很類似,基本都是一組巢狀的for迴圈,外迴圈便利陣列的每一項,內迴圈用於比較


1.氣泡排序

氣泡排序(Bubble Sort),是一種電腦科學領域的較簡單的排序演算法。 它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果順序(如從大到小、首字母從Z到A)錯誤就把他們交換過來。 走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素列已經排序完成。

  function bubbleSort(arr) {
    let len = arr.length;
    console.log(arr)
    
for (let outer = len; outer >= 2; outer--) { for (let inner = 0; inner < outer - 1; inner++) { if (arr[inner] > arr[inner + 1]) { // let tmp = arr[inner]; // arr[inner] = arr[inner + 1]; // arr[inner + 1] = tmp; [arr[inner], arr[inner + 1]] = [arr[inner + 1], arr[inner]] //
es6解構賦值 } } } console.log(arr) } let arrs = [1, 56, 85, 3, 7, 3, 8] bubleSort(arrs)

注意點:

1.外層迴圈,從最大值開始遞減,因為內層是兩兩比較,因此最外層 >=2 時即可停止;

2.內層是兩兩比較,從0開始,比較inner與inner+1,因此,臨界條件是 inner<outer-1

在比較交換的時候,就是計算機中最經典的交換策略,用臨時變數temp儲存值.ES6可以用解構賦值簡化。


2.選擇排序

選擇排序就是從陣列頭部開始,將第一個元素和其他元素比較,檢查完所有的元素後,最小的放在第一個位置,接下來再開始從第二個開始,重複以上一直到最後。

  // 選擇排序
  function selectSort(arr) {
    let len = arr.length;
    for (let i = 0; i < len - 1; i++) {
      for (let j = i + 1; j < len; j++) {
        if (arr[j] < arr[i]) {
          [arr[i], arr[j]] = [arr[j], arr[i]]
        }
      }
    }
    console.log(arr)
  }
  let arrs = [1, 56, 85, 100, 7, 3, 8]
  selectSort(arrs)

注意點:

1.外層迴圈的 i 表示第幾輪,arr[i]表示房錢輪次最靠前的位置

2.內層從 i 開始,j 是 i 的下一位數,一次往後對比,找到小的,放到 i 位置,直到最後,arr[len-1]

時間複雜度

基本排序演算法,基本思想就是兩層迴圈巢狀,第一遍元素O(n),第二遍找位置O(n)疊加就是O(n2)


3.高階排序

如果所有排序都像上面一樣,對大量資料的處理,將是災難性的。

快速排序【問的最多,快速排序】

快排是處理大資料最快的排序演算法之一。它是一種分而治之的演算法,通過遞迴的方式將資料依次分解為包含較小元素和較大元素的不同子序列。該演算法不斷重複這個步驟直至所有資料都是有序的。

簡單說: 找到一個數作為參考,比這個數字大的放在數字左邊,比它小的放在右邊; 然後分別再對左邊和右變的序列做相同的操作:

  1. 選擇一個基準元素,將列表分割成兩個子序列;
  2. 對列表重新排序,將所有小於基準值的元素放在基準值前面,所有大於基準值的元素放在基準值的後面;
  3. 分別對較小元素的子序列和較大元素的子序列重複步驟1和2

  // 快速排序
  function quickSort(arr) {
    if (arr.length <= 1) {
      return arr; // 遞迴出口
    }
    let left = [];
    let right = [];
    let current = arr.splice(0, 1);  // splice 後原arr會少一個,splice返回的是被刪除的新的陣列
    for (let i = 0; i < arr.length; i++) {  // 便利當前陣列
      if (arr[i] < current) {
        left.push(arr[i]) // 放在左邊
      } else {
        right.push(arr[i])  // 放在右邊
      }
    }
    return quickSort(left).concat(current, quickSort(right)); // 遞迴,合併
  }
  let arrs = [1, 56, 85, 100, 7, 3, 8]
  let sum = quickSort(arrs)

時間複雜度總結

是否穩定

如果不考慮穩定性,快排似乎是接近完美的一種方法,但可惜它是不穩定的。 那什麼是穩定性呢?

通俗的講有兩個相同的數A和B,在排序之前A在B的前面,而經過排序之後,B跑到了A的前面,對於這種情況的發生,我們管他叫做排序的不穩定性,而快速排序在對存在相同數進行排序時就有可能發生這種情況。

/*
比如對(5,3A,6,3B ) 進行排序,排序之前相同的數3A與3B,A在B的前面,經過排序之後會變成  
    (3B,3A,5,6)
所以說快速排序是一個不穩定的排序
/*

穩定性有什麼意義? 個人理解對於前端來說,比如我們熟知框架中的虛擬DOM的比較,我們對一個<ul>列表進行渲染,當資料改變後需要比較變化時,不穩定排序或操作將會使本身不需要變化的東西變化,導致重新渲染,帶來效能的損耗。

輔助記憶
  • 時間複雜度記憶
    • 冒泡、選擇、直接 排序需要兩個for迴圈,每次只關注一個元素,平均時間複雜度為O(n²)(一遍找元素O(n),一遍找位置O(n))
    • 快速、歸併、希爾、堆基於二分思想,log以2為底,平均時間複雜度為O(nlogn)(一遍找元素O(n),一遍找位置O(logn))
  • 穩定性記憶-“快希選堆”(快犧牲穩定性)

遞迴

遞迴,其實是自己呼叫自己 遞迴步驟:
  • 尋找出口:遞迴一定要有出口,鎖定出口,保證不會死迴圈
  • 遞迴條件:符合遞迴條件,自己呼叫自己
斐波拉契數列,每個語言講遞迴都會從這個開始。前端可以從深度克隆說起。 Deep clone:實現一個物件的深度克隆
  // Deep Clone
  function deepClone(origin, target) {
    var target = target || {};
    for (let key in origin) {
      if (origin.hasOwnProperty(key)) {    // hasOwnProperty() 方法會返回一個布林值,指示物件自身屬性中是否具有指定的屬性(也就是,是否有指定的鍵)。
        if (Array.isArray(origin[key])) { // 如果是陣列,且巢狀
          target[key] = [];
          deepClone(origin[key], target[key])  // 遞迴
        } else if (typeof origin[key] === 'object' && origin[key] !== null) { //判斷是物件巢狀物件的情況
          target[key] = {};
          deepClone(origin[key], target[key])  // 遞迴
        } else {
          target[key] = origin[key]   // 如果不是巢狀 ,就直接拷貝
        }
      }
    }
    return target
  }
  let a1 = { a: 1, b: 2, c: { c1: 4, c2: 8 } };
  let b1 = deepClone(a1);
  console.log(b1)

// 深淺拷貝還有參考:比如通常可以通過JSON.parse(JSON.stringify(object))來解決。更多詳情可去各大論壇。

上面的方法思路很清晰:

出口:遍歷結束後 return

遞迴條件:遇到引用值Array 或 Object

參考例題:

Q1:Array陣列方法的 flat 方法的實現,如:

Array
var arr1 = [1, 2, [3, 4]];
arr1.flat(); 
// [1, 2, 3, 4]

實現如下:

  Array.prototype.flat = function () {
    var arr = [];
    this.forEach((item, idx) => {
      if (Array.isArray(item)) {
        arr = arr.concat(item.flat()); // 遞迴去除陣列元素
      } else {
        arr.push(item)  // 非陣列直接push進去
      }
    })
    return arr;
  }

測試沒問題

參考評論區另類解法。。

arr.prototype.flat = function() {
    this.toString().split(',').map(item=> +item )
}

toString方法,連線陣列並返回一個字串'2,2,3,2,3,4'

split方法分割字串,變成陣列['2','2','3','2','3','4']

map方法,將string對映成為number型別2,2,3,2,3,4


Q3. 爬樓梯問題

有一樓梯共M級,剛開始時你在第一級,若每次只能跨上一級或二級,要走上第M級,共有多少種走法?

分析: 這個問題要倒過來看,要到達n級樓梯,只有兩種方式,從(n-1)級 或 (n-2)級到達的。所以可以用遞推的思想去想這題,假設有一個數組s[n], 那麼s[1] = 1(由於一開始就在第一級,只有一種方法), s[2] = 1(只能從s[1]上去 沒有其他方法)。

那麼就可以推出s[3] ~ s[n]了。

下面繼續模擬一下, s[3] = s[1] + s[2], 因為只能從第一級跨兩步, 或者第二級跨一步。

function cStairs(n) {
    if(n === 1 || n === 2) {
        return 1;
    } else {
        return cStairs(n-1) + cStairs(n-2)
    }
}

Q4.二分查詢

二分查詢,是在一個有序的序列裡查詢某一個值,與小時候玩的猜數字遊戲非常相啦:

A: 0 ~ 100 猜一個數字
B: 50
A: 大了
B: 25
A: 對頭,就是25

因此,思路也就非常清楚了,這裡有遞迴和非遞迴兩種寫法,先說下遞迴的方法吧:

  • 設定區間,low和high
  • 找出口: 找到target,返回target;
  • 否則尋找,當前次序沒有找到,把區間縮小後遞迴
function binaryFind(arr,target,low = 0,high = arr.length - 1) {
    const n = Math.floor((low+high) /2);
    const cur = arr[n];
    if(cur === target) {
        return `找到了${target},在第${n+1}個`;
    } else if(cur > target) {
        return binaryFind(arr,target,low, n-1);
    } else if (cur < target) {
        return binaryFind(arr,target,n+1,high);
    }
    return -1;
}

接下來,使用迴圈來做一下二分查詢,其實思路基本一致:

function binaryFind(arr, target) {
    var low = 0,
        high = arr.length - 1,
        mid;
    while (low <= high) {
        mid = Math.floor((low + high) / 2);
        if (target === arr[mid]) {
            return `找到了${target},在第${mid + 1}個`
        }
        if (target > arr[mid]) {
            low = mid + 1;
        } else if (target < arr[mid]) {
            high = mid - 1;
        }
    }
    return -1
}



參考連結:https://juejin.im/post/6844903656865677326