前端面試題自檢 算法 設計模式 操做系統部分

說在前面

限於這是個自查手冊,回答不那麼太詳細,若是某個知識點下面有連接,本身又沒有深刻了解過的,理應點擊連接或自行搜索深刻了解。javascript

另外兩個部分 :html

基本算法

Javascript 中的數據結構和算法學習node

排序

穩定性,關鍵字相同的元素,排序後保持着排序前的先後位置面試

選擇排序

時間複雜度最穩定的算法,固定 O(n²) ,具備穩定性算法

  • 爲某一位找到它後面最小的數,而後交換;從前日後循環。

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     //尋找最小的數
                minIndex = j;                 //將最小數的索引保存
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}
複製代碼

插入排序

O(n^2),具備穩定性設計模式

  • 在前面已完成排序的有序序列中不斷向前對比並交換位置,直到找到位置 ;從前日後循環

function insertionSort(arr) {
    var len = arr.length;
    var preIndex, current;
    for (var i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex+1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex+1] = current;
    }
    return arr;
}
複製代碼

歸併排序

O(nlog(n)) ,不具備穩定性數組

首先須要實現merge函數,以後將數組拆分紅兩半,分治;最後再merge回來;瀏覽器

function MergeSort(arr) {
  const len = arr.length
  if (len <= 1) return arr
  const middle = Math.floor(len / 2)
  const left = MergeSort(arr.slice(0, middle))
  const right = MergeSort(arr.slice(middle, len))
  return merge(left, right)
  // 核心函數
  function merge(left, right) {
    let l = 0 
    let r = 0
    let result = []
    while (l < left.length && r < right.length) {
      if (left[l] < right[r]) {
        result.push(left[l])
        l++
      } else {
        result.push(right[r])
        r++
      }
    }
    result = result.concat(left.slice(l, left.length))
    result = result.concat(right.slice(r, right.length))
    return result
  }
}
複製代碼

快速排序

O(nlog(n)),不具備穩定性

函數式的寫法很簡單,先取出第一個數,先用 fliter分紅小於區和大於區,再對兩個區分治;最後合成一個數組

兩次filter遍歷了兩次數組,能夠用for循環遍歷一次來代替

function QuickSort(arr) {
  if (arr.length <= 1) return arr
  const flag = arr.shift()
  const left = QuickSort(arr.filter(num => num <= flag))
  const right = QuickSort(arr.filter(num => num > flag))
  return [...left, flag, ...right]
}
複製代碼

遞歸遍歷

前中後序遍歷

前序節點第一次通過時輸出

中序節點第二次通過時輸出

後序節點第三次通過時輸出

// 前序遍歷
function ProOrderTraverse(biTree) {
    if (biTree == null) return;
    console.log(biTree.data);
    ProOrderTraverse(biTree.lChild);
    ProOrderTraverse(biTree.rChild);
}

// 中序遍歷
function InOrderTraverse(biTree) {
    if (biTree == null) return;
    InOrderTraverse(biTree.lChild);
    console.log(biTree.data);
    InOrderTraverse(biTree.rChild);
}
 
// 後序遍歷
function PostOrderTraverse(biTree) {
    if (biTree == null) return;
    PostOrderTraverse(biTree.lChild);
    PostOrderTraverse(biTree.rChild);
    console.log(biTree.data);
}


複製代碼

非遞歸遍歷(迭代遍歷)

深度優先遍歷

迭代法須要掌握 前序 和 中序 兩種;通常遍歷用到前序,二叉搜索樹用到中序

leetcode 144. 二叉樹的前序遍歷

  • 前序遍歷就是在出棧時加入子樹,而後循環;注意是先入棧右子樹再入棧左子樹;

leetcode 94. 二叉樹的中序遍歷

  • 中序遍歷就是將左側連成一條線上的節點前後入棧,出棧後入棧右節點;須要用 p 指針保存遍歷的位置

    以下圖:首先入棧 A-H,出棧 H、D,D 有右節點,入棧 I ,出棧 I,出棧 B,B 有右節點 E,入棧 E-N ;如此循環...

