前端面試資料整理【演算法篇】

donggg 發表於 2022-04-24
前端 面試 演算法

排序與查詢

排序

參考

穩定排序:兩個相等的記錄,排序前 A,A',排序後仍然是 A,A'
不穩定排序:與上面結構相斥

10個常見的排序:

氣泡排序
穩定排序,O(n)

function bubble(array) {
    if (typeof array === 'object' && array.length) {
        for (let i = 0; i < array.length; i ++) {
            for (let j = i; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    let tmp = array[j]
                    array[j] = array[i + 1]
                    array[j+1] = tmp;
                }
            }
        }
    }
}

快排

  1. 最右邊為基值
  2. pos 記錄目標位置
  3. 逐個遍歷,當遇到索引為i的值小於基值時,發生 i 與 pos 為發生替換。
  4. 此時 pos 為基值的正確位置,以 pos 劃分,左邊是小於基值的,右邊是大於基值的。
function quick(array, left = 0, right = array.length - 1) {
  if (left < right) {
    // 標識基數正確的位置
    let pos = left - 1;
    // 基數
    const rightVal = array[right];

    for (let i = left; i <= right; i++) {
      if (array[i] <= rightVal) {
        pos++;

        // 如果位置合理,是自己替換自己,如果不位置不合理,pos記錄的是替換的位置
        [array[i], array[pos]] = [array[pos], array[i]]
      }
    }
    quick(array, left, pos -1)
    quick(array, pos + 1, right)
  }
  return array
}
quick([2, 1, 4, 5, 3])
PS.實現一個 quickSortByDESC

選擇排序
簡單,理解就是,找到後放到相應位置,然後再次重複

function select(array) {
  for (let i = 0; i < array.length; i++) {
    min = i;
    for (let j = i; j < array.length -j; j++) {
      if (array[min] > array[j]) {
        min = j;
      }
      [array[min], array[i]] = [array[i], array[min]];
    }
  }
}

插入排序

歸併排序
採用分治法

function mergeSort(array) {
  if (array.length < 2) {
    return array;
  }

  const mid = Math.floor(array.length / 2);
  const left = array.slice(0, mid);
  const right = array.slice(mid);

  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];
  
  while(left.length && right.length) {
    if (left[0] > right[0]) {
      result.push(right.shift());
    } else {
      result.push(left.shift());
    }
  }

  while(left.length) {
    result.push(left.shift());
  }

  while(right.length) {
    result.push(right.shift());
  }

  return result;
}

mergeSort([23, 5, 6, 1, 75, 7])

查詢

參考

7個常見的查詢:

1. 順序查詢

2. 二分查詢

function search(array, target) {
    if (!array.length) {
        return -1;
    }
    let left = 0;
    let right = array.length -1;


    while(left <= right) {
        let mid = Math.floor(((right - left) / 2) + left);

        if (array[mid] === target) {
            return mid;
        }
        if (array[mid] < target) {
            left = mid + 1
        } else if (array[mid] > target) {
            right = mid -1
        }
    }
    return -1;
}

3. 差值查詢
待補充

4. 斐波那契查詢
待補充

5. 樹表查詢
待補充

6. 分塊查詢
待補充

7. hash查詢
待補充

演算法題

陣列

陣列去重

// Set

// 暴力,for 雙迴圈

// 暴力,用 Array api 代替,[].indexOf,[].includes,[].fliter

// 排序 + 雙指標

// 轉化成物件,存在向字串轉化的問題。

陣列交集
待補充

平鋪陣列 flat

function flat (array) {
    // 健壯性判斷
    let r = []
    array.forEach((item) => {
        if (item.length){
            r = r.concat(flat(item))
        } else {
            r.push(item)        
        }
    }) 
    return r;

    // es6
    // return array.join().split(',')
    // 
    // array.reduce 也可以
}

console.log(flat([1, [2], [[3]], 4, [5]]))

合併兩個有序的陣列

給出兩個有序的整數陣列 A 和 B,將陣列 B 合併到 A,變成一個有序陣列。假設 A 陣列有足夠的空間存放 B 陣列元素,A 和 B中初始元素數分別為 m 和 n

