JavaScript算法基础及面试总结(1w1字)(1)

  • 满足后进先出,考虑用栈

解题步骤

  • 新建一个栈。

  • 扫描字符串,遇到左括号入栈,遇到和栈定括号类型匹配的右括号就出栈,类型不能匹配直接判定不合法。

  • 最后栈空了就合法,否则不合法

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. 队列的应用场景

场景一:食堂排队打饭

  • 食堂只留一个窗口,排队打饭似春运。

  • 先进先出,保证有序。

image-20211010183426843.png

场景二:js异步中的任务队列

  • js是单线程,无法同时处理异步中的并发任务。

  • 使用任务队列先后处理异步任务。

image-20211010183632137.png

场景三: 计算最近请求次数

  • 有请求就入队,3000ms前发出的请求出队

  • 队列的长度就是最近请求次数

输入:inputs = [[],[1],[100],[3001],[3002]]

输出:[null,1,2,3]

复制代码

2. 队列实战

1. 最近请求的次数

题目链接:933. 最近的请求次数 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 越早发出的请求越早不在最近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. 链表删除

题目链接:237. 删除链表中的节点 - 力扣(LeetCode) (leetcode-cn.com)")

解题思路

  • 无法直接获取被删除的是上个节点。

  • 被删除节点转移到下一个节点。

var deleteNode = function(node) {

node.val = node.next.val;

node.next = node.next.next;

};

复制代码

2. 反转链表

题目链接:206. 反转链表 - 力扣(LeetCode) (leetcode-cn.com)")

解题思路

  • 反转两个节点:将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. 两数相加

题目链接:2. 两数相加 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 小学数学题,模拟相加操作

  • 需要遍历链表

解题步骤

  • 新建一个空链表

  • 遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数追加到新链表上,将十位数留到下一位相加。

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. 删除排序链表的重复元素

题目链接:83. 删除排序链表中的重复元素 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 因为链表是有序的,所有重复元素一定会相邻

  • 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值

解题步骤

  • 遍历链表发现当前元素和下个元素相同,就删除下个元素值

  • 遍历结束后,返回原链表头部

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. 环形链表

题目链接:141. 环形链表 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 两个人在圆形操场上的起点同时起跑,速度快的人一定会超过慢的人一圈

  • 用一块一慢指针遍历链表,如果指针能够相逢,那么链表就是有圈。

解题步骤

  • 用一快一慢两个指针遍历链表,如果指针能够相逢,就返回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

知识点

  1. 如果A沿着原型链能找到B.prototype,那么A instanceof B 为true

  2. 如果在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. 集合的交集

题目链接:349. 两个数组的交集 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 求交集且无序唯一

  • 使用集合

解题步骤

  • 用集合对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. 两个数组的交集

题目链接:349. 两个数组的交集 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 求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. 有效括号

题目链接:20. 有效的括号 - 力扣(LeetCode) (leetcode-cn.com)")

字典优化栈的判断

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. 两数相加

题目链接:1. 两数之和 - 力扣(LeetCode) (leetcode-cn.com)")

  • 把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. 无重复的字符最长子串

题目链接:3. 无重复字符的最长子串 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 先找出所有的不包含重复字符的子串

  • 找出长度最大那个子串,返回其长度即可

步骤

  • 用双指针维护一个滑动窗口,用来剪切子串

  • 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位

  • 过程中,记录所有窗口的长度,并返回最大值。

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. 最小覆盖子串

题目链接:76. 最小覆盖子串 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 先找出所有的包含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. 什么是深度/广度优先遍历?

  • 深度优先遍历:尽可能深的搜索树的分支

  • 广度优先遍历:先访问离根节点最近的节点

image-20211014102848520.png

深度优先遍历口诀:

  • 访问根节点

  • 对根节点的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. 树的最大深度

题目链接:104. 二叉树的最大深度 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 最大深度,考虑使用深度优先遍历

  • 在深度优先遍历过程中,记录没个节点所在的层级,找到最大的层级即可

解题步骤

  • 新建一个变量,记录最大深度

  • 深度优先遍历整颗树,并记录每个节点的层级,同时不断刷新最大深度这个变量

  • 遍历结束后最大深度这个变量

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. 二叉树的层序遍历

题目链接:102. 二叉树的层序遍历 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 层序遍历顺序就是广度优先遍历。

  • 不过在遍历时候需要记录当前节点所在的层级,方便将其添加不同的数组中。

解题步骤

  • 广度优先遍历二叉树

  • 遍历过程中,记录每个节点的层级,并将其添加不同的数组中。

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. 二叉树的中序遍历

题目链接:94. 二叉树的中序遍历 - 力扣(LeetCode) (leetcode-cn.com)")

// 迭代版

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. 路径的总和

题目的链接:112. 路径总和 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 在深度优先遍历过程中,记录当前路径的节点值的和。

  • 在叶子节点处,判断当前路径的节点值是否等于目标值。

解题步骤

  • 深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就是返回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构建图

  • 图的表示法:邻接矩阵、邻接表、关联矩阵…

邻接矩阵:

image-20211019183058302.png

邻接表

image-20211019183229171.png

  • 图常用的操作:深度优先遍历、广度优先遍历

2. 图的深度优先遍历和广度优先遍历

什么是深度/广度优先遍历?

  • 深度优先遍历:尽可能深的搜索图的分支

  • 广度优先遍历:先访问离根节点最近的节点

image-20211019184928264.png

// 描绘图

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. 有效数字

题目链接:65. 有效数字 - 力扣(LeetCode) (leetcode-cn.com)")

思路

image-20211019192114550.png

解题步骤

  • 构建一个表示状态的图

  • 遍历字符串,并沿着图走,如果到某个节点无路可走就返回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. 太平洋大西洋流水问题

题目链接:417. 太平洋大西洋水流问题 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 把矩阵想象成图。

  • 从海岸线逆流而上遍历图,所到之处就可以流到某个太平洋的坐标

解题步骤

  • 新建两个矩阵,分别记录能流到两个大洋的坐标

  • 从海岸线,多管齐下,同时深度遍历图,过程中填充上述矩阵。

  • 遍历两个矩阵,找到流到两个大洋坐标

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. 克隆图

题目链接:133. 克隆图 - 力扣(LeetCode) (leetcode-cn.com)")

思路:

  • 拷贝所有节点

  • 拷贝所有的边

解题思路

  • 深度或者广度优先遍历所有的节点

  • 拷贝所有的节点,存储起来

  • 将拷贝的节点,按照原图的方法进行连接

图的深度优先遍历:

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常用数组表示堆

image-20211020161001192.png

  • 左侧子节点的位置是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个最大元素。

题目链接:215. 数组中的第K个最大元素 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 看到“第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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
叉树`.

  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。

  • js常用数组表示堆

image-20211020161001192.png

  • 左侧子节点的位置是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个最大元素。

题目链接:215. 数组中的第K个最大元素 - 力扣(LeetCode) (leetcode-cn.com)")

思路

  • 看到“第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开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值