// 深度優先非遞歸
// 前序遍歷
function DepthFirstSearch(biTree) {
    let stack = [biTree];
    while (stack.length != 0) {
        let node = stack.pop();
        console.log(node.data);
        // 注意rChild先 才能保證先訪問到左子樹
        if (node.right) stack.push(node.right);
        if (node.left) stack.push(node.left);

    }

}
// 中序遍歷
function DepthFirstInorderSearch(biTree){
    let stack = [biTree]
    let p = biTree
    while(stack.length != 0){
        // 某一節點的左方先全入棧
        while(p.left){
            stack.push(p.left)
            p = p.left
		}
		p = stack.pop()
		console.log(p.data)
		if(p.right){
            stack.push(p)
            p = p.right
        }
    }
}

// 後序遍歷 https://leetcode-cn.com/problems/binary-tree-postorder-traversal/solution/
var postorderTraversal = function(root) {
    let res = [];
    if(!root) {
        return res;
    }
    let stack = [];
    let cur = root;
    do {
        if(cur) {
            stack.push([cur, true]);
            cur = cur.left;
        } else {
            cur = stack[stack.length-1][0];
            if(!cur.right || !stack[stack.length-1][1]) {
                res.push(stack.pop()[0].val);
                cur = null;
            } else {
                stack[stack.length-1][1] = false;
                cur = stack[stack.length-1][0].right;
            }
        }
    }while(stack.length);

    return res;
};
複製代碼

廣度優先遍歷(層序遍歷)

//廣度優先非遞歸 與上面前序迭代遍歷結構相同
function BreadthFirstSearch(biTree) {
    let queue = [];
    queue.push(biTree);
    while (queue.length != 0) {
        let node = queue.shift();
        console.log(node.data);
        if (node.left) {
            queue.push(node.left);
        }
        if (node.right) {
            queue.push(node.right);
        }
    }
}
複製代碼

兩種優先遍歷分析

兩種非遞歸遍歷的結構都是相同的,差異主要是一點

  • 深度優先(前序)是棧(先入後出),廣度優先是隊列(先入先出)
  • 由於深度優先是棧,因此在push左右子樹時,應該先push右子樹再左子樹,才能保證從左到右的順序

生成

層序生成樹

class Node { // 定義節點
    constructor(data){
        this.data = data
        this.left = null
        this.right = null
    }
}
// 層序遍歷結果的數組,生成層序遍歷樹
// 輸入例 ['a','b','d',null,null,'e',null,null,'c',null,null] # 是 null
// a
// / \
// b d
// / \ / \
// # # e #
// / \
// # c
// / \
// # #
function CreateTree(arr) {
  let i = 0
  const head = new Node(arr[i++])
  let queue = [head]
  let next
  while (queue.length) {
    let node = queue.shift()
    next = arr[i++]
    if (!(next == null)) queue.push((node.left = new Node(next)))
    next = arr[i++]
    if (!(next == null)) queue.push((node.right = new Node(next)))
  }
  return head
}
// 或者用 for of 能夠模擬隊列
function CreateTree(arr) {
  let i = 0
  const head = new Node(arr[i++])
  let queue = [head]
  let next
  for (let node of queue) {
    next = arr[i++]
    if (!(next == null)) queue.push((node.left = new Node(next)))
    next = arr[i++]
    if (!(next == null)) queue.push((node.right = new Node(next)))
  }
  return head
}
複製代碼

二叉樹搜索樹 BST(Binary Search Tree)

一文完全掌握二叉查找樹,(多組動圖)史上最全總結

定義

  • 左子樹不爲空時,左子樹上全部節點的值都小於根節點
  • 右子樹不爲空時,右子樹上全部節點的值都大於根節點
  • 左右子樹也是二叉搜索樹
  • 沒有重複的節點

其餘特色

  • 中序遍歷輸出的是有序序列

    能夠用於輸出第 K 大/小 的節點

  • 最左邊的節點的值是最小的,最右邊的節點是值最大的

