# 算法基础

# 时间复杂度和计算方法

计算时间复杂度就是计算代码执行多少次,循环多少次。

例:

o(1)
n+1=2
console.log(n)

o(n)
for(let i = 0;i < n;i++){
    console.log(i)
}

o(n^2)
for(let i =0;i<n;i++){
    for(let j = 0;j < n;j++){
        console.log()
    }
}

o(logN)
let i =1
while(i<n){
    i* = 2
    console.log(n)
}

递归进行一次递归调用,每次递归调用的最大深度为 logn。考虑递归最大深度即可

时间复杂度为 O(logn)

一次递归中调用多次递归,这里需要画出递归树进行分析,考虑的是递归调用次数

也就是二叉树的节点个数,O(2^n)

对于较复杂的情况,也是调用多次递归,这里计算复杂度是通过话出递归树,然后计算层数和每个节点处理的时间复杂度

归并排序和快速排序,一共有 logn 层,然后每一层需要处理的节点是 n 个节点,所以时间复杂度为 O(nlogn)

# 空间复杂度

空间复杂度计算就是计算声明的变量,或者内存变量是多少个

例:

O(1);
let i = 1;
console.log(1);

O(n);
let a = [];
for (let i = 0; i < n; i++) {
  a.push[i];
}

O(n ^ 2);

let a = [];
for (let i = 0; i < n; i++) {
  a.push([]);
  for (let j = 0; j < n; j++) {
    a[i][j].push(j);
  }
}

#

后进先出的一种内存结构.比如 js 中的数组

let stack = [];
let item1 = stack.push(1);
let item2 = stack.pop();

栈这种数据结构出现得场景

  • 十进制转二进制 在利用短除法时计算二进制,最后取的是余数从后面排到前面,采用的是栈的思想,后进先出。
  • 有效的花括号,例如判断一堆括号是否是有效的闭合的就可以采用栈的形式

