算法2 - 04

133 阅读2分钟

算法学习知识结构

image.png

算法复杂度 complexity - 时间复杂度、空间复杂度

时间复杂度

  1. 关注点在循环次数最多的代码块
  2. 最大值原则 - 存在多个循环,总复杂度等于最大的代码块复杂度
  3. 乘法原则 - 嵌套代码复杂度等于嵌套内外代码块复杂度的乘积
  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)

复杂度用例:

  1. 常数阶
  const sum_plus = function () {
    let i = 1;
    let j = 2;

    ++i;
    j++;
    return i + j;
  }

  // O(1)
  1. 线性阶
  const foo2 = function(n) {
    for (let i = 1; i <= n; i++) {
      let j = i;
      j++;
    }
  }

  // O(n)
  1. 对数阶
  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)
  1. 线性对数阶
  const foo4 = function (n) {
    for (let m = 1; m < n; m++) {
      let i = 1;
      while(i < n) {
        i = i * 2;
      }
    }
  }

  // O(nlogN)
  1. 平方阶
  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) 平方阶

空间复杂度 - 与时间相对

主要看存储

  1. 常量
  let j = 0;
  for (let i = 0 i < n; i++) {
    j++;
  }
  // O(1)
  1. 线性增长
  let j = [];
  for (let i = 0; i < n; i ++) {
    j.push(i);
  }
  // O(n)

... 指数log, 嵌套

基础算法

Divide & Conquer

工作原理:(如何确定case适用分治)

  1. 可以明确设定一条基线
  2. 根据此基线可以不停将问题进行分解,直到所有内容符合基线标准

常见:快排、分班

数组中一个明确的数字作为一个基线

  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 - 利益最大化

如何在有限的时间内,听到最多的课 image.png

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;
  }

动态规划

image.png

第一人:拿到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 如何做处理?

图 与 图算法

image.png

  1. 构成: 边集合(偶数集合,一个定点两个边) + 顶点集合
  2. 分类:有向图、无向图、构造图(复合引用)

复合索引: image.png

假设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);
    }
  }

何时使用图例来解决问题 - 路径类问题、查找类问题

图解决深度优先问题

image.png

起始点开始查找,直到最末的顶点,再返回追溯,直到没有路径为止

  // 扩充 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);
        }
      })
    }

  } 

广度优先,相邻节点优先

image.png

  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;
          }
        })
      }
    }
  }

面试题:最短路径解决办法

利用广度优先天然临近查找的优势

  1. 需要一个数组用来保存所有执行的路径
  2. 除了标记节点是否被访问过之外,添加一条边来描述顶点到当前顶点的路径
  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);