JavaScript版数据结构与算法

数据结构相当“锅碗瓢盆”,算法相当于“菜谱”两者互相关联密不可分


  • 时间复杂度 就是函数 用大O表示 算法在运行过程中消耗的时间大小
    执行几次 就表示 O几 比如 O(1) 、O(n) 取最大值
  • 空间复杂度 也是函数 用大O表示 算法在运行过程中临时占用存储空间的大小
    占用几次 就表示 O几 比如 O(1) 、O(n) 取最大值

数据结构 计算机存储组织数据的方式,算法 解决问题的清晰指令 数据结构为算法服务 算法围绕数据结构操作

后进先出 js没有栈结构 需要用array模拟 java有栈,所谓的栈顶元素就是 数组长度-1

  1. 入栈: push()
  2. 出栈: pop() (移除数组的最后一项并返回它)
  3. 栈顶元素:stack[stack.length-1] 数组的最后一位。

场景:函数调用堆栈 js执行过程严格按照堆栈的模式,最后调用的函数 最早执行完

js的解释器就是用栈的方式运行代码

const fun1  = () => {
    fun2()
}
const fun2 = () => {
    fun3()
}
const fun3 = () => {   
}
fun1();

通过debug测试 进入fun1方法内部 会依此调用fun2,fun3,调用结束后会从fun3依次从调用堆栈中删除 会先删除fun3,万全符合,后进先出的场景

20.有效的括号

1、提前记录好 右括号类型), }, ] 和 左括号类型 (, {, [ 的映射表,当遍历中遇到左括号的时候,就放入栈 stack;

2、当遇到右括号时,就把 stack 顶的元素 pop 出来,看一下是否是这个右括号所匹配的左括号(比如 ( 和 ) 是一对匹配的括号),不匹配则 返回 false;

3、当遍历结束后,栈中不应该剩下任何元素,返回 true ,否则返回 false。

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
  const charMap = {
    ')': '(',
    '}': '{',
    ']': '['
  }
  const stack = []
  for(let i = 0; i < s.length; i ++) {
    const c = s[i]
    if(charMap[c]) {
      const charPop = stack.pop()
      // 栈顶、和当前字符串 是否一对匹配的括号,不匹配则 返回 `false`
      if(charPop !== charMap[c]) return false
    } else {
      stack.push(c)
    }
  }
  return stack.length === 0
};

数据结构之“队列”

先进先出,场景:食堂排队打饭先进先出保证有序。js中用数组模拟队列

  1. 入队:push()
  2. 出对:shift()
  3. 队头:queue[0]

js是单线程的运行机制,无法同时处理多个异步任务,使用任务队列先进先出处理异步任务

思考:js是单线程的运行机制,无法同时处理多个异步任务和同步任务,所以面试中会有异步和同步的任务那个先执行的考察!

链表

多个元素组成的列表,元素存储不连续通过next指针连在一起,为什么不用数组?

数组与链表

  1. 数组:数组中间增删改查元素,往往需要移动元素。
  2. 链表:链表中间增删改查元素,不需要移动元素只需要更改next指向即可。
  3. 链表:存贮的元素不是连续的,之间通过next连接

js中没有链表一般用object模拟链表

在这里插入图片描述

注意链表常用操作:遍历链表、插入、删除

遍历链表的算法:1、先申明一个指针 2、在循环里把当前的指针指向下一个链表

const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;

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

// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;

// 删除
c.next = d;

前端与链表

原型链本质就是链表,原型链通过 __proto__连接各个原型对象 比如:(function.prototype,object.prototype)而不是next

原型链长啥样子?

在这里插入图片描述

注意到对象的原型链 特别短 这个很重要 有相关的面试题考察

关于原型链的两个知识点

instanceof的原理,并用代码实现

在这里插入图片描述

const instanceofs = (A,B) => {
  let p = A;
  while (p) {
      if (p === B.prototype) {
          return true;
      }
      p = p.__proto__;
  }
  return false;
}

如果在对象上没有找到a属性就会沿着原型链去找,关于函数和数组的原型链可以查考上面的图片

let obj = {}
let f = function(){}

Object.prototype.a = '11';
Function.prototype.b = '22'

console.log(obj.a)
console.log(obj.b)
console.log(f.a);
console.log(f.b)

11
undefined
11
22

在这里插入图片描述

删除排序链表中的重复元素

使用 cur, next 两个指针表示当前值和下一值, 若 cur 指针的值与 next 指针的值相等, 则将 next 指针往后移动一位即可。

输入:head = [1,1,2,3,3]
输出:[1,2,3]

var deleteDuplicates = function(head) {
    if (!head) {
        return head;
    }

    let cur = head;
    while (cur.next) {
        if (cur.val === cur.next.val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }
    return head;
};

数据结构之“集合”

无序且唯一的数据结构,es6中的Set就是集合结构,js中的集合用 set 没有交集的概念 需要通过数组的筛选去实现

应用场景

  1. 因为是无序唯一 可以去重复
  2. 判读某个元素是否在集合中
  3. 求交集

es6中set的使用方法

let mySet = new Set();

增加操作
mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });

添加两个5只会保留一个 添加两个相同的对象会都存在 因为它们的引用地址不一样

const has = mySet.has(o);
是否存在

mySet.delete(5);
删除

for(let [key, value] of mySet.entries()) console.log(key, value);
遍历

Set转换数组
const myArr = Array.from(mySet);

数组转Set
const mySet2 = new Set([1,2,3,4]);

求交集和差集
const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));

