[数据结构和算法03] 栈 和 队列

208 阅读9分钟

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式
[封装03-设计模式] Decorator 装饰器模式在前端的应用
[封装04-设计模式] Publish Subscribe 发布订阅模式在前端的应用
[封装05-ElementUI源码01] Row Col Container Header Aside Main Footer

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序
[数据结构和算法02] 回文字符串
[数据结构和算法03] 栈 和 队列

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

复习笔记-01
复习笔记-02
复习笔记-03
复习笔记-04

(一) 前置知识

(1) 一些单词

palindrome 回文串

pointer 指针 
overflow 溢出
underflow 下溢
rear 背面 后面 屁股

integer 整型
decimal 十进制
binary 二进制

(2) 数组

  • 概念
    • 数组是一种线性结构,可以在数组的任意位置 插入删除 数据
    • 如果我们对这种任意性加以限制,就可以得到 队列
    • ( 栈和队列 ) 就是比较常见的 ( 受限的线性结构 )
  • 添加删除
    • 效率比较低,因为删除后,后面的所有元素都要移动位置

(3) 如何合并两个有序的数组,合并后任然保持有序?

  • 要求时间复杂度是 O(n)
// 1
// 方法1
// 如何把 ( 两个有序序列 ) 合并 ( 成一个有序序列 ) ?

const arr1 = [8, 6, 4, 1];
const arr2 = [10, 9, 7, 5, 3, 2, 11]; // 无序数组,先得处理成有序,升序

const merge = (arr1, arr2) => {
  const result = [];

  const _arr1 = arr1.slice().sort((a, b) => a - b);
  const _arr2 = arr2.slice().sort((a, b) => a - b);
  // 浅拷贝,不直接修改原数据
  // 无序数组,先排序,正序排序,因为比较时是取的较小的值进入result,即是一个升序的数组

  // 思路
  // 1. 先把两个无序的数组处理成有序,并且是升序的数组
  // 2. shift取出两个数组中的第一个元素,比较,小的进入 result
  // 3. shift继续取出两个数组中的第一个元素,比较,小的进入 result
  // 4. 不断重复,直到两个数组中数组长度较小的数组取空为止,数组长度较长的数组可以直接通过concat合并到result,因为剩下的值一定比result中的大,并且是有序的,所以直接concat

  while (_arr1.length && _arr2.length) {
    _arr1[0] < _arr2[0]
      ? result.push(_arr1.shift())
      : result.push(_arr2.shift());
    // 循环结束的条件是:只要有一个数组的元素取完,就结束循环
    // 循环的比较两个数组的第一个元素的大小,小的取出来放进result
    // 注意:shift() 删除数组第一个元素,改变原数组,返回值是删除的元素
  }

  return result.concat(_arr1.length ? _arr1 : _arr2);
  // 将两个数组中有剩余元素的数组 和 result 合并
  // concat 是将第二个数组合并到第一个数组的末尾,不改变原数组
};
2022.04.04
// 2
// 方法2
// 如何把 ( 两个有序序列 ) 合并 ( 成一个有序序列 ) ?
---
// 题目:合并两个有序的数组,合并后的数组要仍然保持有序
// 原理
// - 因为两个数组都是有序,我们再做一次升序排序
// - 因为是升序并且有序的两个数组,所以只需要比较 ( 同一下标位置的成员大小即可 )

const arr1 = [1, 4, 5, 7, 8];
const arr2 = [2, 3, 4, 5, 6];

function merge(arr1, arr2) {
  // 预先处理 - 把两个数组都变成升序
  const array1 = arr1.slice().sort((a, b) => a - b); // 浅拷贝,再升序
  const array2 = arr2.slice().sort((a, b) => a - b);
  const result = [];

  for (let i = 0; i < Math.min(arr1.length, arr2.length); i++) { 
    // 取长度小的数组长度为遍历的边界,剩下的数组的成员一定有序并大于前面的值,木桶原理
    array1[i] >= array2[i]
      ? result.push(array2[i], array1[i])
      : result.push(array1[i], array2[i]);
  }

  return result;
}

const res = merge(arr1, arr2);
console.log("res: ", res);

(4) Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER 在js中表示一个 ( 正无穷的数字 )

(5) 十进制转二进制

100转换为二进制
---

100/2=50....(余数为0) 
50/2=25.....(余数为0) 
25/2=12.....(余数为1) 
12/2=6......(余数为0) 
6/2=3.......(余数为0) 
3/2=1.......(余数为1)
1/2=0.......(余数为1)

