前端面试题自检 算法 设计模式 操做系统部分

说在前面

限于这是个自查手册,回答不那么太详细,若是某个知识点下面有连接,本身又没有深刻了解过的,理应点击连接或自行搜索深刻了解。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)

磁盘臂调度算法

先来先服务

最短寻找时间优先算法

电梯算法

相关文章
相关标签/搜索