算法学习知识结构
算法复杂度 complexity - 时间复杂度、空间复杂度
时间复杂度
- 关注点在循环次数最多的代码块
- 最大值原则 - 存在多个循环,总复杂度等于最大的代码块复杂度
- 乘法原则 - 嵌套代码复杂度等于嵌套内外代码块复杂度的乘积
function total(n) {
let sum = 0; // 执行一行代码需要 t 时间
for (let i = 0; i < n; i++) { // nt
sum += i; // nt
}
return sum; // t
}
// 执行了 t + nt + nt + t = 2(n + 1)t 长时间
function total(n) {
let sum = 0; // 执行一行代码需要 t 时间
for (let i = 0; i < n; i++) { // nt
for (let j = 0; j < n; j++) { // n * nt
sum = sum + i + j; // n * nt
}
}
return sum; // t
}
// 执行了 t + nt + n * nt + n * nt + t = (2n^2 + n + 2)t
// 当 n => 无穷大时, 2(n + 1)t => O(n);
// (2n^2 + n + 2)t => O(n^2)
常见的复杂度还有: 常数阶 O(1) 对数阶 O(logN)
复杂度用例:
- 常数阶
const sum_plus = function () {
let i = 1;
let j = 2;
++i;
j++;
return i + j;
}
// O(1)
- 线性阶
const foo2 = function(n) {
for (let i = 1; i <= n; i++) {
let j = i;
j++;
}
}
// O(n)
- 对数阶
const foo3 = function (n) {
let i = 1;
while (i < n) {
i = i * 2
}
}
// i 的等比变化 2^n
// 2 的 x 次方 等于n, 那么 x = log2^n
// 循环log2^n 次之后,该段代码就结束了
// O(logN)
- 线性对数阶
const foo4 = function (n) {
for (let m = 1; m < n; m++) {
let i = 1;
while(i < n) {
i = i * 2;
}
}
}
// O(nlogN)
- 平方阶
function total(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
sum = sum + i + j;
}
}
return sum;
}
// O(n^2)
时间复杂度大小对比
O(1) 常数阶 < O(logN) 对数阶 < O(n) 线性阶 < O(nlogN) 线性对数阶 < O(n^2) 平方阶
空间复杂度 - 与时间相对
主要看存储
- 常量
let j = 0;
for (let i = 0 i < n; i++) {
j++;
}
// O(1)
- 线性增长
let j = [];
for (let i = 0; i < n; i ++) {
j.push(i);
}
// O(n)
... 指数log, 嵌套
基础算法
Divide & Conquer
工作原理:(如何确定case适用分治)
- 可以明确设定一条基线
- 根据此基线可以不停将问题进行分解,直到所有内容符合基线标准
常见:快排、分班
数组中一个明确的数字作为一个基线
const quickSort = function(arr) {
// 4. 校验
if(arr.length <=1) {
return arr;
}
// 1. 找到基线/中间值, 并对基线左右进行声明
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0]
let left = [];
let right = [];
// 2. 遍历当前内容,按照基线去划分左右
for (let i = 0; i < arr.length) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 3. 递归处理,不断根据基线生成新内容,并进行连接
return quickSort(left).concat([pivot], quickSort(right));
}
时间复杂度 介于 O(n) 到 O(logN)
Greedy - 利益最大化
如何在有限的时间内,听到最多的课
7:00 开始js原理,下课立马上算法,紧接着再上设计模式
贪婪本质:利益最大化,始终之查找到最大的项目,尽可能快满足需求
场景:给定一个整数数组inputArr,找到一个具有最大和的连续子数组(子数组必须包含一个元素),返回其最大和
何时适用贪婪:需要查找最大项目等类型,同时满足利益最大化
const maxSubArray = function(inputArr) {
// 传入值判断
if (numbs.length <= 1) return inputArr;
let rtnArr = inputArr[0];
let sum = 0;
for (const num of inputArr) {
// 最快扩充当前数据量or最短途径满足要求
if (sum > 0) {
sum += num;
} else {
sum = num;
}
rtnArr = Math.max(rtnArr, sum)
}
return rtnArr;
}
动态规划
第一人:拿到300元走人 第二人:首先看到300 -> 200 -> 150 拿到150之后发现也可拿200,最终拿到350元
何时适用东归:将待求解的问题分解成若干子问题;子问题之间互相有联系
场景:有序数组、杨辉三角、斐波那契数列
斐波那契数列: F(0) = 0, F(1) = 1 F(n) = F(n - 1) + F(n - 2) // Fn 为最后两项之和,其中 n > 1
[0,1,1,2,3,5,8,13,21,34,...]
const fib = function(n) {
// 传入校验
if (n < 2) {
return n;
}
}
// 1. 确定分界
let pre = 0;
let next = 0;
let res = 1;
// 2. 遍历所有内容进行运算执行
for (let i = 2; i <= n; i++) {
// 3. 所有内容项目进行关联与隔离
pre = next;
next = res;
res = pre + next;
return res;
}
高阶问题:git diff 如何做处理?
图 与 图算法
- 构成: 边集合(偶数集合,一个定点两个边) + 顶点集合
- 分类:有向图、无向图、构造图(复合引用)
复合索引:
假设1,4,6 三个点放在一个数组里,对应[0] [1] [2],可以通过索引找到1,4,6;可以通过1 找到 2,3;通过4找到5
面试题:如何实现一个图类
class Graph {
constructor(v) {
this.vertices = v; // 确定顶点数
this.edges = 0; // 边集合数
this.arr = [];
// 初始化描述数组 - 多少个顶点就有多少元素进行连接
// 遍历所有内容,初始化
for (let i = 0; this.vertices; i++) {
this.arr[i] = [];
}
}
// 图操作: 边操作 + 绘图
addEdge(v, w) {
this.arr[v].push(w); // 点连边
this.arr[w].push(v); // 边连点
this.edges++;
}
showGraph() {
for (let i = 0; i < this.vertices; i ++) {
let str = i + '->';
for (let j = 0; j < this.vertices; j++) {
if (this.arr[i][j] !== undefined) {
str += this.arr[i][j];
}
}
}
console.log(str);
}
}
何时使用图例来解决问题 - 路径类问题、查找类问题
图解决深度优先问题
起始点开始查找,直到最末的顶点,再返回追溯,直到没有路径为止
// 扩充 Graph 类
class Graph {
constructor () {
//...
this.marked = [];
for (let i = 0; i < this.vertices; i++) {
this.marked[i] = false; // 没有访问过
}
}
dfs(v) {
this.marked[v] = true;
if (this.arr[v] !== undefined) {
console.log('visited' + v);
}
this.arr[v].forEach(w => {
if (!this.marked[v]) {
this.dfs(w);
}
})
}
}
广度优先,相邻节点优先
class Graph {
bfs(s) {
let queue = [];
this.marked[s] = true;
queue.push(s);
while(queue.length > 0) {
let v = queue.shift();
if (v !== undefined) {
console.log('visited' + v);
}
this.arr[v].forEach(w => {
if (!this.marked[w]) {
queue.push(w);
this.marked[w] = true;
}
})
}
}
}
面试题:最短路径解决办法
利用广度优先天然临近查找的优势
- 需要一个数组用来保存所有执行的路径
- 除了标记节点是否被访问过之外,添加一条边来描述顶点到当前顶点的路径
constructor() {
//..
this.edgeTo = [];
}
bfs(s) {
let queue = [];
this.marked[s] = true;
queue.push(s);
while(queue.length > 0) {
let v = queue.shift();
if (v !== undefined) {
console.log('visited' + v);
}
this.arr[v].forEach(w => {
if (!this.marked[w]) {
queue.push(w);
this.marked[w] = true;
// 做一个连接顶点记录
this.edgeTo[w] = v;
}
})
}
}
function pathTo (t, v) {
let source = t;
for (let i = 0; i < this.vertices; i++) {
this.marked[i] = false;
}
this.bfs(source);
if (!this.marked[v]) {
return undefined;
}
let path = [];
for (let i = v; i !== source; i = this.edgeTo[i]) {
path.unshift(i);
}
path.unshift(source);
let str = '';
for (let i in path) {
if (i < path.length - 1) {
str += path[i] + '->'
} else {
str += path[i];
}
}
console.log(str);
return path;
}
实例化
let g = new Graph(5)
// 添加索引和边
g.addEdge(0, 1);
g.addEdge(1, 3);
g.addEdge(0, 4);
g.pathTo(0, 4);