导航
[封装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;
}
(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
(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);
}
面试题
(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);
资料
- 实现push pop min时间复杂度是1的栈 www.jianshu.com/p/e0a9093a0…
- 递归实现反转一个栈1 www.pianshen.com/article/644…
- 递归实现反转一个栈2 juejin.cn/post/693622…
- 栈实现10进制转2进制 juejin.cn/post/689780…
- 队列 segmentfault.com/a/119000001…