搜索

若是樹是空的,則查找未命中,返回 null ;

若是被查找的鍵和根節點的鍵相等,查找命中,返回根節點對應的值;

若是被查找的鍵較小,則在左子樹中繼續查找,若是被查找的鍵較大,則在右子樹中繼續查找。

性能:最好 O(logn) 一顆平衡二叉樹 最差O(n) 退化成鏈表

// 遞歸法
function search(head, data){
    //在以x爲根結點的子樹中查找並返回鍵key所對應的節點
    //若是找不到,就返回null
    if(head == null) return null;
    if(data < head.data) return search(head.left, data);
    else if(data > head.data) return search(head.right, data);
    else return head;
}
// 迭代法
function search(head, data) {
  while (head) {
    if (data < head.data) head = head.left
    else if (data > head.data) head = head.right
    else return head
  }
  return null
}
複製代碼

插入

  • 插入是做爲葉子節點插入
  • 須要先查找後再插入
function insert(head, data) {
  if (head === null) return new Node(data)
  while (true) {
    if (data < head.data) {
      if (head.left) head = head.left
      else return (head.left = new Node(data))
    } else if (data > head.data) {
      if (head.right) head = head.rightleft
      else return (head.right = new Node(data))
    } else return false
  }
}
複製代碼

二叉查找樹的這樣插入算法必定能保證正確性嗎?是的,它這樣插入必定符合定義;可是一組無序的數據,它能夠有n多種二叉搜索樹的形式,其中有一種二叉搜索樹,它的左子樹和右子樹高度差不超過1,能夠給查找帶來最好的性能,這就是平衡二叉樹,平衡二叉樹能夠經過普通的二叉查找樹調整獲得。

徹底二叉樹

徹底二叉樹有以下性質:

  • 結點的編號對應該結點在數組的下標

  • 父節點的下標爲 i,則左子節點下標爲 2i + 1,右子節點下標爲 2i + 2

    具體視根節點位置而定,根節點序號爲 0 時,左子節點下標爲 2i + 1,右子節點下標爲 2i + 2

    根節點序號爲 1 時,左子節點下標爲 2i,右子節點下標爲 2i + 1

由於有如上性質,因此徹底二叉樹通常用數組儲存。

堆是一顆徹底二叉樹。

定義

以小頂堆爲例(大頂堆將小於換成大於)

  • 根節點小於或者等於左子樹和右子樹上的全部結點
  • 若是子樹存在,那麼它也是堆

做用:

  • 堆排序

    在原數組上進行,大頂堆能夠轉化爲升序序列;小頂堆能夠轉化爲降序序列;

    能夠得到第 K 大/小 的元素

堆排序

堆排序一共有三個步驟

​ 1. 建堆

​ 2. 將堆頂元素取出,並將堆底元素放置堆頂

​ 由於堆頂的元素是當前全部元素中最大/最小的,因此按序取出就是有序序列

​ 3. 篩選(即重建堆)

重複 2 和 3步驟

篩選

篩選:當堆的根元素髮生了改變,重建堆的過程

或者說知足如下條件才能進行篩選

  • 左右子樹都是堆,且高度差距不大於1

建堆也須要篩選,篩選步驟貫穿整個堆排序,因此它是堆的重中之重;

具體步驟以下

  • 找出左右子節點值最大的節點,與根節點比較,若是值大於根節點,那麼將它們值進行交換,以後指針向下移至交換過的子節點;一直循環下去
  • 若是某一節點子節點均不大於根節點或爲葉子節點,說明當前樹符合堆的規範,直接返回;

分析:由於子樹必定是堆,因此發生了交換後,又回到了剛剛的問題:堆的根元素髮生改變,重建堆,一直到不交換或葉子結點爲止

以下圖,根節點變爲 6,與左子節點進行交換,而後指針往左子節點移