数据结构之“字典”

存储唯一值的数据结构,它以键值对的形式来储存 es6中 map 就是字典

1两数之和 349两个数组的交集 20有效的括号 3无重复字符的最长子串(涉及到新建动态区间)76最小覆盖子串(用双指针维护一个滑动窗口 比较困难)

es6中map的使用方法

const m = new Map();

// 增
m.set('a', 'aa');
m.set('b', 'bb');

// 删
m.delete('b');
m.clear();

// 改
m.set('a', 'aaa');

两数之和
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

const twoSum = (nums, target) => {
  const prevNums = {};                    // 存储出现过的数字,和对应的索引               

  for (let i = 0; i < nums.length; i++) {       // 遍历元素   
    const curNum = nums[i];                     // 当前元素   
    const targetNum = target - curNum;          // 满足要求的目标元素   
    const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
    if (targetNumIndex !== undefined) {         // 如果存在,直接返回 [目标元素的索引,当前索引]
      return [targetNumIndex, i];
    } else {                                    // 如果不存在,说明之前没出现过目标元素
      prevNums[curNum] = i;                     // 存入当前的元素和对应的索引
    }
  }
}

两个数组的交集
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

var intersection = function(nums1, nums2) {
     nums1=nums1.sort((a,b)=>a-b);
     nums2=nums2.sort((a,b)=>a-b);
    let res=new Set();
    let i=0;
    let j=0;
    while(i<nums1.length && j<nums2.length){
        if(nums1[i]<nums2[j]){
            i++;
        }else if(nums1[i]>nums2[j]){
            j++;
        }else{
            res.add(nums1[i]);
            i++;
            j++;
        }
    }
    return [...res];
};

有效的括号
输入:s = “()[]{}”
输出:true
输入:s = “(]”
输出:false

var isValid = function(s) {
    const n = s.length;
    if (n % 2 === 1) {
        return false;
    }
    const pairs = new Map([
        [')', '('],
        [']', '['],
        ['}', '{']
    ]);
    const stk = [];
    for (let ch of s){
        if (pairs.has(ch)) {
            if (!stk.length || stk[stk.length - 1] !== pairs.get(ch)) {
                return false;
            }
            stk.pop();
        } 
        else {
            stk.push(ch);
        }
    };
    return !stk.length;
};

树–多叉树

分层数据的抽象模型 js中没有树 通过object和array构建一个树

深度优先遍历(尽可能深的搜索树的分支 ==》 相当于看书从目录章节内容从头看到尾)
广度优先遍历(先访问离根节点最近的分支 ==》先看目录再看章节最后看内容)