function mergeArray(arrayA, arrayB) {
  // 健壯性檢查:資料型別,陣列長度
  const m = arrayA.length;
  const n = arrayB.length;
  let posA = 0;
  let posB = 0;
  let array = [];
  for (let i = 0; i < Math.max(m, n); i++) {
    if (arrayA[i] === undefined || arrayB[i] === undefined) {
      break;
    }
    if (arrayA[posA] >= arrayB[posB]) {
      array.push(arrayB[posB++])
      array.push(arrayA[posA++])
    } else {
      array.push(arrayA[posA++])
      array.push(arrayB[posB++])
    }
  }
  if (posB < n) {
    array = array.concat(arrayB.slice(posB, n))
  }
  if (posA < m) {
    array = array.concat(arrayA.slice(posA, m))
  }
  return array;
}
const A = [1, 3, 5, 7, 9];
const B = [2, 4]
mergeArray(A, B)

給出一組數字,給出所有數字的所有排列

例如輸入 [1, 2, 3],輸出 [[1, 2, 3] [1, 3, 2] [2, 1, 3] [2, 3, 1] [3, 2, 1] [3, 1, 2]] ,按數字在陣列中的位置為排列依據,按字典排序輸出
// 回溯
function permute(array) {
    const length = array.length;

    if (length === 0) {
        return array;
    }

    
    const result = []; // 結果陣列 [[], ...]
    const used = []; // 已經使用過的陣列
    const path = []; // 當前生成的組合
    dfs(array, length, 0, path, used, result)
    return result
}

/**
 * dfs 深度優先遍歷
 * @param  {array}  array   原始陣列
 * @param  {number} len     原始陣列長度,代表停止遍歷
 * @param  {number} depth   當前遍歷深度
 * @param  {array}  used 當前遍歷的結果
 * @param  {array}  result     儲存所有符合的結果
 */
function dfs(array, len, depth, path, used, result) {

    if (depth === len) {
        result.push([].concat(path))
        return;
    }
    
    for (let i = 0; i < len; i++) {
        if (!used[i]) {
            
            path.push(array[i]);
            used[i] = true;

            dfs(array, len, depth + 1, path, used, result)

            // 回溯
            used[i] = false;
            path.pop();
        }
    }
}

permute([1, 2, 3])

陣列之和為零

給你一個包含 n 個整數的陣列 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有和為 0 且不重複的三元組。

注意:答案中不可以包含重複的三元組。

字串

最長迴文子串【雙指標】

給你一個字串 s,找到 s 中最長的迴文子串。
輸入:s = "babad"
輸出:"bab"
解釋:"aba" 同樣是符合題意的答案。
// 暴力,時間複雜度  O(n^3) 空間複雜度 O(1)
function longestPalindrome(s) {
    if (s.length < 2) {
        return s;
    }

    let maxLength = 0;
    let begin = 0;
    for (let i = 0; i < s.length; i++) {
        for(let j = i; j < s.length; j++) {
            if (j - i + 1 > maxLength & validPalindromic(s, i, j)) {
                maxLength = j - i +1;
                begin = i
            } 
        }
    }
    return s.substring(begin, maxLength)
} 

// 驗證是否迴文
function validPalindromic(s, left, right) {
    while(left < right) {
        if (s[left++] != s[right--]) {
            return false;
        }
    }
    return true
}

longestPalindrome('babad') // bab
longestPalindrome('babaaaaa') // aa, 有問題,應該是 aaaa

// 中心擴充套件演算法,時間複雜度  O(n^2) 空間複雜度 O(1)
function longestPalindrome(s) {
    if (s.length < 2) {
        return s;
    }
    const len = s.length;
    let start = 0;
    let end = 0;

    for (let i = 0; i < s.length; i++) {
        // 奇數對稱
        let len1 = entendAroundCenter(s, i, i);
        // 偶數對稱
        let len2 = entendAroundCenter(s, i, i+1);
        let len = Math.max(len1, len2);
        // 獲取最長迴文長度,大於目前起始位,更新,i 是中心位
        if (len > end - start && len1 > len2) {
            // 奇數
            start = i - ((len - 1) / 2);
            end = i + ((len - 1) / 2);
        } else if (len > end - start && len2 > len1) {
            // 偶數
            start = i - (len / 2);
            end = i + (len / 2);
        }
    }
    return s.substring(start, end + 1) // end + 1 是因為要包含 end 處
} 