解决这个问题的思路就是使用栈,首先遍历字符串,然后如果元素碰到{([这种字符就往栈推送元素,否则判断元素和栈顶元素是否同时满足{} (),[]这种组合。满足则将元素推出栈。最后判断是否栈为空,为空则返回true

function isvalid(s) {
  let stack = [];
  if (stack.length % 2 === 1) {
    return false;
  }
  for (let i = 0; i < s.length; i++) {
    let c = s[i];
    if (c === "{" || c === "(" || c === "[") {
      stack.push(c);
    } else {
      let p = stack[stactk.length - 1];
      if ((p === "(" && c === ")") || (p === "{" && c === "}" && p === "[" && p === "]")) {
        stack.pop();
      } else {
        return false;
      }
    }
  }
  return statck.length === 0;
}
  • 函数调用,js 解析器就使用了栈
// 首先入栈func1 func2 func3 出栈先调用func3 func2 func1
const func1 = () => {
  fun2();
};
const func2 = () => {
  func3();
};

func1();

# 队列

先进先出

const array = [];
array.push();
array.shift();

使用场景:

  • 食堂打饭
  • js 异步任务队列
  • 计算请求次数

计算三千毫秒以内的请求次数

输入:inputs = ["RecentCounter","ping","ping","ping","ping"], inputs = [[],[1],[100],[3001],[3002]] 输出:[null,1,2,3,3]

时间复杂度 o(n),空间复杂度 O(队列长度)

function counter() {
  // 保证使用同一个队列
  this.q = [];
}

Function.prototype.ping = function(t) {
  // 请求时间入队
  this.q.push(t);
  // 大于三千毫秒出队
  while (this.q[0] < t - 3000) {
    this.q.shift();
  }
  return this.q.length;
};

var counter = new counter();
counter.ping(t);

# 链表

链表元素可以用对象进行模拟,相比与数组最大的特点就是不用移动元素位置

const a = { val: "a" };
const b = { val: "b" };
const c = { val: "c" };

a.next = b;
b.next = c;

// 遍历链表
let p = a;
while (p) {
  console.log(p);
  p = p.next;
}

// 增加
const e = { val: "e" };
a.next = e;
e.next = b;

// 删除
a.next = b;

删除某个节点

思路就是将节点换成下个节点的值,然后删除掉下个节点

var deleteNode = function(node) {
  node.val = node.next.val;
  node.next = node.next.next;
};

反转链表

首先定义双指针遍历,p1,p2 分别指向头部和 null,然后在遍历能进行的条件下将 p1 指向 p2

var reverseList = function(head) {
  let p1 = head;
  let p2 = null;
  while (p1) {
    // 保存状态
    const temp = p1.next;
    // 反转
    p1.next = p2;
    // 双指针遍历 一前一后
    p2 = p1;
    p1 = temp;
  }
  // p1最后指向null
  return p2;
};

两数之和

思路: 首先设置两个指针,新建一个链表

如果两个链表元素的值相加大于 10,就把个位数追加进新链表,十位数放到下一次再增加 每次添加的值都是: 两个链表的值再加上十位数的值

最后遍历链表。如果链表循环结束之后还有进 1 情况,就把 carry 追加到链表上面,最后返回新链表

var addTwoNumbers = function(l1, l2) {
  // 初始化链表
  const l3 = new ListNode(0);
  let p1 = l1;
  let p2 = l2;
  let p3 = l3;
  // 下一轮要加的数
  let carry = 0;
  while (p1 || p2) {
    // 取值
    const v1 = p1 ? p1.val : 0;
    const v2 = p2 ? p2.val : 0;
    const v3 = v1 + v2 + carry;
    // 获取下一轮叠加数字 也就是获取十位数
    carry = Math.floor(v3 / 10);
    // 获取本轮个位上数 然后追加到链表上
    p3.next = new ListNode(v3 % 10);
    // 遍历链表
    if (p1) p1 = p1.next;
    if (p2) p2 = p2.next;
    p3 = p3.next;
  }
  // 需要考率到while循环之后进1情况
  if (carry) {
    p3.next = new ListNode(carry);
  }
  return l3.next;
};

删除链表重复元素

节点值和下个节点值相同,删除

function deleteAlreadyExisted(head) {
  let p = head;
  while (p && p.next) {
    if (p.val === p.next.val) {
      p.next = p.next.next;
    }
  }
  return head;
}

判断是否是环形链表

定义两个一块一慢的指针遍历,相逢则成环

function isCircleListNode(head) {
  let p1 = head;
  let p2 = head;
  while (p1 && p2 && p2.next) {
    p1 = p1.next;
    p2 = p2.next.next;
    if (p1 === p2) {
      return true;
    }
  }
  return false;
}

# 集合

无需且唯一的数据结构,可以去重

// 去重
const arr1 = [1, 2, 3, 4, 4, 4];
const arr2 = [...new Set(arr1)];

// 判断一个值是否存在集合中
const set1 = new Set([1, 2]);
set1.has(1);

// 求交集
const set2 = new Set([1, 2, 3, 4, 5]);
const set3 = new Set([...set1].filter(item => set2.has(item) === item));

求两个数组的交集

[...new Set(nums1)].filter(item => nums2.includes(item));

# 字典 map

求一个字符的不重复最长子串长度

var lengthOfLongestSubstring = function(s) {
  let l = 0;
  let res = 0;
  // 记录子串位置和值
  const map = new Map();
  for (let r = 0; r < s.length; r++) {
    // 遇到重复的就把左指针移动到重复节点的下一个 并且需要保证map中保存的数据是在滑动窗口中
    if (map.has(s[r]) && map.get(s[r]) >= l) {
      l = map.get(s[r]) + 1;
    }
    res = Math.max(res, r - l + 1);
    map.set(s[r], r);
  }
  return res;
};

时间: O(n) 空间: O(m) 不重复子串长度

#

深度广度优先遍历

深度优先遍历: 将他看成正常看书顺序,先封面,再从第一章开始看依次下去

口诀: 先访问根节点,再挨个深度有限遍历根节点的 children

const tree = {
  val: "a",
  children: [
    {
      val: "b",
      children: [
        {
          val: "d",
          children: []
        },
        {
          val: "e",
          children: []
        }
      ]
    },
    {
      val: "c",
      children: [
        {
          val: "f",
          children: []
        },
        {
          val: "g",
          children: []
        }
      ]
    }
  ]
};
let dfs = root => {
  // 访问根节点
  console.log(root.val);
  // 递归挨个遍历子节点
  root.children.forEach(dfs);
};
dfs(tree);

广度优先遍历: 将他看成先看封面再看目录,再去看文章

let bfs = root => {
  // 根节点入队
  let q = [root];
  while (q.length > 0) {
    // 队头出队 并访问
    const n = q.shift();
    console.log(n.val);
    // 队头children挨个入队
    n.children.forEach(child => {
      q.push(child);
    });
  }
};

先中后序遍历

Last update: 7/28/2021, 7:33:21 PM