// 父子節點交換
function swap(arr, i, j) {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}
// 篩選
function shiftDown(A, i, length) {
  let temp = A[i] // 當前父節點
  // j<length 的目的是對結點 i 如下的結點所有作順序調整
  for (let j = 2 * i + 1; j < length; j = 2 * i + 1) {
    temp = A[i] // 將 A[i] 取出,整個過程至關於找到 A[i] 應處於的位置
    if (j + 1 < length && A[j] < A[j + 1]) {
      j++ // 找到兩個孩子中較大的一個,再與父節點比較
    }
    if (temp < A[j]) {
      swap(A, i, j) // 若是父節點小於子節點:交換;不然跳出
      i = j // 交換後,temp 的下標變爲 j
    } else {
      break
    }
  }
}
複製代碼

創建

咱們輸入一個無序的數組,如何把它創建成堆?只須要從最後一個節點開始,到第一個節點,每一個進行篩選,就能夠構成一個堆;實際上,建堆是從最後一個非葉子節點開始,由於葉子節點已是堆了,不須要篩選。

分析:假若有某非葉子節點 n,左右子節點 2n + 1,2n + 2,由於 2n + 1 和 2n + 2 先前已經作過篩選,它們已是堆了,問題就轉化成:當堆的根元素髮生了改變,重建堆的過程,這裏的根指的是 n;一直篩選到根節點最後整個數組就是一個堆了

function createHeap(A) {
  // 從最後一個非葉子節點開始 即 Math.floor(A.length / 2 - 1)
  for (let i = Math.floor(A.length / 2 - 1); i >= 0; i--) {
    shiftDown(A, i, A.length)
  }
}
複製代碼

排序

排序原理咱們已經知道了:建堆,取最大,重建堆

// 堆排序
function heapSort(A) {
  // 初始化大頂堆,從第一個非葉子結點開始
  createHeap(A)
  // 排序,每一次for循環找出一個當前最大值,數組長度減一
  for(let i = Math.floor(A.length-1); i>0; i--) {
    swap(A, 0, i); // 根節點與最後一個節點交換
    shiftDown(A, 0, i); // 從根節點開始調整
  }
}
複製代碼

其餘

二分搜索

邏輯很簡單,判斷數組[中間下標]與搜索值比較大小後,左邊或右邊下標移到中間下標,注意點是邊界處理問題

// 邏輯很簡單,主要是邊界處理的問題
function binarySearch(arr, num) {
  let left = 0
  let right = arr.length - 1
  let middle
  while (right - left > 1) {
    middle = Math.floor((left + right) / 2)
    const now = arr[middle]
    if (num === now) return middle
    if (num > now) left = middle
    else right = middle
  }
  if (arr[right] === num) return right
  if (arr[left] === num) return left
  return -1
}

// precision是返回根平方與num的差的絕對值小於這個精度
function sqrtInt(num, precision) {
  precision = 1 / 10 ** precision // 轉換爲末尾 如 0.1
  let left = 1
  let right = (1 + num) / 2
  while (true) {
    const middle = (left + right) / 2
    const middleSquare = middle ** 2
    const diff = middleSquare - num
    if (Math.abs(diff) < precision) return middle
    if (diff > 0) right = middle
    else left = middle
  }
}
複製代碼

設計模式

前端須要瞭解的9種設計模式

一. 結構型模式(Structural Patterns)

經過識別系統中組件間的簡單關係來簡化系統的設計。

適配器模式

適配器用來解決兩個已有接口之間不匹配的問題,它並不須要考慮接口是如何實現,也不用考慮未來該如何修改;適配器不須要修改已有接口,就可使他們協同工做;

外觀模式

外觀模式是最多見的設計模式之一,它爲子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統1、更簡潔、更易用的API。不少咱們經常使用的框架和庫基本都遵循了外觀設計模式,好比JQuery就把複雜的原生DOM操做進行了抽象和封裝,並消除了瀏覽器之間的兼容問題,從而提供了一個更高級更易用的版本。其實在平時工做中咱們也會常常用到外觀模式進行開發,只是咱們不自知而已。