// 由中心向外擴充套件
function entendAroundCenter(s, left, right) {
    // 開始擴散
    while(left > 0 && right < s.length && s[left] === s[right]) {
        left--;
        right++;
    }
    // right 和 left 多擴了1位
    return (right - 1) - (left + 1) + 1; // 合併計算 right - left -1;
}

longestPalindrome('babaaaaa')

無重複字元的最長子串【雙指標】
滑動視窗,有重複的就左邊出,右邊進

var lengthOfLongestSubstring = function(str) {
 if (!str.length) return 0
    let tmpStr = ''   // 每次迴圈找到的不含重複字元的子字串
    let maxStrLen = 0   // 最大不含重複字元的子字串的長度
    let len = str.length   
    let left = 0  // 不含重複字元的子字串的左遊標
    for (let i = 0; i < len; i++) {
        if (tmpStr.indexOf(str[i]) !== -1) {
            left += (str.slice(left, i).indexOf(str[i]) + 1)
            continue
        }
        tmpStr = str.slice(left, i + 1)
        maxStrLen = Math.max(maxStrLen, tmpStr.length)
    }
    return maxStrLen
};

最長無重複子串

例如 abcabcbb 結果 abc

利用平移視窗

var lengthOfLongestSubstring = function(str) {
 if (!str.length) return 0
    let tmpStr = ''   // 每次迴圈找到的不含重複字元的子字串
    let maxStrLen = 0   // 最大不含重複字元的子字串的長度
    let len = str.length   
    let left = 0  // 不含重複字元的子字串的左遊標
    for (let i = 0; i < len; i++) {
        if (tmpStr.indexOf(str[i]) !== -1) {
            left += (str.slice(left, i).indexOf(str[i]) + 1)
            continue
        }
        tmpStr = str.slice(left, i + 1)
        maxStrLen = Math.max(maxStrLen, tmpStr.length)
    }
    return maxStrLen
};

動態規劃

全部規劃路徑數

僅返回數即可。

輸入m = 3, n = 2
輸出 3
var uniquePaths = function(m, n) {
    let dp = new Array(m)
    for(let i = 0; i < dp.length; i++) {
        dp[i] = new Array(n).fill(1)
    }

    for(let i = 1; i < dp.length; i++) {
        for(let j = 1; j < dp[0].length; j++) {
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        }
    }
    return dp[m - 1][n - 1]
};

uniquePaths(3, 2)

揹包問題本

假設我們有 5 個物品,重量分別為 2,2,4,6,3,揹包總容量為 9 Kg。

最短路徑

假設我們有一個 n*n 的矩陣,矩陣中每個元素都是正數。
var shortestPath = function(array) {
    const len = array[0].length;

    for (let i = 1; i < len; i++) {
        for(let j = 1; j < len; j++) {
            array[i][j] += Math.min(array[i - 1][j], array[i][j - 1])
        }

    }
    return array[len - 1][len - 1];
}

shortestPath(
    [
        [1, 3, 5, 9],
        [2, 1, 3, 4],
        [5, 2, 6, 7],
        [6, 8, 4, 3]
    ]
)

其他

菲波那切數列

要求輸入一個整數 n,輸出菲波那切數列數列的第 n 項, 第0項為 0 ,第1項為1。
// 最耗效能的遞迴寫法。
function fibonacci(n) {
    if (n < 2) {
        return n;
    }
    return fibonacci(n-1) + fibonacci(n-2)
}


function fibonacci(n) {
    if (n < 2) {
        return n;
    }
    let a = 0; // 初始0位
    let b = 1; // 初始1位
    let c = 0; // fibonacci 數
    for (let i = 2; i <= n; i++) {
        c = a + b;
        // 不能直接用i,會變成c = 2i - 3
        a = b;
        b = c;
    }
    return c;
}