const tree = {
  val: '1',
  children: [
    {
      val: '2',
      children: [
        {
          val: '21',
          children: []
        }
      ]
    },
    {
      val: '3',
      children: [
        {
          val: '31',
          children: []
        }
      ]
    }
  ]
}
// 深度遍历就是普通的递归
const dfs = (tree) =>{
  console.log(tree.val);
  if(tree.children && tree.children.length){
    tree.children.forEach(dfs)
  }
}
// 广度遍历用到了队列 
const bfs = (root) =>{
  const q = [root];
  while(q.length>0){
    const t = q.shift();
    console.log(t.val);
    t.children.forEach((child) =>{
      q.push(child);
    })
  }
}

树—二叉树

104二叉树的最大深度(需用到递归设置level) 111二叉树的最小深度(解法有问题提交报错) 102二叉树的层序遍历(运行报错) 94二叉树的中序遍历112路径总和

递归版

// 先序遍历 访问根节点 对根节点的左子树进行先序遍历 对根节点的右子树进行先序遍历  简称 根左右 采用递归的方式
const perOrder = (root) =>{
      if(!root) return;
      console.log(root.val);
      perOrder(root.left)
      perOrder(root.right)
    }

// 中序遍历 对根节点的左子树进行中序遍历 访问根节点 对根节点的右子树进行中序遍历 简称左根右 采用递归的方式

const inOrder = (root) =>{
      if (!root) return;
      inOrder(root.left);
      console.log(root.val);
      inOrder(root.right);
    }

// 后序遍历 对根节点的左子树进行后序遍历 对根节点的右子树进行后序遍历 访问根节点 简称左右根 采用递归的方式

const endOrder = (root) =>{
      if (!root) return;
      endOrder(root.left);
      endOrder(root.right);
      console.log(root.val);
    }

非递归版 (重要考察)

// 先序遍历 根左右

const preOrder1 = (root) =>{
      if (!root) return;
      const stack = [root];
      while(stack.length){
        const t = stack.pop();
        console.log(t.val);
        if (t.right) stack.push(t.right)
        if (t.left) stack.push(t.left)
      }
    }

// 中序遍历 左根右

const inOrder1 = (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 endOrder1 = (root) =>{
      if (!root) return;
      const outStack = [];
      const stack = [root];
      while(stack.length){
        const t = stack.pop();
        // console.log(t.val);
        outStack.push(t);
        if (t.left) stack.push(t.left)
        if (t.right) stack.push(t.right)
      }
      while(outStack.length){
        const n = outStack.pop();
        console.log(n.val);
      }
    }

由边链接的点 比如 路线 航班图 二元关系 一条边连接两个点,js中用object和array表示,深度优先遍历(访问根节点,对根节点没访问过的相邻节点挨个深度遍历) 广度优先遍历 (递归队列的方式)

图的表示法 邻接矩阵 邻接表
65有效数字(比较困难 理解困难)417太平洋大西洋水流问题(深度优先遍历)

深度优先遍历
let set = new Set();
    const dfs11 = (node) =>{
      console.log(node);
      set.add(node);
      graph[node].forEach((c) =>{
        if (!set.has(c)){
          dfs(c);
        }
      })
    }

广度优先遍历
const set11 = new Set();
    set.add(2);
    const q = [2]
    while(q.length){
      const t = q.shift();
      console.log(t);
      graph[t].forEach((c) =>{
        if (!set.has(c)){
          q.push(c);
          set.add(t);
        }
      })
    }

完全二叉树 特殊的树 没成节点完全填满 如果没有填满只缺少右边的若干节点,所有节点都大于等于或(小于等于)它的子节点 大于等于叫最大堆 小于等于叫最小堆

js中用 数组表示堆 可以根据公式获取节点的位置

// 任意节点 获取它的左侧子节点的位置 公式 2index+1
// 任意节点 获取它的右侧子节点的位置 公式 2
index+2
// 任意节点 获取它的父节点位置 公式 (index-1)/2 只求商数 不看余数
// 用途:高效快速找出最小值 最大值 第k个最大值 第k个最小值

// 最小堆
    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 2*i+1
      }
      getRightIndex(i){
        return 2*i+2
      }
      // 上移的方法
      shiftUp(index){
        if (index == 0) return;
        const parent = this.getParentIndex(index);
        if (this.heap[parent]>this.heap[index]){
          this.swap(parent, index)
          this.shiftUp(parent)
        }
      }
      // 下移的方法
      shiftDown(index){
        const left = this.getLeftIndex(index);
        const right = this.getRightIndex(index);
        if (this.heap[left] < this.heap[index]){
          this.swap(left, index)
          this.shiftDown(left)
        }
        if (this.heap[right] < this.heap[index]){
          this.swap(right, index)
          this.shiftDown(right)
        }
      }
      insert(v){
        this.heap.push(v);
        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
      }
    }

