限於這是個自查手冊,回答不那麼太詳細,若是某個知識點下面有連接,本身又沒有深刻了解過的,理應點擊連接或自行搜索深刻了解。javascript
另外兩個部分 :html
穩定性,關鍵字相同的元素,排序後保持着排序前的先後位置面試
時間複雜度最穩定的算法,固定 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);
}
複製代碼
迭代法須要掌握 前序 和 中序 兩種;通常遍歷用到前序,二叉搜索樹用到中序
中序遍歷就是將左側連成一條線上的節點前後入棧,出棧後入棧右節點;須要用 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);
}
}
}
複製代碼
兩種非遞歸遍歷的結構都是相同的,差異主要是一點
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
}
複製代碼
其餘特色
中序遍歷輸出的是有序序列
能夠用於輸出第 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步驟
篩選:當堆的根元素髮生了改變,重建堆的過程
或者說知足如下條件才能進行篩選
建堆也須要篩選,篩選步驟貫穿整個堆排序,因此它是堆的重中之重;
具體步驟以下
分析:由於子樹必定是堆,因此發生了交換後,又回到了剛剛的問題:堆的根元素髮生改變,重建堆,一直到不交換或葉子結點爲止
以下圖,根節點變爲 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
}
}
複製代碼
經過識別系統中組件間的簡單關係來簡化系統的設計。
適配器用來解決兩個已有接口之間不匹配的問題,它並不須要考慮接口是如何實現,也不用考慮未來該如何修改;適配器不須要修改已有接口,就可使他們協同工做;
外觀模式是最多見的設計模式之一,它爲子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統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;
}
}
複製代碼
代理模式能夠解決如下的問題:
要實現代理模式須要三部分:
Real Subject
:真實對象Proxy
:代理對象Subject
接口:Real Subject 和 Proxy都須要實現的接口,這樣Proxy才能被當成Real Subject的「替身」使用處理對象的建立,根據實際狀況使用合適的方式建立對象。常規的對象建立方式可能會致使設計上的問題,或增長設計的複雜度。建立型模式經過以某種方式控制對象的建立來解決問題。
將構造函數進行二次封裝
顧名思義,單例模式中Class的實例個數最多爲1。當須要一個對象去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此以外的場景儘可能避免單例模式的使用,由於單例模式會引入全局狀態,而一個健康的系統應該避免引入過多的全局狀態。
用於識別對象之間常見的交互模式並加以實現,如此,增長了這些交互的靈活性。
觀察者模式又稱發佈訂閱模式(Publish/Subscribe Pattern),是咱們常常接觸到的設計模式,平常生活中的應用也比比皆是,好比你訂閱了某個博主的頻道,當有內容更新時會收到推送;又好比JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察對象(subject)維護一組觀察者(observer),當被觀察對象狀態改變時,經過調用觀察者的某個方法將這些變化通知到觀察者。
策略模式簡單描述就是:對象有某個行爲,可是在不一樣的場景中,該行爲有不一樣的實現算法。好比每一個人都要「交我的所得稅」,可是「在美國交我的所得稅」和「在中國交我的所得稅」就有不一樣的算稅方法。最多見的使用策略模式的場景如登陸鑑權,鑑權算法取決於用戶的登陸方式是手機、郵箱或者第三方的微信登陸等等,並且登陸方式也只有在運行時才能獲取,獲取到登陸方式後再動態的配置鑑權策略。全部這些策略應該實現統一的接口,或者說有統一的行爲模式。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);
});
複製代碼
從以上示例能夠得出使用策略模式有如下優點:
IPC方式包括:管道、系統IPC(信號量、消息隊列、共享內存)和套接字(socket)。
所謂死鎖,是指多個進程在運行過程當中因爭奪資源而形成的一種僵局,當進程處於這種僵持狀態時,若無外力做用,它們都將沒法再向前推動。
進程間互相佔用對方所需資源,等待對方釋放資源的僵持狀態;
避免系統進入不安全的狀態
首先有一個請求分配的進程需求列表,它們有的已經被分配了部分資源;
選取一個剩餘資源知足所需資源的進程,進程釋放資源後系統將以前分配的資源和剛剛分配的資源一同回收(因此剩餘資源總量增長了,能夠知足更多進程了),再繼續選取一個能夠知足的進程,如此反覆,直到全部進程都被知足了
這一個知足進程的前後順序被稱爲 安全隊列 ,找出 安全隊列 能夠避免系統進入一個不安全的狀態
首先須要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源狀況。所以,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會致使死鎖。當進程請求一組資源時,假設贊成該請求,從而改變了系統的狀態,而後肯定其結果是否還處於安全狀態。若是是,贊成這個請求;若是不是,阻塞該進程知道贊成該請求後系統狀態仍然是安全的。
按照做業到達的前後順序進行服務。缺點是對短做業不利
最短的做業優先獲得服務。缺點是對長做業不利且可能產生飢餓
短做業優先的 搶佔式 版本。
調度程序老是選擇剩餘運行時間最短的那個進程運行。當一個新做業到達時,其整個時間同當前進程的剩餘時間作比較。若是新的進程比當前運行進程須要更少的時間,當前進程就被掛起,而運行新的進程。
輪流爲各個進程分配服務,讓每一個服務均可以獲得響應。它是一個搶佔式的算法
按照各進程到達就緒隊列的順序,輪流讓各個進程執行一個時間片(如100ms)。若進程未在一個時間片內執行完,則剝奪處理機,將進程從新放到就緒隊列隊尾從新排隊。