反轉連結串列
參考

// 迭代法 空間複雜度:O(1) 時間複雜度:O(n)
function reverseList(head) {
  if (!head || !head.next) {
    return head;
  }

  let prev = null;
  let cur = head;
  while (cur) {
    const next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
  }
  return head;
}

// 尾遞迴 空間複雜度:O(n) 時間複雜度:O(n)
// 從頭節點開始,遞迴反轉它的每一個節點,直到 null
function reversList(head) {
  if (!head || !head.next) {
    return head;
  }
  head = reverse(null, head)
  return read;
}

function reverse(prev, cur) {
  if (!cur) return prev;
  const next = cur.next;
  cur.next = prev
  return reverse(cur, next);
}

// 遞迴
// 根據 head.next 推入棧中,重複:next = head.next, head = stach.pop(), next.next = head, head.next = null, 跳出 head.next === null
function reversList(head) {
  if (!head || !head.next) {
    return head;
  }
  const next = head.next;
  const head = reversList(next);
  
  next.next = head;
  next.next = null;

  return head;
}
遞迴的邏輯圖示
遞迴的邏輯圖示

並行請求

未完成,未測試

const f = (url) => new Promise((resolve) => {
  console.log('f:', url)
  setTimeout(() => resolve({ code: 200 }), 500)
})


// 併發控制
function createRequest(num){
  // 健壯性檢查
  const queue = [];
  let count = 0; // 正在執行的請求數

  const create = (url, opitons) => () =>
    f(url, opitons).then((res) => {
      count++;
      return res
    }).catch(() => {
      // 維護個錯誤佇列
    }).finally(() => {
      count--;
      if (queue.length) {
        queue.shift()()
      }
    })

  

  return function(url, options) {
    const stack = create(url, options)
    // 排隊
    if (count >= num) {
      queue.push(stack)
    } else {
      // 0, 1, 2
      return stack()
    }
  }
}

// ---->
// ---->
// ---->
//   ---->
//   ---->
const request = createRequest(3);


// 利用 webmessage 實現併發
request('/a').then((r) => console.log('res', r))
request('/b').then((r) => console.log('res', r))
request('/c').then((r) => console.log('res', r))
request('/e').then((r) => console.log('res', r))
request('/f').then((r) => console.log('res', r))

// -----

// 模擬 fetch
const f = (url) => new Promise((resolve) => {
    const time = Math.random() * 1000
    setTimeout(() => {
      console.log(url, time)
      resolve({ time })
    }, time)
  })

function createlimitRequest(limit) {
  const waitQueue = [];
  let requestintCount = 0;
  const r = (url) => {
    requestintCount++;
    return f(url)
      .then((res) => res).finally(() => {
        if (waitQueue.length) {
          requestintCount++;
          return waitQueue.shift();
        }
      })
  }
  return function (url) {
    if (requestintCount > limit) {
      waitQueue.push(r)
    }
    return r(url)
  } 
}

const request = createlimitRequest(4);

function requestAll(urls) {
  const results = []
  let count = 0;
  return new Promise((resolve) => {
    urls.forEach((url, index) => 
      request(url).then((res) => {
        results[index] = res
      }).catch((err) => {
        results[index] = err
      }).finally(() => {
        count++;
        if (count === urls.length) {
          resolve(results)
        }
      })
    )
  })
}


requestAll([
  '/a',
  '/b',
  '/c',
  '/d',
  '/e',
  '/f',
])

復原 IP 地址

給定一個只包含數字的字串,用以表示一個 IP 地址,返回所有可能從 s 獲得的 有效 IP 地址 。你可以按任何順序返回答案。

有效 IP 地址 正好由四個整數(每個整數位於 0 到 255 之間組成,且不能含有前導 0),整數之間用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "[email protected]" 是 無效 IP 地址。

輸入:s = "25525511135"
輸出:["255.255.11.135","255.255.111.35"]

待補充

買股票時機

假設你有個陣列,第 i 個元素是股票第 i 天的價格。你有一次買入和賣出的機會。(只有買入後才能賣出)。求最大收益。

待補充

相關文章