排序(算法) 搜索(算法)

冒泡排序 比较所有相邻元素 如果第一个比第二个大交换他们 一轮下来最后一个数最大 执行n-1轮 比较简单 // 两个循环 性能不太好

Array.prototype.bubbleSort = function () {
      for(let i = 0;i<this.length-1; i++){
        for(let j = 0; j<this.length-1 -i; j+=1){
          if (this[j]>this[j+1]){
            const temp = this[j];
            this[j] = this[j+1];
            this[j+1] = temp;
          }
        }
      }
      
    }

选择排序 找到数组中最小值 选中他并放到第一位 接着找到第二个 依次类推 执行n-1轮 和冒泡排序一样 性能不太好

Array.prototype.selectionSort = function () {
      // 找到数组中最小值的下标
      for(let i = 0; i<this.length-1; i++){
        let index = i;
        for(let j = i; j<this.length; j++){
          if (this[j] < this[index]){
            index = j
          }
        }
        if(index !== i) {
          const temp = this[i];
          this[i] = this[index]
          this[index] = temp
        }
      }
    }

**插入排序 从第二个数开始往前比 比他大就往后排 依次类推直到最后一位 **

Array.prototype.insertionSort = function(){
      // 从第二个数开始往前比 的方法 1、申明一个下标 从第二个下标开始
      for(let i = 1; i<this.length; i++){ // 从第二个数遍历
        const two = this[i];
        let j = i;
        while(j>0){
          if (this[j-1]>two){
            this[j] = this[j-1]
          } else {
            break;
          }
          j--
        }
        this[j] = two
      }
      
    }

归并排序
分:把数组劈成两半 递归对子数组执行’分’的操作 直到分成一个个单独的数
合:把两个数合并为有序数组 再对有效数组合并 直到全部子数组合并成一个完整的数组

Array.prototype.mergeSort = function(){
      const rec = (arr) =>{
        if (arr.length == 1) { return arr }
        const mid = Math.floor(arr.length/2);
        const left = arr.slice(0,mid);
        const right = arr.slice(mid, arr.length)
        // 准备了有序数组 准备合并
        const orderLeft = rec(left);
        const orderRight = rec(right);
        const res = []
        while(orderLeft.length || orderRight.length){
          if (orderLeft.length && orderRight.length){
            res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
          } else if (orderLeft.length) {
            res.push(orderLeft.shift())
          } else if (orderRight.length) {
            res.push(orderRight.shift())
          }
        }
        return res;
      }
      // // 将结果 需要拷贝到this 上的 一个算法
      const res = rec(this);
      res.forEach((n,i) =>{this[i] = n})

    }

快速排序 比以上四种排序的性能都要好
分区:在数组中任意找一个基准 所有比基准小的元素放到基准前面 所有比基准大的元素放到基准的后面
递归:递归的对基准前后的子数组进行分区

