栈 Stack
- 后进先出
- 后进先出的数据结构 入 push () 出 pop()
- 需要后进先出的场景: 1、十进制转二进制 2、有效括号 3、函数调用堆栈
队列 queue
- 先进先出, 保证有序
- 后进先出的数据结构 入 push () 出 shift()
- 需要先进先出的场景 1、食堂打饭 2、js异步中的任务队列(js是单线程) 3、计算最近请求次数
事件循环与任务队列
代码刚执行时有一个主事件放到任务队列中,然后JS引擎会去任务队列中拿到这个事件去执行,执行到setTimeOut时发现这是一个异步任务,就交给WebAPI来执行这个异步任务,它继续执行后面的代码。setTimeout 0秒后执行完,接着把执行完后的回调函数放到任务队列中,但是任务队列中还有一个事件没执行完,即主事件。主事件执行完了才轮到回调函数执行。
环形链表
- point: 快慢指针
原型链
- 如果A沿着原型链能找到B.prototype, 那么A instanceof B为true.
- 如果在A对象上没有找到X属性,那么会沿着原型链找X属性。
- question:
- instanceOf的原理,并用代码实现 如果A沿着原型链可以找到B的原型对象,那么A instanceOf B为true, 反之为false
const instanceOf = (A, B) => { let p = A; while(p){ if(p === B.prototype){ return true; } p = p.__proto__; } return false; }
链表总结
- 链表里的元素存储不是连续的,之间通过next连接。
- Javascript中没有链表,但可以用Object模拟链表
- 链表常用操作: 修改next, 遍历链表
- JS中的原型链也是一个链表
- 使用链表指针可以获取JSON的节点值
集合 (无序且唯一)
- 数组集合互转
- 集合转数组 a.
b.const arr = [...set];const arr = Array.from(set);- 数组转集合
const set = new Set(arr); - 求数组交集
[...new Set(arr)].filter(x => arr2.includes(x));
字典
- has(), set(), get(), delete()
深度优先遍历
- 算法(递归): 1、访问根节点 2、对根节点的children挨个进行深度优先遍历
- 代码:
function dfs(root){ console.log(root.val); root.children.forEach(dfs); };
广度优先遍历
- 算法: 1、新建一个队列,把根节点入队 2、把队头出队并访问 3、把队头的children挨个入队 4、重复第二、三步,直到队列为空
- 代码:
function bfs(root){ const q = [root]; while(q.length > 0){ const h = q.shift(); console.log(h.val); h.children.forEach(x => { q.push(x); }); } };
先序遍历算法
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
- 非递归版代码:
const preorder = (root) => { if(!root) {return;} const stack = [root]; while(stack.length){ const n = stack.pop(); console.log(n); if(n.right) stack.push(n.right); if(n.left) stack.push(n.left); } };
中序遍历算法
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
- 非递归版代码:
const inorder = (root) => { if(!root) { return; } const stack = []; let p = root; while(stack.length || p){ while(p) { stack.push(p); p = p.left; } const n = stack.pop(); console.log(n.val); p = n.right; } }
后序遍历算法
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
- 非递归版代码:
const postorder = (root) => { if(!root) {return;} const outputStack = []; const stack = [root]; while(stack.length) { const n = stack.pop(); outputStack.push(n); if(n.left) stack.push(n.left); if(n.right) stack.push(n.right); } while(outputStack.length){ const n = outputStack.pop(); console.log(n.val); } };
遍历JSON的所有节点值
const json = {
a: {b: { c : 1}},
d: [1, 2],
}
// concat用于连接多个数组
// Object.keys(obj)会返回一个由传入对象的可枚举属性组成的数组,
数组名的排列顺序和使用for...in...循环遍历该对象返回的顺序一致。
const dfs = (n) => {
console.log(n);
Object.keys(n).forEach(k => {
dfs(n[k], path.concat(k));
});
};
dfs(json, []);
图的深度优先遍历
- 访问根节点
- 对根节点的没访问过的相邻节点挨个进行深度优先遍历
- 代码实现:
const visited = new Set(); const dfs = (n) => { console.log(n); visited.add(n); graph[n].forEach(c => { if(!visited.has(c)){ dfs(c); } }); };
图的广度优先遍历
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的没访问过的相邻节点入队
- 重复第二、三步,直到队列为空
- 代码实现:
const visited = new Set(); visited.add(2); const q = [2]; while(q.length){ const n = q.shift(); console.log(n); graph[n].forEach(c => { if(!visited.has(c)){ q.push(c); visited.add(c); } }); }
堆
- 代码实现:
class MinHeap { constructor(){ this.heap = []; } swap(i1, i2){ const temp = this.heap[i1]; this.heap[i1] = this.heap[i2]; this.heap[i2] = temp; } getParentIndex(i){ return Math.floor((i-1)/2); } getLeftIndex(i){ return (i*2 + 1); } getRightIndex(i){ return (i*2 + 2); } shiftUp(index){ if(index == 0) { return; } const parentIndex = this.getParentIndex(index); if(this.heap[this.parentIndex] > this.heap[index]){ this.swap(parentIndex, index); this.shiftUp(parentIndex); } } shiftDown(index){ const leftIndex = this.getLeftIndex(index); const rightIndex = this.getRightIndex(index); if(this.heap[leftIndex] < this.heap[index]){ this.swap(leftIndex, index); this.shiftDown(leftIndex); } if(this.heap[rightIndex] < this.heap[index]){ this.swap(rightIndex, index); this.shiftDown(rightIndex); } } insert(value){ this.heap.push(value); this.shiftUp(this.heap.length-1); } pop(){ this.heap[0] = this.heap.pop(); this.shiftDown(0); } peek(){ return this.heap[0]; } size(){ return this.heap.length; } }
选择排序
- 代码实现:
Array.prototype.selectionSort = function(){ for(let i = 0; i< this.length - 1; i++){ let indexMin = i; for(let j = i; j < this.length; j++){ if(this[j] < this[indexMin]){ indexMin = j; } } if(!indexMin == i){ let temp = this[i]; this[i] = this[indexMin]; this[indexMin] = temp; } } }
插入排序
-
算法思路:
1、从第二个数开始往前比
2、比它大就往后排
3、以此类推到最后一个数
-
代码实现:
Array.prototype.insertSort = function(){ for(let i = 0; i< this.length; i++){ let temp = this[i]; let j = i; while(j > 0){ if(this[j - 1] > temp){ this[j] = this[j-1]; } else { break; } j--; } this[j] = temp; } }
归并排序
-
算法思路:
1、把数组分成两半,再递归的对子数组进行‘分’操作,直到分成一个个单独的数。
2、把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组。
-
代码实现:
function merge(arr){ if(arr.length ==1) return arr; let mid = Math.floor(arr.length/2); let left = arr.slice(0, mid); let right = arr.slice(mid, arr.length); let res = []; let leftArr = merge(left); let rightArr = merge(right); while(leftArr.length || rightArr.length){ if(leftArr.length && rightArr.length){ if(leftArr[0] < rightArr[0]){ res.push(leftArr.shift()); } else if (leftArr[0] > rightArr[0]){ res.push(rightArr.shift()); } } else if(leftArr.length){ res.push(leftArr.shift()); }else if(rightArr.length){ res.push(rightArr.shift()); } } return res; }
快速排序
-
算法思路
1、分区:从数组中任意选择一个基准,所有比基准小的元素放在基准的前面,比基准大的元素放在基准的后面。
2、递归:递归地对基准前后的子数组进行分区
-
代码实现:
Array.prototype.quickSort = function(){ const rec = (arr) => { if(arr.length === 1) return res; let left = []; let right = []; let mid = arr[0]; for(let i =1; i< arr.length; i++){ if(arr[i] > mid){ right.push(arr[i]); }else { left.push(arr[i]); } } return [...rec(left), mid, ...rec(right)]; }; const res = rec(this); res.forEach((n,i) => {this[i] = n}); };
二分搜索
-
算法思路
1、从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。
2、如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。
-
代码实现:
Array.prototype.binarySearch = function(item){ let low = 0; let high = this.length - 1; while(low <= high){ let mid = Math.floor((low + high) /2); let elem = this[mid]; if(elem < item){ low = mid + 1; } else if( elem > item){ high = mid - 1; } else{ return mid; } } return -1; };
分治
- 将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
- 应用场景: 归并排序、快速排序、二分搜索、翻转二叉树...
动态规划
- 将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。
- 与分治的区别:看他们的子问题是否是独立的。如果重叠,则为动态规划;如果独立,则为分治。
- 应用场景:斐波那契数列
- 步骤: 1、定义子问题 2、反复执行
贪心算法
- 期盼通过每个阶段的局部最优选择,从而达到全局的最优
- 结果并不一定是最优
回溯算法
- 回溯算法是一种渐进式寻找并构建问题解决方式的策略
- 回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。
- 全排列:时间复杂度:o(n!) 空间复杂度:因为使用了递归,本质是堆栈,为线性增长,所以为o(n)
- 子集:时间复杂度:o(2^n), 每个元素两种情况(存在或不存在) 空间复杂度:o(n)