所以100的二进制表示形式为1100100

(6) 队列

  • 队列是一种受限的线性表,只允许在在表的前端进行删除后端进行插入
  • 对头:进行删除的一端叫做队头
  • 队尾:进行插入的一端叫做队尾
  • 先进先出
数组 实现 队列
- enqueue队尾入队,dequeue队首出队
---

class Queue {
  queue = [];

  // 队尾入队 rear - 队尾插入
  enqueue = (element) => {
    this.queue.push(element);
  };

  // 对首出队 front - 队首删除
  dequeue = () => this.queue.shift();

  // 队首
  front = () => this.queue[0];

  // 队尾
  near = () => this.queue[this.size() - 1];

  size = () => this.queue.length;

  clear = () => (this.queue = []);

  isEmpty = () => !!this.size();
}

(二) 栈

(1) 概念

  • 栈是一个 ( 受限的线性表 )
    • 限定仅在 ( 表尾 ) 进行 ( 插入 ) 和 ( 删除 )
    • 即限定只在一端进行操作,限定操作的一端叫做 ( 栈顶 ),另一端是栈底
    • 是一种 ( 后进先出 ) 的数据结构,( FILO -> first in last out )
  • stack overflow 栈溢出
  • stack underflow 栈下溢

(2) 利用数组实现栈

利用数组实现栈 - 后进先出
---

class Stack {
  stacks = [];

  push = (element) => {
    this.stacks.push(element);
  };

  // pop
  // - 删除栈顶元素,并返回删除的栈顶元素
  // - 注意:需要处理边界条件,也就是栈已经为空的情况,避免stack underflow栈下溢
  pop = () => !this.isEmpty() ? stacks.pop() : 'Stack Underflow';

  // peek
  // - 作用:获取栈顶元素,不改变栈结构
  // - 注意:空栈的情况
  peek = () => {
    return this.size() ? this.stacks[this.size() - 1] : undefined;
  };

  size = () => this.stacks.length;
  isEmpty = () => this.size() === 0;
  clear = () => this.stacks = [];
}

(3) js实现一个 push pop min max 时间复杂度为 O(1) 的栈

js实现一个栈,要求push,pop,min,max的时间复杂度是1
---

思路
1. 维护三个栈,一个正常的栈dataStack,一个min栈minStack,一个max栈maxStack
2. push
   - dataStack正常入栈
   - minStack比较minStack栈顶元素和入栈元素,入栈较小的元素;如果初始时栈为空,则元素也直接入栈minStack
   - maxStack比较maxStack栈顶元素和入栈元素,入栈较大的元素;如果初始时栈为空,则元素也直接入栈maxStack
3. pop
  - dataStack,minStack,maxStack 同时出栈
  - 出栈后,minStack中栈顶仍然是剩下元素的最小值,因为添加的时每次都是最小的元素在栈顶,maxStack同理
4. min
  - 输出minStack栈顶元素,max同理
实现
// 实现 push pop min max 时间复杂度是 O(1) 的栈
class Stack {
  stacks = []; // 正常的栈
  minStacks = []; // 该栈存储动态最小值
  maxStacks = []; // 该栈存储动态最大值

  push = (element) => {
    if (this.isEmpty()) {
      // 如果stacks为空,都同时入栈
      this.stacks.push(element);
      this.minStacks.push(element);
      this.maxStacks.push(element);
    } else {
      const minTop = this.peek("minStacks")
      const maxTop = this.peek("maxStacks")
      const minElement = minTop < element ? minTop : element;
      const maxElement = maxTop > element ? maxTop : element;
      this.stacks.push(element);
      this.minStacks.push(minElement) // 较小值入栈
      this.maxStacks.push(maxElement) // 较大值入栈
    }
  };
  pop = () => {
    this.stacks.pop();
    this.minStacks.pop();
    this.maxStacks.pop();
  };

  min = () => this.peek('minStacks')
  max = () => this.peek('maxStacks')

  size = () => this.stacks.length;
  peek = (stackType) => this.size() ? this[stackType][this.size() - 1] : "空栈";
  isEmpty = () => this.size() === 0;
}

image.png

(4) 如何利用递归实现删除栈底元素,并返回栈底元素?

getAndRemoveLastStackElement
用 ( 递归 ) 实现 ( 删除并返回栈底元素 )
---

const stack = [1, 2, 3]; // 我们直接用数组的pop和push来模拟一个栈,那么1是栈底,3是栈顶

