- 满足后进先出,考虑用栈
解题步骤:
-
新建一个栈。
-
扫描字符串,遇到左括号入栈,遇到和栈定括号类型匹配的右括号就出栈,类型不能匹配直接判定不合法。
-
最后栈空了就合法,否则不合法
var isValid = function(s) {
//优化点:如果字符串长度为奇数,直接返回false
if(s.length % 2 === 1) return false
//1. 创建一个栈
const stack = []
for(let i=0;i<s.length;i++){
const c = s[i]
//2.1 判断是否为左括号,就进栈
if(c === ‘(’ || c === ‘{’ || c === ‘[’){
stack.push©;
} else{
//2.2 否则是右括号的话是否匹配相同的括号,匹配相同就出栈,不相同就进栈
const t = stack[stack.length - 1]
if(
(t === ‘(’ && c ===‘)’) ||
(t === ‘{’ && c ===‘}’) ||
(t === ‘[’ && c ===‘]’)
){
stack.pop()
} else{
return false
}
}
}
//3. 判断是否为空栈,如果是空栈就返回true,否则就返回false
return stack.length === 0;
};
复制代码
3. 栈的总结
-
栈是一个后进先出的数据结构
-
JavaScript中没有栈,但可以用Array的所有功能。
-
栈常用的操作:push(进栈)、pop出栈、stack[stack.length-1]
- 队列
-
一个先进先出的数据结构。
-
JavaScript中没有队列,但可以用Array实现所有功能
1. 队列的应用场景
场景一:食堂排队打饭
-
食堂只留一个窗口,排队打饭似春运。
-
先进先出,保证有序。
场景二:js异步中的任务队列
-
js是单线程,无法同时处理异步中的并发任务。
-
使用任务队列先后处理异步任务。
场景三: 计算最近请求次数
-
有请求就入队,3000ms前发出的请求出队
-
队列的长度就是最近请求次数
输入:inputs = [[],[1],[100],[3001],[3002]]
输出:[null,1,2,3]
复制代码
2. 队列实战
1. 最近请求的次数
思路:
-
越早发出的请求越早不在最近3000ms内
-
满足先进先出,考虑用队列
解题步骤:
-
先建一个队列
-
有请求就入队,把3000ms前发出的请求出队
-
队列的长度就是最近请求次数
var RecentCounter = function() {
this.queue = []
};
/**
-
@param {number} t
-
@return {number}
*/
RecentCounter.prototype.ping = function(t) {
this.queue.push(t)
while(this.queue[0] < t - 3000){
this.queue.shift()
}
return this.queue.length;
};
复制代码
3. 队列总结
-
队列是先进先出的一个数据结构。
-
JavaScript中没有队列,但可以Array实现的所有功能
-
队列操作:push、shift、queue[0]
- 链表
1. 链表是什么
-
多个元素组成的列表
-
元素存储不连续,用next指针连在一起。
2. 数组和链表的区别
-
数组:增删非首尾元素往往需要移动元素
-
链表:增删非首尾元素,不需要移动元素,只需要改next的指向即可。
3. JS中的链表
-
JavaScript中没有链表
-
可以用Object模拟链表
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§{
console.log(p.val);
p = p.next
}
复制代码
4.链表实战
1. 链表删除
解题思路:
-
无法直接获取被删除的是上个节点。
-
被删除节点转移到下一个节点。
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
复制代码
2. 反转链表
解题思路:
-
反转两个节点:将n+1的next指向n
-
反转多个节点:双指针遍历链表,重复上述操作。
解题步骤:
-
双指针一前一后遍历链表
-
反转双指针
var reverseList = function(head) {
let p1 = head
let p2 = null
while(p1){
const temp = p1.next
p1.next = p2
p2 = p1
p1 = temp
}
return p2
};
复制代码
3. 两数相加
思路:
-
小学数学题,模拟相加操作
-
需要遍历链表
解题步骤:
-
新建一个空链表
-
遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数追加到新链表上,将十位数留到下一位相加。
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 val = v1 + v2 + carry;
carry = Math.floor(val / 10)
p3.next = new ListNode(val % 10);
if(p1) p1 = p1.next
if(p2) p2 = p2.next
p3 = p3.next
}
if(carry){
p3.next = new ListNode(carry)
}
return l3.next
};
复制代码
4. 删除排序链表的重复元素
思路:
-
因为链表是有序的,所有重复元素一定会相邻
-
遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
解题步骤:
-
遍历链表发现当前元素和下个元素相同,就删除下个元素值
-
遍历结束后,返回原链表头部
var deleteDuplicates = function(head) {
let p = head
while(p&&p.next){
if(p.val === p.next.val){
//移除掉后一项
p.next = p.next.next
} else{
p = p.next
}
}
return head
};
复制代码
5. 环形链表
思路:
-
两个人在圆形操场上的起点同时起跑,速度快的人一定会超过慢的人一圈
-
用一块一慢指针遍历链表,如果指针能够相逢,那么链表就是有圈。
解题步骤:
-
用一快一慢两个指针遍历链表,如果指针能够相逢,就返回true
-
遍历结束后,还没有相逢就返回false
var hasCycle = function(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
};
复制代码
6. js中的原型链
1. 原型链的简介
-
原型链的本质是链表
-
原型链的节点是各种原型对象(prototype)
-
原型链通过__ proto __属性链接各种原型对象
2. 原型链长啥样
->代表__ proto __ 链接
-
obj -> Object.prototype -> null
-
func -> Fuction.prototype -> Object.prototype -> null
-
arr -> Array.prototype -> Object.prototype -> null
知识点:
-
如果A沿着原型链能找到B.prototype,那么A instanceof B 为true
-
如果在A对象上没有找到x属性,那么会沿着原型链找x属性。
Object.prototype.x = ‘x’
Function.prototype.y = ‘y’
const func = () => {};
console.log(func.x);//x
console.log(func.y);//y
复制代码
手写一个instanceof:
本题考查上述知识点1
解法:遍历A的原型链,如果找到B.prototype,返回true,否则返回false
const Myinstanceof = (A,B) =>{
let p = A
while§{
if(p === B.prototype){
return true;
}
p = p.proto;
}
return false;
}
复制代码
7. 前端与链表:使用链表指正获取JSON的节点值
const json ={
a:{b:{c:1}},
d:{e:2}
}
const path = [‘a’,‘b’,‘c’]
let p = json
path.forEach(k=>{
p = p[k];
})
//p:1
复制代码
5. 链表总结
-
链表的元素存储不是连续的,之间通过next连接
-
JavaScript中没有链表,可以用Object模拟链表
-
链表常用的操作:修改next、遍历链表
-
JS原型链也是一个链表,之间通过__ proto __连接
-
使用链表可以获取JSON的节点值
- 集合
1. 集合是什么
-
一种
无序且唯一
的数据结构。 -
ES6中有集合,名为Set。
-
集合的常用去重操作:去重、去判断某元素是否在集合中、求交集
2. 集合实战
1. 集合的交集
思路:
-
求交集且无序唯一
-
使用集合
解题步骤:
-
用集合对nums1去重
-
遍历nums1,筛选nums2也包含的值
var intersection = function(nums1, nums2) {
let set = new Set(nums1)
let set2 = new Set(nums2)
return […set].filter(item => set2.has(item))
};
复制代码
3. Set操作
-
使用Set对象:new、add、delete、has、size
-
迭代Set:多种迭代方法、Set与Array互转、求交集/差集
具体操作可看如下代码:
let MySet = new Set();
// add方法
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})
// has方法
const has = MySet.has(‘some text’)
// delete方法
MySet.delete(5)
// 迭代方法
for(let item of MySet) console.log(item);
for(let item of MySet.keys()) console.log(item);
for(let item of MySet.values()) console.log(item);
for(let [key,value] of MySet.entries()) console.log(key,value);
//Array和Set互转
//Set转换Array
// 1. 赋值解构
const myArr1 = […MySet]
// 2. 数组Array.from方法
const myArr2 = Array.from(MySet)
//Array转换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)))
复制代码
- 字典
1. 字典是什么?
-
与集合类似,字典也是一种存储唯一值的数据结构,但它是以
键值对
的形式来存储 -
ES6中有字典,名为Map.
-
字典的常用操作:键值对的增删改查。
const m = new Map()
//增
m.set(‘a’,‘aa’)
m.set(‘b’,‘bb’)
//删
m.delete(‘b’) //根据键来删除
// m.clear() //全删
//改
m.set(‘a’,‘aaa’)
//查
m.get(‘a’)
复制代码
2. 字典实战
1. 两个数组的交集
思路:
-
求nums1和nums2都有的值
-
用字典建立一个映射关系,记录nums1里面的值。
-
遍历nums2,找到nums1里面也有的值。
解题步骤:
-
新建立一个字典,遍历nums1,填充字典。
-
遍历nums2,遇到字典的值就选出,并从字典中删除。
var intersection = function(nums1, nums2) {
let map = new Map()
nums1.forEach(n => {
map.set(n,true)
})
const res = []
nums2.forEach(n => {
if(map.get(n)){
res.push(n)
map.delete(n)
}
})
return res
}
//时间复杂度O(m + n)
//空间复杂度为O(m)
复制代码
2. 有效括号
字典优化栈的判断:
var isValid = function(s) {
if(s.length % 2 === 1) return false;
const stack = [];
const map = new Map();
map.set(‘(’,‘)’);
map.set(‘{’,‘}’);
map.set(‘[’,‘]’);
for(let i = 0;i < s.length;i++){
const c = s[i]
if(map.has©){
stack.push©
} else{
const t = stack[stack.length - 1];
if(map.get(t) === c){
stack.pop();
} else{
return false
}
}
}
return stack.length === 0
}
复制代码
3. 两数相加
-
把nums想象成相亲者。
-
把target想象成匹配条件。
-
用字典建立一个婚姻介绍所,存储相亲者的数字和下标
解题机构:
-
新建一个字典作为结婚介绍所。
-
nums里的值,逐个来介绍所找对象,没有合适的就先登记着,有合适的就牵手成功。
var twoSum = function(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const a = target - nums[i]
if (map.has(a)) {
return [map.get(a),i]
}
else{
map.set(nums[i],i)
}
}
};
//时间复杂度O(n) 空间复杂度也是O(n)
复制代码
4. 无重复的字符最长子串
思路:
-
先找出所有的不包含重复字符的子串
-
找出长度最大那个子串,返回其长度即可
步骤:
-
用双指针维护一个滑动窗口,用来剪切子串
-
不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
-
过程中,记录所有窗口的长度,并返回最大值。
var lengthOfLongestSubstring = function(s) {
let left = 0
let res = 0
const map = new Map()
for(let right = 0;right<s.length;right++){
if(map.has(s[right]) && map.get(s[right]) >= left){
left = map.get(s[right]) + 1 //相同就滑动一位
}
res = Math.max(res,right - left + 1)
map.set(s[right],right)
}
return res
};
//时间复杂度 O(n) 空间复杂度O(m) m为不重复子串的长度
复制代码
5. 最小覆盖子串
思路:
-
先找出所有的包含T的子串
-
找出长度最小那个子串,返回即可
步骤:
-
用双指针维护一个滑动窗口
-
移动右指针,找到包含T的子串,移动左指针,尽量减少包含T的子串的长度
var minWindow = function(s, t) {
let l = 0;
let r = 0;
const need = new Map()
for(let c of t){
need.set(c , need.has©? need.get© + 1 : 1) //判断T字符需要多少
}
let needType = need.size; //设置需要都是种类
let res = ‘’;
while(r < s.length){
const c = s[r];
if(need.has©){ // 如果找到需要的字符串
need.set(c,need.get© - 1); // 找到就减一
if(need.get© === 0) needType-- // 找到为零的话就类型减一
}
while(needType === 0){ // 如果是类型数已找完整,就执行以下操作
const newRes = s.substring(l , r+1); // 得到需要的字符串
if(!res || newRes.length < res.length) res = newRes
const c2 = s[l];
if(need.has(c2)){
need.set(c2 , need.get(c2) + 1);
if(need.get(c2) === 1) needType++
}
l++
}
r++
}
return res
};
// 时间复杂度O(n+m) 空间复杂度O(m)
复制代码
3. 字典总结
-
与结婚类似,字典也是一种存储唯一值的数据结构,但它是以
键值对
的形式 -
ES6有字典,名为Map
-
字典的常规操作:键值对的增删改查
- 树
1. 树是什么?
-
一种分层的数据的抽象模型
-
前端工作中常见的树包括:DOM树,级联选择、树形控件…
-
js中没有树,但是可以用Object中Array构建树。
2. 什么是深度/广度优先遍历?
-
深度优先遍历:尽可能深的搜索树的分支
-
广度优先遍历:先访问离根节点最近的节点
深度优先遍历口诀:
-
访问根节点
-
对根节点的children挨个进行深度优先遍历
const tree = {
val:‘a’,
children: [
{
val:‘b’,
children:[
{
val:‘d’,
children:[]
},
{
val:‘e’,
children:[]
}
],
},
{
val:‘c’,
children:[
{
val:‘f’,
children:[]
},
{
val:‘g’,
children:[]
}
],
}
]
}
const dfs = (root) => {
console.log(root.val);
// root.children.forEach((child) => {dfs(child)})
root.children.forEach(dfs)
}
dfs(tree) // a b d e c f g
复制代码
广度优先遍历口诀:
-
新建一个队列,把根节点入队
-
把头部出队访问。
-
把对头的children挨个入队
-
重复第二、三步,直到队列为空。
const tree = {
val:‘a’,
children: [
{
val:‘b’,
children:[
{
val:‘d’,
children:[]
},
{
val:‘e’,
children:[]
}
],
},
{
val:‘c’,
children:[
{
val:‘f’,
children:[]
},
{
val:‘g’,
children:[]
}
],
}
]
}
const bfs = (root) =>{
const q = [root];
while(q.length > 0){
const n = q.shift()
console.log(n.val);
n.children.forEach(child =>{
q.push(child);
});
}
}
bfs(tree); // a b c d e f g
复制代码
3. 二叉树是什么?
-
树中每一个节点最多只能有两个子节点。
-
在js中通常Object来模拟二叉树。
const bt = {
val: 1,
left: {
val:2,
left: {
val: 4,
left: null,
right: null
},
right: {
val: 5,
left: null,
right: null
},
},
right: {
val: 3,
left: {
val: 6,
left: null,
right: null
},
right: {
val: 7,
left: null,
right: null
},
},
}
module.exports = bt;
复制代码
先序遍历算法口诀:
-
访问根节点
-
对根节点的左子树进行先序遍历
-
对根节点的右子树进行先序遍历
递归版:
const bt = require(‘./bt’);
const preorder = (root) => {
if(!root) return;
console.log(root.val);
preorder(root.left);
preorder(root.right);
}
preorder(bt); // 根左右: 1 2 4 5 3 6 7
复制代码
非递归版:
const preorder = (root) => {
if(!root) return;
const stack = [root]
while(stack.length){
const n = stack.pop();
console.log(n.val);
if(n.right) stack.push(n.right)
if(n.left) stack.push(n.left)
}
}
复制代码
中序遍历算法口诀:
-
对根节点的
左
子树进行中序遍历。 -
访问
根
节点 -
对根节点的
右
子树进行中序遍历。
const bt = require(‘./bt’)
const inorder = (root) => {
if(!root) return
inorder(root.left)
console.log(root.val);
inorder(root.right)
}
inorder(bt) //4 2 5 1 6 3 7
复制代码
非递归版:
const inorder = (root) => {
if(!root) return
const stack = []
let p = root;
while(stack.length || p){
while§{
stack.push§
p = p.left;
}
const n = stack.pop()
console.log(n.val);
p = n.right;
}
}
复制代码
后序遍历算法口诀:
-
对根节点的左子树进行后序遍历
-
对根节点的右子树进行后序遍历
-
访问根节点
const bt = require(‘./bt’)
const postorder = (root) =>{
if(!root) return;
postorder(root.left)
postorder(root.right)
console.log(root.val);
}
postorder(bt); // 4 5 2 6 7 3 1
复制代码
非递归版:
const postorder = (root) => {
// 1.左右根 —> 根右左
if(!root) return;
const outputStack = [];
const stack = [root];
while(stack.length){
const n = stack.pop()
outputStack.push(n)
if(n.left) stack.push(n.left)
if(n.right) stack.push(n.right)
}
while(outputStack.length){
const n = outputStack.pop()
console.log(n.val);
}
}
复制代码
4. 树的实战
1. 树的最大深度
思路:
-
最大深度,考虑使用深度优先遍历
-
在深度优先遍历过程中,记录没个节点所在的层级,找到最大的层级即可
解题步骤:
-
新建一个变量,记录最大深度
-
深度优先遍历整颗树,并记录每个节点的层级,同时不断刷新最大深度这个变量
-
遍历结束后最大深度这个变量
var maxDepth = function(root) {
let res = 0;
const dfs = (n,l) => {
if(!n) return;
if(!n.left && !n.right) res = Math.max(res,l)
dfs(n.left,l+1)
dfs(n.right,l+1)
}
dfs(root,1)
return res
};
// 空间复杂度最好情况O(log(n)),最坏的情况O(n)
复制代码
2. 树 的最小深度
思路:
-
求最小深度,考虑广度优先遍历。
-
在广度优先遍历过程中,遇到叶子节点停止遍历,返回节点层级。
解题步骤:
-
广度优先遍历整颗树,并记录每一个节点的层级。
-
遇到叶子节点,返回节点层级,停止遍历。
var minDepth = function(root) {
if(root === null) return 0
const q = [[root,1]];
while(q.length){
const [n,l] = q.shift();
if(!n.left && !n.right) return l;
if(n.left) q.push([n.left,l+1]);
if(n.right) q.push([n.right,l+1]);
}
};
// 时间复杂度O(n),空间复杂度O(n)
复制代码
3. 二叉树的层序遍历
思路:
-
层序遍历顺序就是广度优先遍历。
-
不过在遍历时候需要记录当前节点所在的层级,方便将其添加不同的数组中。
解题步骤:
-
广度优先遍历二叉树
-
遍历过程中,记录每个节点的层级,并将其添加不同的数组中。
var levelOrder = function(root) {
if(!root) return [];
const q = [root]
const res = []
while(q.length){
let len = q.length;
res.push([]);
while(len–){ // 保证在同一层级
const n = q.shift()
res[res.length -1].push(n.val);
if(n.left) q.push(n.left)
if(n.right) q.push(n.right)
}
}
return res
};
复制代码
4. 二叉树的中序遍历
// 迭代版
var inorderTraversal = function(root) {
const res = []
const stack = []
let p = root;
while(stack.length || p){
while§{
stack.push§
p = p.left
}
const n = stack.pop()
res.push(n.val)
p = n.right
}
return res;
};
//递归版
var inorderTraversal = function(root) {
const res = []
const rec = (n) =>{
if(!n) return;
rec(n.left)
res.push(n.val)
rec(n.right);
};
rec(root)
return res
};
复制代码
5. 路径的总和
思路:
-
在深度优先遍历过程中,记录当前路径的节点值的和。
-
在叶子节点处,判断当前路径的节点值是否等于目标值。
解题步骤:
-
深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就是返回true
-
遍历结束,如果没有匹配,就返回false
var hasPathSum = function(root, targetSum) {
if(!root) return false;
let res = false
const dfs = (n,s)=>{
if(!n.left && !n.right && s=== targetSum) res = true
if(n.left) dfs(n.left,s+n.left.val)
if(n.right) dfs(n.right,s+n.right.val)
}
dfs(root,root.val)
return res
};
// 时间复杂度O(n) 空间复杂度为O(n) 因为使用递归
复制代码
6. 遍历JSON的所有节点值
const json = {
a: {b: {c:1}},
d:[1,2]
}
const dfs = (n,path) =>{
console.log(n,path);
Object.keys(n).forEach(k =>{
dfs(n[k],path.concat(k));
})
}
dfs(json,[])
复制代码
输出:
{ a: { b: { c: 1 } }, d: [ 1, 2 ] } []
{ b: { c: 1 } } [ ‘a’ ]
{ c: 1 } [ ‘a’, ‘b’ ]
1 [ ‘a’, ‘b’, ‘c’ ]
[ 1, 2 ] [ ‘d’ ]
1 [ ‘d’, ‘0’ ]
2 [ ‘d’, ‘1’ ]
复制代码
5. 前端与树:渲染Antd的树组件
react+antd组件树的js部分
const {Tree} = antd;
const {TreeNode} = Tree;
const json = [
{
title:‘一’,
key:‘1’,
children:[{title:‘三’ , key:‘3’, children:[]}]
},
{
title:‘二’,
key:‘2’,
children:[{title:‘四’,key:‘4’,children:[]}]
}
]
class Demo extends React.Component {
dfs = (n) =>{
return (
{n.children.map(this.dfs)}
)
}
render(){
return(
{json.map(this.dfs)}
)
}
}
ReactDOM.render(,mountNode)
复制代码
6. 章节总结
-
树是一种分层数据的抽象模型,在前端广泛应用
-
树的常规用操作:深度/广度优先遍历、先中后序遍历…
- 图
1. 图是什么
-
图是
网络结构
的抽象模型,是一组由边
连接的节点
-
图可以表示任何二元关系:比如道路、航班…
-
js没有图,但是可以用Object和Array构建图
-
图的表示法:邻接矩阵、邻接表、关联矩阵…
邻接矩阵:
邻接表:
- 图常用的操作:深度优先遍历、广度优先遍历
2. 图的深度优先遍历和广度优先遍历
什么是深度/广度优先遍历?
-
深度优先遍历:尽可能深的搜索图的分支
-
广度优先遍历:先访问离根节点最近的节点
// 描绘图
const graph = {
0: [1,2],
1:[2],
2:[0,3],
3:[3]
}
module.exports = graph
复制代码
深度优先遍历算法口诀
-
访问根节点。
-
对根节点的
没访问过的相邻节点
挨个进行深度优先遍历。
深度优先实现代码:
const graph = require(‘./graph’)
const visted = new Set();
const dfs = (n) =>{
console.log(n);
visted.add(n);
graph[n].forEach(c => {
if(!visted.has©){
dfs©
}
});
}
dfs(2)
复制代码
广度优先遍历算法口诀:
-
新建一个队列,把根节点入队
-
把队头并访问。
-
把队头的
没有访问过的相邻节点
入队 -
重复第二、三步,直到队列为空
const graph = require(‘./graph’)
const visited = new Set()
visited.add(2);
const q = [2];
while(q.length){
const n = q.shift();
console.log(n);
graph[n].forEach(c => {
if(!visited.has©){
q.push©
visited.add©
}
});
}
复制代码
3. 图的实战
1. 有效数字
思路:
解题步骤:
-
构建一个表示状态的图
-
遍历字符串,并沿着图走,如果到某个节点无路可走就返回false
-
遍历结束,如果走到3/5/6,就返回true,否则返回false。
var isNumber = function(s) {
const graph = {
0: {‘blank’:0,‘sign’:1,‘.’:2,‘digit’:6},
1: {‘digit’:6,‘.’:2},
2: {‘digit’:3},
3: {‘digit’:3,‘e’:4},
4: {‘digit’:5,‘sign’:7},
5: {‘digit’:5},
6: {‘digit’:6,‘.’:3,‘e’:4},
7: {‘digit’:5}
};
let state = 0;
for(c of s.trim()){
if(c >= ‘0’ && c <= ‘9’){
c = ‘digit’;
} else if(c === ’ '){
c = ‘blank’;
} else if(c ===‘+’ || c ===‘-’){
c =‘sign’;
} else if(c === ‘E’){
c = ‘e’
}
state = graph[state][c];
if(state === undefined) return false
}
if(state === 3 || state === 5 || state === 6){
return true
}
return false
};
//时间复杂度O(n) 空间复杂度O(1)
复制代码
2. 太平洋大西洋流水问题
思路:
-
把矩阵想象成图。
-
从海岸线逆流而上遍历图,所到之处就可以流到某个太平洋的坐标
解题步骤:
-
新建两个矩阵,分别记录能流到两个大洋的坐标
-
从海岸线,多管齐下,同时深度遍历图,过程中填充上述矩阵。
-
遍历两个矩阵,找到流到两个大洋坐标
var pacificAtlantic = function(heights) {
if(!heights||!heights[0]) return []
const m = heights.length
const n = heights[0].length
const flow1 = Array.from({length:m},()=> new Array(n).fill(false))
const flow2 = Array.from({length:m},()=> new Array(n).fill(false))
const dfs = (r,c,flow) =>{
flow[r][c] = true;
[[r-1,c],[r+1,c],[r,c-1],[r,c+1]].forEach(([nr,nc])=>{
if(
//保证在矩阵中
nr >= 0 && nr < m &&
nc >= 0 && nc < n &&
//防止死循环
!flow[nr][nc] &&
//保证逆流而上
heights[nr][nc] >= heights[r][c]
){
dfs(nr,nc,flow)
}
})
};
//沿着海岸线逆流而上
for(let r = 0;r<m;r++){
dfs(r,0,flow1)
dfs(r,n-1,flow2)
}
for(let c = 0;c<n;c++){
dfs(0,c,flow1)
dfs(m-1,c,flow2)
}
//收集能流到两个大洋的坐标
const res = []
for(let r = 0;r<m;r++){
for(let c = 0;c<n;c++){
if(flow1[r][c]&&flow2[r][c]){
res.push([r,c])
}
}
}
return res
};
复制代码
3. 克隆图
思路:
-
拷贝所有节点
-
拷贝所有的边
解题思路:
-
深度或者广度优先遍历所有的节点
-
拷贝所有的节点,存储起来
-
将拷贝的节点,按照原图的方法进行连接
图的深度优先遍历:
var cloneGraph = function (node){
if(!node) return;
const visited = new Map()
const dfs = (n)=>{
const nCopy = new Node(n.val)
visited.set(n,nCopy);
(n.neighbors || []).forEach(ne =>{
if(!visited.has(ne)){
dfs(ne)
}
nCopy.neighbors.push(visited.get(ne))
})
};
dfs(node);
return visited.get(node)
}
// 时间复杂度O(n) 空间复杂度O(n)
复制代码
图的广度优先遍历:
var cloneGraph = function (node){
if(!node) return;
const visited = new Map()
visited.set(node,new Node(node.val))
const q = [node];
while(q.length){
const n = q.shift()
n.neighbors.forEach(ne =>{
if(!visited.has(ne)){
q.push(ne)
visited.set(ne,new Node(ne.val))
}
visited.get(n).neighbors.push(visited.get(ne))
})
}
return visited.get(node)
}
复制代码
4. 图的总结
-
图是
网络结构
的抽象模型,是一组由边
连接的节点 -
图可以表示任何二元关系,比如道路、航班…。
-
js没有图,但可以用Object和Array构建图。
-
图的表示法:邻接矩阵、邻接表…
-
图的常用操作:深度优先/广度优先遍历
- 堆
1. 什么是堆?
-
堆是一颗特殊的
完全二叉树
. -
所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。
-
js常用数组表示堆
-
左侧子节点的位置是2*index + 1。
-
右侧子节点的位置是2*index + 2.
-
父节点位置(index - 1)/2
-
堆能高效、快速地找到最大值和最小值,时间复杂度:O(1)
-
找到第K个最大(小)元素。
2. 最小堆类
-
在类里,声明一个数组,用来装元素。
-
主要方法:插入、删除堆顶、获取堆顶、获取堆大小。
- 插入
-
将值插入堆的底部,即数组的尾部。
-
然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值。
-
大小为k的堆插入元素的时间复杂度为O(logk)
- 删除堆顶
-
用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
-
然后下移:将新堆顶赫尔它的子节点进行交换,直到子节点大于等于这个新堆顶。
-
大小为k的堆中删除堆顶的时间复杂度为O(logk)。
- 获取堆顶和堆的大小
-
获取堆顶:返回数组的头部。
-
获取堆的大小:返回数组的长度
class MinHeap {
constructor(){
this.heap = []
}
swap(i1,i2){ //交换操作
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
getPartIndex(i){ // 获取父节点操作
return Math.floor((i - 1) / 2)
// return (i - 1) >> 1;
}
getLeftIndex(i){
return i * 2 + 1;
}
getRightIndex(i){
return i * 2 + 2;
}
shiftUp(index){ // 上移算法
if(index == 0) { return; }
const parentIndex = this.getPartIndex(index) // 获取子节点的下标
if(this.heap[parentIndex] > this.heap[index]){ //如果父节点大于子节点
this.swap(parentIndex, index)
}
}
shiftDown(index){ // 下移算法
const leftIndex = this.getLeftIndex(index);
const RightIndex = this.getRightIndex(index);
if(this.heap[leftIndex] < this.heap[index]){
this.swap(leftIndex,index)
this.shiftDown(leftIndex)
}
if(this.heap[RightIndex] < this.heap[index]){
this.swap(RightIndex,index)
this.shiftDown(RightIndex)
}
}
insert(value){ //插入操作
this.heap.push(value);
this.shiftUp(this.heap.length -1); //向上移
this.shiftUp(parentIndex);
}
pop(){ //删除操作
this.heap[0] = this.heap.pop();
this.shiftDown(0)
}
peek() { //获取头部操作
return this.heap[0];
}
size() { // 获取大小操作
return this.heap.length;
}
}
复制代码
3. 第K个最大元素问题
-
构建一个最小堆,并将元素依次插入堆中。
-
当堆的容量超过K,就删除对顶。
-
插入结束后,堆顶就是第K个最大元素。
思路:
-
看到“第k个最大元素”
-
考虑选择使用最小堆
解题步骤:
-
构建最小堆,并依次把数组的值插入堆中
-
当堆的容量超过K,就会删除堆顶。
-
插入结束后,堆顶就是第K个最大元素
var findKthLargest = function(nums, k) {
const h = new MinHeap();
nums.forEach(n => {
h.insert(n)
if(h.size() > k) h.pop();
})
return h.peek();
};
// 时间复杂度O(nlogk) 空间复杂度O(k)
复制代码
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
叉树`.
-
所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。
-
js常用数组表示堆
-
左侧子节点的位置是2*index + 1。
-
右侧子节点的位置是2*index + 2.
-
父节点位置(index - 1)/2
-
堆能高效、快速地找到最大值和最小值,时间复杂度:O(1)
-
找到第K个最大(小)元素。
2. 最小堆类
-
在类里,声明一个数组,用来装元素。
-
主要方法:插入、删除堆顶、获取堆顶、获取堆大小。
- 插入
-
将值插入堆的底部,即数组的尾部。
-
然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值。
-
大小为k的堆插入元素的时间复杂度为O(logk)
- 删除堆顶
-
用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
-
然后下移:将新堆顶赫尔它的子节点进行交换,直到子节点大于等于这个新堆顶。
-
大小为k的堆中删除堆顶的时间复杂度为O(logk)。
- 获取堆顶和堆的大小
-
获取堆顶:返回数组的头部。
-
获取堆的大小:返回数组的长度
class MinHeap {
constructor(){
this.heap = []
}
swap(i1,i2){ //交换操作
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
getPartIndex(i){ // 获取父节点操作
return Math.floor((i - 1) / 2)
// return (i - 1) >> 1;
}
getLeftIndex(i){
return i * 2 + 1;
}
getRightIndex(i){
return i * 2 + 2;
}
shiftUp(index){ // 上移算法
if(index == 0) { return; }
const parentIndex = this.getPartIndex(index) // 获取子节点的下标
if(this.heap[parentIndex] > this.heap[index]){ //如果父节点大于子节点
this.swap(parentIndex, index)
}
}
shiftDown(index){ // 下移算法
const leftIndex = this.getLeftIndex(index);
const RightIndex = this.getRightIndex(index);
if(this.heap[leftIndex] < this.heap[index]){
this.swap(leftIndex,index)
this.shiftDown(leftIndex)
}
if(this.heap[RightIndex] < this.heap[index]){
this.swap(RightIndex,index)
this.shiftDown(RightIndex)
}
}
insert(value){ //插入操作
this.heap.push(value);
this.shiftUp(this.heap.length -1); //向上移
this.shiftUp(parentIndex);
}
pop(){ //删除操作
this.heap[0] = this.heap.pop();
this.shiftDown(0)
}
peek() { //获取头部操作
return this.heap[0];
}
size() { // 获取大小操作
return this.heap.length;
}
}
复制代码
3. 第K个最大元素问题
-
构建一个最小堆,并将元素依次插入堆中。
-
当堆的容量超过K,就删除对顶。
-
插入结束后,堆顶就是第K个最大元素。
思路:
-
看到“第k个最大元素”
-
考虑选择使用最小堆
解题步骤:
-
构建最小堆,并依次把数组的值插入堆中
-
当堆的容量超过K,就会删除堆顶。
-
插入结束后,堆顶就是第K个最大元素
var findKthLargest = function(nums, k) {
const h = new MinHeap();
nums.forEach(n => {
h.insert(n)
if(h.size() > k) h.pop();
})
return h.peek();
};
// 时间复杂度O(nlogk) 空间复杂度O(k)
复制代码
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-MV0tq1rx-1714917695490)]
[外链图片转存中…(img-dbirYIui-1714917695490)]
[外链图片转存中…(img-PHNJrVZ0-1714917695490)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!