限于这是个自查手册,回答不那么太详细,若是某个知识点下面有连接,本身又没有深刻了解过的,理应点击连接或自行搜索深刻了解。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)。若进程未在一个时间片内执行完,则剥夺处理机,将进程从新放到就绪队列队尾从新排队。