const getAndRemoveLastStackElement = (stack) => {
  const result = stack.pop();
  if (!stack.length) { // 直接用数组长度来模拟获取stack的size
    return result;
  } else {
    const last = getAndRemoveLastStackElement(stack);
    stack.push(result);
    return last;
  }
};
const res = getAndRemoveLastStackElement(stack);
console.log(`res`, res);
console.log(`stack-删除栈底后`, stack);
// 过程分析

// 1
// 第一次 getAndRemoveLastStackElement
// - result:3
// - stack:[1, 2]
// - const last = getAndRemoveLastStackElement(stack)
// ---- stack.push(result) ---> 该代码这里不会执行,因为进入getAndRemoveLastStackElement([1,2])
// ---- return last ----------> 该代码这里不会执行,因为进入getAndRemoveLastStackElement([1,2])

// 2
// 第二次
// - result:2
// - stack:[1]
// - const last = getAndRemoveLastStackElement(stack)
// ---- stack.push(result) ---> 该代码这里不会执行,因为进入getAndRemoveLastStackElement([1])
// ---- return last ----------> 该代码这里不会执行,因为进入getAndRemoveLastStackElement([1])

// 3
// 第三次
// - result:1
// - 此时, stack.pop()后的stack已经是空栈了,直接 return 1

// 4
// 接着执行 2 中没有执行的代码
// - result:2
// - last: 1
// - stack.push(2) ==================> 此时 stack = [2]
// - return 1

// 5
// 接着执行 1 中没有执行的代码
// - result:3
// - last:1
// ---- stack.push(3) ===============> 此时 stack = [2, 3]
// ---- return 1

image.png

(5) 如何利用递归反转一个栈?

  • 第一步
    • 利用(3)中的 getAndRemoveLastStackElement
    • 先删除栈底元素,并返回这个栈底元素
  • 第二步
    • 通过 reverse 实现反转
// 获取到栈底元素返回并移除
function getAndRemoveLastElement(stack) {
  let result = stack.pop();
  if (stack.length == 0) {
    return result;
  } else {
    let last = getAndRemoveLastElement(stack);
    stack.push(result);
    return last;
  }
}

// 调用getAndRemoveLastElement,将获取的栈底元素压入到栈中
function reverse(stack) {
  if (stack.length == 0) {
    return;
  }
  let i = getAndRemoveLastElement(stack);
  reverse(stack);
  stack.push(i);
}

(6) 用栈实现一个十进制转二进制

用栈实现一个十进制转二进制
- 因为入栈时,是余数从上往下的入栈,而栈又是后进先出的线性结构,出栈就是后进先出,刚好是二进制的读取方式
---

// 100转换为二进制:
// 100/2=50....(余数为0);
// 50/2=25.....(余数为0);
// 25/2=12.....(余数为1);
// 12/2=6......(余数为0);
// 6/2=3.......(余数为0);
// 3/2=1.......(余数为1);
// 1/2=0.......(余数为1);
// 所以100的二进制表示形式为1100100;

class Stack {
  stacks = [];
  push = (element) => this.stacks.push(element);
  pop = () => this.stacks.pop(); // 这里简单模拟,不做边界等情况的分析
  size = () => this.stacks.length;
}

function decimalToBinary(number) {
  const stack = new Stack();
  // 存
  while (number > 0) {
    stack.push(number % 2);
    number = Math.floor(number / 2); // 小数时,向下取整数
  }
  // 取
  let result = "";
  while (stack.size()) {
    result += stack.pop();
  }

  return Number(result);
}

image.png

面试题

(1) 寻找数组中两数之和为target的元素

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */

// 方法1
// - 时间复杂度:O(n2)
var twoSum = function(nums, target) {
    let outCount = 0;
    let inCount = 0; 
    let res = []
    for(let i = 0; i < nums.length; i++) {
        outCount++
        let isFind = false
        for(let j = i + 1; j < nums.length; j++) {
            inCount++
            if (nums[i] + nums[j] === target) {
               res = [i, j]
               isFind = true
               break;
            }
        }
        if (isFind) break;
    }
    console.log('outCount', outCount)
    console.log('inCount', inCount)
    return res
};