Array.prototype.quickSort = function(){
      // 实现递归分区算法
      const rec = (arr) =>{
        if (arr.length === 1) { return arr }
        const left = [];
        const right = [];
        const mid = arr[0];
        for(let i = 1; i < arr.length; i+=1){
          if (arr[i] < mid){
            left.push(arr[i])
           } else {
            right.push(arr[i])
          }
        }
        
        return [...rec(left), mid, ...rec(right)]
      }
      const res = rec(this);
      res.forEach((n,i) =>{this[i] = n})
    }

顺序搜索 低效 入门算法 相当于indexOf

Array.prototype.sxSort = function(item){
      for(let i = 0; i < this.length; i++){
        if (this[i] == item){
          return i
        }
      }
      return -1;
    }

二分搜索 前提是一个有序的数组 (有序以后就能判断是在中间元素的左侧还是右侧)
从数组中间元素开始搜索 如果是目标值则终止搜索 如果目标值大于或者小于中间元素 则在大于或小于中间元素的那一半数组里搜索
// 21合并两个有序链表 374猜数字大小(完全应用了下面的算法)但是结果出错
// 下面的算法 比递归在空间复杂度上更小

Array.prototype.binarySort = function (item) {
      let low = 0;
      let hight = this.length-1;
      while(low <= hight){
        const mid = Math.floor((low+hight)/2);
        const ele = this[mid];
        if (ele < item){
          low = mid+1
        } else if (ele > item){
          hight = mid-1
        } else {
          return mid
        }
      }
      return -1;
    }

算法思路

分而治之

// 分而治之 算法设计中的方法 是一种算法思想
// 它将一个问题拆分成和原问题相识的小问题(独立) 递归解决 再将结果合并

// 归并排序 就是根据分而治之设计的
// 快速排序 也是根据分而治之设计的

// 涉及到的算法
// 226翻转二叉树 可以采用分而治之的思路 翻转函数 其实就是个递归 (比较简单 思路很重要)
// 100相同的树 分 分别获取树的左子树和右子树 执行报错需另找算法
// 101对称二叉树 判断二叉树是不是静态对称的 100和101算法很相似 都用到了递归函数

动态规划

// 算法设计中的方法 是一种思想
// 把问题分解为相互重叠的子问题
// 70爬楼梯 定义子问题 f(n) = f(n-1)+f(n-2) 反复执行子问题
// 198打家劫舍 f(k) = Math.max(f(k-2)+Ak,f(k-1)) 所谓的子问题就是定一个公式 反复执行子问题 理解太困难

贪心算法

// 算法设计思想 通过局部最优 达到全局最优 但是有时候不一定全局最优
// 455分饼干 算法相对简单 122买卖股票的最佳时机 II

回溯算法 算法思想
// 先选择一条路 如果走不通再回到原点的思路 有很多排列方式 需要递归模拟所有的排列方式
// 46全排列 每个组合不重复:定义个递归算法 时间复杂度难

46全排列 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

const permute = (nums) => {
    const res = [];
    const used = {};

    function dfs(path) {
        if (path.length == nums.length) { // 个数选够了
            res.push(path.slice()); // 拷贝一份path,加入解集res
            return;                 // 结束当前递归分支
        }
        for (const num of nums) { // for枚举出每个可选的选项
            // if (path.includes(num)) continue; // 别这么写!查找是O(n),增加时间复杂度
            if (used[num]) continue; // 使用过的,跳过
            path.push(num);         // 选择当前的数,加入path
            used[num] = true;       // 记录一下 使用了
            dfs(path);              // 基于选了当前的数,递归
            path.pop();             // 上一句的递归结束,回溯,将最后选的数pop出来
            used[num] = false;      // 撤销这个记录
        }
    }

    dfs([]); // 递归的入口,空path传进去
    return res;
};

有待验证

const fuse = (arr) =>{ 
  const result = [];
  const backtrack = (path)=>{
    if (path.length == arr.length){
      result.push(path);
      return;
    }
    arr.forEach((n) =>{
      if (path.includes(n)){ return }
      backtrack(path.concat(n))
    })
  }
  deepFor([]);
  return result
}

// 78子集 理解起来有点困难 递归遍历 比较复杂

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值