好比,咱們能夠應用外觀模式封裝一個統一的DOM元素事件綁定/取消方法,用於兼容不一樣版本的瀏覽器和更方便的調用:

// 綁定事件
function addEvent(element, event, handler) {
  if (element.addEventListener) {
    element.addEventListener(event, handler, false);
  } else if (element.attachEvent) {
    element.attachEvent('on' + event, handler);
  } else {
    element['on' + event] = fn;
  }
}

// 取消綁定
function removeEvent(element, event, handler) {
  if (element.removeEventListener) {
    element.removeEventListener(event, handler, false);
  } else if (element.detachEvent) {
    element.detachEvent('on' + event, handler);
  } else {
    element['on' + event] = null;
  }
}
複製代碼

代理模式

代理模式能夠解決如下的問題:

  1. 增長對一個對象的訪問控制
  2. 當訪問一個對象的過程當中須要增長額外的邏輯

要實現代理模式須要三部分:

  1. Real Subject:真實對象
  2. Proxy:代理對象
  3. Subject接口:Real Subject 和 Proxy都須要實現的接口,這樣Proxy才能被當成Real Subject的「替身」使用

建立型模式(Creational Patterns)

處理對象的建立,根據實際狀況使用合適的方式建立對象。常規的對象建立方式可能會致使設計上的問題,或增長設計的複雜度。建立型模式經過以某種方式控制對象的建立來解決問題。

工廠模式

將構造函數進行二次封裝

單例模式

顧名思義,單例模式中Class的實例個數最多爲1。當須要一個對象去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此以外的場景儘可能避免單例模式的使用,由於單例模式會引入全局狀態,而一個健康的系統應該避免引入過多的全局狀態。

行爲型模式(Behavioral Patterns)

用於識別對象之間常見的交互模式並加以實現,如此,增長了這些交互的靈活性。

觀察者模式

觀察者模式又稱發佈訂閱模式(Publish/Subscribe Pattern),是咱們常常接觸到的設計模式,平常生活中的應用也比比皆是,好比你訂閱了某個博主的頻道,當有內容更新時會收到推送;又好比JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察對象(subject)維護一組觀察者(observer),當被觀察對象狀態改變時,經過調用觀察者的某個方法將這些變化通知到觀察者

策略模式

img

策略模式簡單描述就是:對象有某個行爲,可是在不一樣的場景中,該行爲有不一樣的實現算法。好比每一個人都要「交我的所得稅」,可是「在美國交我的所得稅」和「在中國交我的所得稅」就有不一樣的算稅方法。最多見的使用策略模式的場景如登陸鑑權,鑑權算法取決於用戶的登陸方式是手機、郵箱或者第三方的微信登陸等等,並且登陸方式也只有在運行時才能獲取,獲取到登陸方式後再動態的配置鑑權策略。全部這些策略應該實現統一的接口,或者說有統一的行爲模式。Node 生態裏著名的鑑權庫 Passport.js API的設計就應用了策略模式。

仍是以登陸鑑權的例子咱們仿照 passport.js 的思路經過代碼來理解策略模式:

將if-else中的邏輯抽離成不一樣方法,更關注不一樣方法內的邏輯且方便切換。

/** * 登陸控制器 */
function LoginController() {
  this.strategy = undefined;
  this.setStrategy = function (strategy) {
    this.strategy = strategy;
    this.login = this.strategy.login;
  }
}

/** * 用戶名、密碼登陸策略 */
function LocalStragegy() {
  this.login = ({ username, password }) => {
    console.log(username, password);
    // authenticating with username and password... 
  }
}

/** * 手機號、驗證碼登陸策略 */
function PhoneStragety() {
  this.login = ({ phone, verifyCode }) => {
    console.log(phone, verifyCode);
    // authenticating with hone and verifyCode... 
  }
}

/** * 第三方社交登陸策略 */
function SocialStragety() {
  this.login = ({ id, secret }) => {
    console.log(id, secret);
    // authenticating with id and secret... 
  }
}

const loginController = new LoginController();

