重点关注
数据结构与算法的特点、应用场景、JS实现、时间/ 空间复杂度。
刷题顺序
较为科学的刷题方式,推荐按照类型刷题,相当于集中训练。
数据结构与算法简介
数据结构:计算机存储、组织数据的方式
程序= 数据结构 + 算法
栈、队列、链表: 有序的(羊肉串);
集合、字典:无序的(米饭);
树、堆、图:公司组织架构、道路
学习的算法
- 【链表】遍历链表、删除链表节点。
- 【树、图】深度dfs/广度bfs优先遍历。
- 【数组】冒泡/选择/插入/归并/快速排序、顺序/二分搜索。
时间复杂度的计算
时间复杂度是一个函数,用大O来表示,定性描述该算法的运行时间。
定性:
时间复杂度相加是忽略小的
O(1) + O(n) = O(n) 取时间复杂度大的,因为小的可以忽略。
时间复杂度相乘则不能忽略
嵌套的双层for循环 O(n) * O(n) = O(n^2)
时间复杂度 O(logN) 代表2的多少次方为N
log8 = 3
空间复杂度的计算
空间复杂度也是一个函数,也是用大O来表示的,常见的空间复杂度 O(1) O(n) O(n^2)
算法在运行过程中临时占用存储空间大小的量度,说直白点就是你写的代码占用存储空间大小多不多
let i =1 ; i += 1; // O(1) 只声明一个单独变量。
// O(n)
let arr = []
for(i = 0; i < 10; i++) {
arr.push(i)
}
// O(n^2) 其实就是一个矩阵 行列,本质就是一个二维数组
数据结构之-“栈” stack
栈就是一个后进先出,先进后出的数据结构,现实中的例子就是蜂窝煤,拿的时候只能先拿最上面的,push(入栈)、pop(出栈),javascript可以通过Array实现所有栈的功能
什么场景下用“栈”?
- 需要后进先出的场景(十进制转二进制、判断括号是否有效、函数调用堆栈),我们都可以用“栈”来解决问题
为什么十进制要用到“栈”?
因为我们拿到的余数最后要倒序输出,比如35的二进制就是100011
function decimalismToBinary(num){
let stack = []
while(num) {
stack.push(num % 2);
num = Math.floor( num / 2 );
}
return stack.reverse()
}
// reverse反转数组,返回新的数组,原数组也会被改变
JS中函数的调用堆栈也是典型的后进先出的,背后的JS解析器就是用的栈的数据结构来控制函数的调用顺序,使它能够满足后进先出的顺序。
如下图就是执行到function3的时候的调用堆栈
数据结构之-“队列” queue
是一个先进先出,就像水管里面的水一样,与栈是相反的。javaScript没有队列,但是我们可以通过Array实现队列的所有功能。
模拟队列的先进先出需要使用Array的shift方法
let arr = [];
arr.push(1);
arr.push(2);
let item = arr.shift(); // item = 1 arr = [2]
什么场景使用到队列
所有符合先进先出的场景都可以考虑使用队列的数据结构进行解决。先进先出,保证有序。
场景
【食堂打饭,谁先排第一位,谁先打饭就走】、
【JS异步中的任务队列,比如遇见微任务先放到微任务队列,等主进程执行完之后去执行微任务队列的时候,先执行第一个微任务】
【计算最近请求次数】是一道算法题
数据结构之-“链表”
链表:多个多素组成的列表,元素存储不连续,用next指针连在一起。
链表 vs 数组:链表其实和数组比较像,那么为什么还要用链表?
原因是数组是连续的,增删非首尾的元素时候往往要移动元素,但是链表增删非首尾的元素,不需要移动元素,只需要改变next的指向即可。
JS中的链表
JavaScript是没有链表的数据结构,但是我们可以用Object来模拟一个链表。
JavaScript实现链表
let a = { value: 'a' }
let b = { value: 'b' }
let c = { value: 'c' }
let d = { value: 'd' }
a.next = b
b.next = c
c.next = d
// p 就是一个指针
let p = a
// 遍历链表
while (p) {
console.log('faith=============', p.value)
p = p.next
}
// 插入链表
let w = { value: 'w' }
let temp = b.next
b.next = w
w.next = temp
// 删除 w
let temp = w.next
b.next = temp
Js中的原型链
原型链本质上也是链表结构
arr.__proto__.__proto__.__proto__
arr.__proto__ // Array prototype
arr.__proto__.__proto__ // Object prototype
arr.__proto__.__proto__.__proto__ // null
使用链表指针获取JSON的节点值
const json = {
a: { b: { c: 100}},
d: {e: 200}
}
const path = ['a', 'b', 'c']
// 怎么通过链表的思想来获取 a.b.c ?
let p1 = json;
// 模拟链表循环
path.forEach((item) => {
p1 = p1[item]
})
console.log(p1) // 100
总结
链表里的元素存储不是连续的,是通过next进行连接的。
JavaScript是没有链表的数据结构,但是我们可以通过Object模拟链表。
Js中的原型链也是一个链表,只不过不是通过next连接,而是通过 proto
集合
一种无序且唯一的数据结构、ES6中有集合,Set
集合中常用的操作:去重、判断某元素是否在集合中、求交集
// 去重
let arr = [1, 1, 2]
let arr2 = [...new Set(arr)]
// 判断元素是否在集合中
let arr = [1, 2]
let set = new Set(arr)
set.has(1) // true
set.has(5) // false
使用ES6的Set
1、new add delete has size
2、迭代Set:多重迭代方法、Set与Array互换、求交集/差集
- 使用for of 迭代集合
let set = new Set([1, 2, 3,4])
// 直接 item of set
for(let item of set) {
console.log(item)
}
// item of set.keys()
for(let item of set.keys()) {
console.log(item)
}
// item of set.values()
for(let item of set.values()) {
console.log(item)
}
set.forEach(item => {
console.log(item)
})
for(let [k, v] of set.entries()) {
console.log(k, v) // key value 都是一样
}
// Set与Array互换
Array.from(set)、[...set]
// 差集 和交集比只是判断条件是非 (A差B + B差A = 补集)
[... new Set( nums2.filter((item) => !set.has(item)) )]
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
// 通过字典来实现
let map = new Map()
nums1.forEach((item) => {
map.set(item, true)
})
let res = []
nums2.forEach((item) => {
if(map.get(item)) {
res.push(item)
map.delete(item)
}
})
return res
};
总结
1、无序且唯一
2、ES6内是有集合的,它的名字是Set
3、集合的常用操作:去重、判断某元素是否在集合中、求交集/差集/补集
编码
20.有效的括号 LeetCode
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
// "{[]}"
if(s.length % 2 !== 0) {
return false;
}
let stack = [];
let map = new Map();
map.set(')', '(');
map.set('}', '{');
map.set(']', '[')
for(let i = 0; i < s.length; i++) {
if(['(', '{', '['].includes(s[i])) {
stack.push(s[i])
} else {
if(map.get(s[i]) !== stack.pop()) {
return false
}
}
}
return stack.length === 0 ? true : false;
};
237.LeetCode 删除链表中的节点
// 输入:head = [4,5,1,9], node = 5
// 输出:[4,1,9]
// 时间复杂度O(1) 空间复杂度O(1)
var deleteNode = function(node) {
node.val = node.next.val; // 如果要删除5,那么将5改成1(新)
node.next = node.next.next // 然后再删除1(久)
};
// ```jsx
// 输入:head = [4,5,1,9], node = 5
// 输出:[4,1,9]
// 时间复杂度O(1) 空间复杂度O(1)
var deleteNode = function(node) {
node.val = node.next.val; // 如果要删除5,那么将5改成1(新)
node.next = node.next.next // 然后再删除1(久)
};
// 总结: [4, 5, 1, 9] 如果删除5,当前node节点是5,我们无法拿到上个 node.next,那么就把1借过来给5,相当于删除1 [4, 1, 9]
206.反转链表 LeetCode
双指针 p1=head p2=null 基于2个节点的反转思想 p1走一步,p2跟着p1屁股走一步 返回p2
时间复杂度 O(n) 空间复杂度O(1)
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
// [1, 2, 3, 4]
//p2 = null p1
// p2 p1
// p2 p1
let p1 = head, p2 = null;
while(p1) {
// 获取临时
let temp = p1.next;
// 先反转 再往前
p1.next = p2; // 一定要先反转,这块要注意
// 反转之后再往前
p2 = p1; // p2往前走
p1 = temp; // p1往前走
}
return p2
};
2.两数相加 LeetCode
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
// 时间复杂度 O(n) 空间复杂度O(n)
var addTwoNumbers = function(l1, l2) {
// 创建输出链表
let l3 = new ListNode(0)
// 遍历链表 3个指针
let p1 = l1, p2 = l2, p3 = l3;
let carry = 0; // 进位
// 遍历 l1 l2
while(p1 || p2) { // 有可能长度不一样
// 第一次循环伪代码
// p1.val + p2.val == 7
// fllor (7 / 10 ) = 0
// p3.next = new new ListNode(7)
// p1 = p1.next
// p2 = p2.next
// p3 = p3.next
// 第二次循环伪代码
// p1.val + p2.val + fllor (7 / 10 )
// floor (p1.val + p2.val + fllor (7 / 10 ) / 10) =
let value = (p1 ? p1.val : 0) + (p2 ? p2.val : 0) + carry;
// 保留10进位的数下次相加
carry = Math.floor(value / 10)
// 当前链表取个位数
p3.next = new ListNode(value % 10)
p1 = p1 && p1.next
p2 = p2 && p2.next
p3 = p3.next
}
// 这个是为了处理 [1,1,9] [8,8,8] 循环了3次 [9, 9, 7] 此刻carry是1但是没有被循环到
if(carry) {
p3.next = new ListNode(carry)
}
return l3.next
};
// 简化一下
var addTwoNumbers = function(l1, l2) {
// 创建输出链表
let l3 = new ListNode(0)
// 遍历链表 3个指针
let p1 = l1, p2 = l2, p3 = l3;
let carry = 0; // 进位
// 遍历 l1 l2
while(p1 || p2) { // 有可能长度不一样
let v1 = (p1 ? p1.val : 0);
let v2 = (p2 ? p2.val : 0);
let value = v1 + v2 + carry;
// 保留10进位的数下次相加
carry = Math.floor(value / 10)
// 当前链表取个位数
p3.next = new ListNode(value % 10)
p1 = p1 && p1.next
p2 = p2 && p2.next
p3 = p3.next
}
// 这个是为了处理 [1,1,9] [8,8,8] 循环了3次 [9, 9, 7] 此刻carry是1但是没有被循环到
if(carry) {
p3.next = new ListNode(carry)
}
return l3.next
};
83.删除排序链表中的重复元素 LeetCode
需要用到:遍历链表、删除链表的节点
时间复杂度 O(n) 空间复杂度O(1)
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
// 创建一个指针 p1
// 判断当前节点val与下个节点的val是否一样,如果一样删除下个节点,指针不用动 [1, 1, 2] >> [1, 2]
// p1 p1
// 如果不一样 [1, 2, 3] 不删除,只移动指针
// p1
let p1 = head;
while(p1 && p1.next) {
if(p1.val === p1.next.val) {
p1.next = p1.next.next
} else {
p1 = p1.next
}
}
return head
// 总结,遍历链表,相同删除不移动指针,不相同移动指针
};
141.环形链表 LeetCode
用一快一慢的指针遍历链表,如果指针能够相逢就返回true
就像一个圆形操场,一个跑的慢(慢指针)一个快(快指针),跑的快的肯定会与跑的慢的相遇。
时间复杂度 O(n) 空间复杂度O(1)
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
let p1 = head, p2 = head;
while(p1 && p2 && p2.next) {
p1 = p1.next
p2 = p2.next.next
if(p1 === p2) {
return true
}
}
return false
};
349.两个数组的交集 LeetCode
时间复杂度 O(n^2) filter * has
空间复杂度 O(n)
总结:需要用到集合的has方法与数组的filter方法
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
* 交集-就要使用Set集合
*/
var intersection = function(nums1, nums2) {
// [1, 2, 2, 1] [2, 2]
let set = new Set(nums1)
return [... new Set( nums2.filter((item) => set.has(item)) )]
};
通过字典Map的方式来实现两个数组的交集
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
// 通过字典来实现
let map = new Map()
nums1.forEach((item) => {
map.set(item, true)
})
let res = []
nums2.forEach((item) => {
if(map.get(item)) {
res.push(item)
map.delete(item)
}
})
return res
};