// 方法2
// - 时间复杂度 O(n)
var twoSum = function(nums, target) {
    const len = nums.length
    const map = new Map()

    // 把嵌套的for,平铺降低时间复杂度
    for(let i = 0; i < len; i++) {
        map.set(nums[i], i)
    }

    for(let i = 0; i < len; i++) {
        const nextTarget = target - nums[i]
        const mapNextTarget = map.get(nextTarget)
        if (map.has(nextTarget) && i !== mapNextTarget) {
            return [i, mapNextTarget]
        }
    }
};

(2) 将扁平数组 转换成 树

// 将数组 转换成 树状结构

const arr = [
  { id: 1, parentId: -1 },
  { id: 2, parentId: 1 },
  { id: 3, parentId: 1 },
  { id: 4, parentId: 2 },
  { id: 5, parentId: 4 },
];

function listToTree(arr) {
  let map = {},
    len = arr.length,
    tree = [];

  for (let i = 0; i < len; i++) {
    arr[i].children = [];
    map[arr[i].id] = arr[i];
  }
  for (let i = 0; i < len; i++) {
    const item = arr[i];
    if (item.parentId !== -1) {
      map[item.parentId].children.push(item); // 具有引用关系
    } else {
      tree.push(item);
    }
  }

  return tree;
}

const res = listToTree(arr);
console.log(`res`, res);
2022.04.04
数组转成树
---

const arr = [
  { id: 1, parentId: -1 },
  { id: 2, parentId: 1 },
  { id: 3, parentId: 1 },
  { id: 4, parentId: 2 },
  { id: 5, parentId: 4 },
];

const targetTree = {
  id: 1,
  children: [
    {
      id: 2,
      parentId: 1,
    },
    {
      id: 3,
      parentId: 1,
      children: [
        {
          id: 4,
          parentId: 3,
          children: {
            id: 5,
            parentId: 4,
          },
        },
      ],
    },
  ],
};

function listToTree(arr) {
  let map = {},
    len = arr.length,
    tree = [];

  for (let i = 0; i < len; i++) {
    arr[i].children = [];
    map[arr[i].id] = arr[i];  // 先给每项添加children属性,map中的每项的值中也就具有了children属性
  }
  for (let i = 0; i < len; i++) {
    const item = arr[i];
    if (item.parentId !== -1) {
      map[item.parentId].children.push(item); // 具有引用关系
    } else {
      tree.push(item); // 这个item中是具有 children 属性的
    }
  }

  return tree[0];
  // 1. 这里返回的是类似 tree 的数组
  // 2. 如果要真正的转成 tree,可以获取数组中的第一个成员
}

const res = listToTree(arr);
console.log(`res`, res);

(2) 将 树 转换成 扁平数组

// 树 转成 数组
const tree = [
  {
    id: 1,
    children: [
      {
        id: 2,
        parentId: 1,
      },
      {
        id: 3,
        parentId: 1,
        children: [
          {
            id: 4,
            parentId: 3,
          },
        ],
      },
    ],
  },
];

const targetArr = [
    { id: 1 },
    { id: 2, parentId: 1 },
    { id: 3, parentId: 1 },
    { id: 4, parentId: 3 },
];

function treeToArray(tree) {
  const result = [];

  function normalize(tree) {
    for (let i = 0; i < tree.length; i++) {
      const itemChildren = tree[i].children;
      if (itemChildren) {
        result.push(tree[i]);
        normalize(itemChildren);
        // delete itemChildren; // 做拆平时,删除当前一层的children
        // 2022.04.04 
        // 注意上面的写法是错误的,改成下面的写法
       delete tree[i].children
      } else {
        result.push(tree[i]);
      }
    }
  }
  normalize(tree);

  return result;
}

const res = treeToArray(tree);
console.log(`res`, res);

(3) 数组成员的最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀
---

// - 注意审题:是最长公共 - 前缀 !!!!!!!!
// - 解题思路:
//    - 以第一个成员字符串作为最长公共前缀
//    - 依次和后面的成员比较,找出第12个相同的前缀,再拿这个公共前缀后第3个成员对比,公共前缀在和第4个成员对比
// - 比如:
// 第一次 ( flower flogwx flgod ) -> flo
// 第二次 ( flo flgod ) -> fl

const strs = ["flower", "flow", "floight"];
var longestCommonPrefix = function (strs) {
  return strs.reduce((pre, cur) => {
    let res = "";
    const min = Math.min(pre.length, cur.length);
    for (let i = 0; i < min; i++) {
      if (pre[i] === cur[i]) {
        res += pre[i];
      } else break;
    }
    return res;
  });
};
const res = longestCommonPrefix(strs);
console.log(`res`, res);

资料