// 調用用戶名、密碼登陸接口,使用LocalStrategy
app.use('/login/local', function (req, res) {
  loginController.setStrategy(new LocalStragegy());
  loginController.login(req.body);
});

// 調用手機、驗證碼登陸接口,使用PhoneStrategy
app.use('/login/phone', function (req, res) {
  loginController.setStrategy(new PhoneStragety());
  loginController.login(req.body);
});

// 調用社交登陸接口,使用SocialStrategy
app.use('/login/social', function (req, res) {
  loginController.setStrategy(new SocialStragety());
  loginController.login(req.body);
});
複製代碼

從以上示例能夠得出使用策略模式有如下優點:

  1. 方便在運行時切換算法和策略
  2. 代碼更簡潔,避免使用大量的條件判斷
  3. 關注分離,每一個strategy類控制本身的算法邏輯,strategy和其使用者之間也相互獨立

操做系統

《王道操做系統》學習筆記總目錄+思惟導圖

進程相關

進程和線程的區別?

  • 進程是資源分配的最小單位,線程是CPU調度的最小單位
  • 線程在進程下行進,一個進程能夠包含多個線程
  • 不一樣進程間數據很難共享,同一進程下不一樣線程間數據很易共享
  • 線程上下文切換比進程上下文切換快的多
  • 進程要比線程消耗更多的計算機資源
  • 進程間不會相互影響,一個線程掛掉將致使整個進程掛掉

進程間通訊 IPC

進程間通訊的五種方式

IPC方式包括:管道、系統IPC(信號量、消息隊列、共享內存)和套接字(socket)。

死鎖

所謂死鎖,是指多個進程在運行過程當中因爭奪資源而形成的一種僵局,當進程處於這種僵持狀態時,若無外力做用,它們都將沒法再向前推動。

進程間互相佔用對方所需資源,等待對方釋放資源的僵持狀態;

避免死鎖-銀行家算法

避免系統進入不安全的狀態

  • 系統每種資源的總量
  • 系統每種資源已分配的總量
  • 系統每種資源未分配的總量

首先有一個請求分配的進程需求列表,它們有的已經被分配了部分資源;

選取一個剩餘資源知足所需資源的進程,進程釋放資源後系統將以前分配的資源和剛剛分配的資源一同回收(因此剩餘資源總量增長了,能夠知足更多進程了),再繼續選取一個能夠知足的進程,如此反覆,直到全部進程都被知足了

這一個知足進程的前後順序被稱爲 安全隊列 ,找出 安全隊列 能夠避免系統進入一個不安全的狀態

首先須要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源狀況。所以,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會致使死鎖。當進程請求一組資源時,假設贊成該請求,從而改變了系統的狀態,而後肯定其結果是否還處於安全狀態。若是是,贊成這個請求;若是不是,阻塞該進程知道贊成該請求後系統狀態仍然是安全的。

做業調度算法

先來先服務(First Come First Server)

按照做業到達的前後順序進行服務。缺點是對短做業不利

短做業優先(Short First Server)

最短的做業優先獲得服務。缺點是對長做業不利且可能產生飢餓

最短剩餘時間優先

短做業優先的 搶佔式 版本。

調度程序老是選擇剩餘運行時間最短的那個進程運行。當一個新做業到達時,其整個時間同當前進程的剩餘時間作比較。若是新的進程比當前運行進程須要更少的時間,當前進程就被掛起,而運行新的進程。

時間片輪轉法

輪流爲各個進程分配服務,讓每一個服務均可以獲得響應。它是一個搶佔式的算法

按照各進程到達就緒隊列的順序,輪流讓各個進程執行一個時間片(如100ms)。若進程未在一個時間片內執行完,則剝奪處理機,將進程從新放到就緒隊列隊尾從新排隊。

內存置換算法

最佳置換算法

先進先出

最近最久未使用置換算法(LRU)

磁盤臂調度算法

先來先服務

最短尋找時間優先算法

電梯算法

相關文章
相關標籤/搜索