0x01 基本概念
Array 数组
数组存储一系列同一类型的数值,但是在 JavaScript 里面可以存储不同类型的数值,但还是要遵守最佳实践(因为大多数语言的数组都不能存储多种类型)。
创建和初始化
// 方法一:使用 new 关键字
var week = new Array();
var week = new Array(7); // 创建一个长度为 7 的空数组
var week = new Array('Sunday', 'Monday');
// 方法二:建议直接使用 [] 来创建一个数组
var week = [];
var week = ['Sunday', 'Monday'];
访问
直接使用中括号传递数值,如 week[1]
插入
使用索引为 2 的位置插入字符串 g,对应索引为 2,3,4 的元素需要后移。所以数组中插入元素的时间复杂度为 O(n)
// 一、在尾部添加
var numbers = [1,2,3,4];
numbers[numbers.length] = 5;
numbers.push(6)
// output: [1,2,3,4,5,6]
// 二、在头部添加
numbers.unshift(0)
// output: [0, 1,2,3,4,5,6]
// 三、任意位置添加
numbers.splice(2, 0, 222); // MDN - Array.prototype.shift
// output:[0, 1, 222, 2, 3, 4, 5]
删除
// 一、头部删除
numbers.shift() // MDN - Array.prototype.shift()
// output:[, 1, 222, 2, 3, 4, 5]
// 二、尾部删除
numbers.pop() // MDN - Array.prototype.pop()
// output:[1, 222, 2, 3, 4, ]
// 三、任意位置删除
numbers.splice(1, 1); // MDN - Array.prototype.splice()
// output:[1, 2, 3, 4]
其他方法
除了刚才数组操作必备的 push、pop、shift、unshift、splice 之外,其他数组相关的方法也需要熟悉:
- concat:连接 2 个或更多数组
- 迭代相关:every、some、forEach 、filter、map、reduce
- 与字符串间的转换相关:join、toString、valueOf
- 搜索:indexOf、lastIndexOf
- 顺序改变:reverse、sort
ES6 & ES7 新增 (参考:BABEL-ES2015)
- copyWithin
- entries
- find
- findIndex
- fill
- from
- keys
- of
- values
- includes
Linked List 链表
常见的链表
- singly-Linked List 单链表
- doubly-Linked List 双链表
- 循环链表
- 双向循环链表
- ...
单链表
示意图:
使用 JavaScript 实现一个单链表的大致骨架:
// 单链表节点
class Node {
constructor(value, next){
this.value = (value === undefined ? 0 : value);
this.next = (next === undefined ? null : next);
}
}
class SinglyLinkedList {
constructor() {
this.head = null; // 头虚拟节点
this.size = 0; // 链表元素数
}
// 其他方法 - 可自己实现
append(element){}; // 尾部添加
insert(positon, element){}; // 指定位置添加
remove(element){}; // 移除
removeAt(positon){}; // 制定位置移除
isEmpty(){}; // 是否为空
indexOf(){}; // 返回索引,如果没有返回 -1
size(){}; // 返回链表中包含的元素个数
...
}
访问
相对于数组类型,链表的好处在于,添加或者删除元素的时候,不需要移动其他元素。数组的另一个细节是可以直接任何位置的任何元素,而链表访问一个中间元素,需要从起点(HEAD 表头)开始迭代,直到找到所需元素,如访问链表中的第一个元素可以表示为 HEAD.next
,第二个元素可以表示为 HEAD.next.next
(如下图)。
- 访问链表中第一个元素,为最优时间复杂度 O(1),只进行一次查询;
- 访问链表中的最后一个元素,为最差时间复杂度为 O(n),需要迭代整个链表;
- 访问链表中间的元素,为平均时间复杂度为 O(n/2),去掉常数部分为 O(n);
插入
在 prev
节点 和 current
节点中间插入一个新的 node
的节点,可以这样做:
- 第一步:使
node.next
的值指向current
节点 - 第二步:使
prev.next
的值设为node
节点
这样链表中就多了一个节点,进行 2 次操作,时间复杂度为 O(2) ≈ O(1)
删除
如删除 prev
节点和 current
节点中间的节点,可以这样做:
- 使
prev
的next
指针指向 current 节点
即可完成对中间节点的删除,时间复杂度为 O(1)
双链表
/**
* 双链表骨架
* 案例:yallist: https://github.com/isaacs/yallist/blob/1649cc57394b5affeca2c573943ebe3ed7d39119/yallist.js#L7
*/
// 双链表节点
class Node {
constructor(value, next, prev){
this.value = value;
this.next = (next === undefined ? null : next);
this.prev = (prev === undefined ? null : prev); // 比单链表新增
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null; // 比单链表新增
this.size = 0;
}
// 其他方法 - 参照 yallist: https://github.com/isaacs/yallist/blob/1649cc57394b5affeca2c573943ebe3ed7d39119/yallist.js#L7 实现
removeNode(){};
unshiftNode(){};
pushNode(){};
push(){};
unshift(){};
pop(){};
shift(){};
forEach(){};
forEachReverse(){};
get(){};
getReverse(){};
...
}
具体代码实现可以参考 node-lru-cache 的底层依赖,一个双链表的实现 :yallist
关于这个库的官方简介是:
For when an array would be too big, and a Map can't be iterated in reverse order
循环链表
循环链表与单链表唯一的区别在于:最后一个元素指向下一个节点不是 null,而是第一个节点。
双向循环链表
双向循环链表和双链表区别在于:HEAD
节点的 prev
指针之前 TAIL
,TAIL
节点的 next
指针指向 HEAD
数组与链表时间复杂度对比
下方表格中只表示平均情况下 Array 和 Linked List 时间复杂度对比情况
数据结构 | Access - AVG | Search - AVG | Insertion - AVG | Deletion - AVG |
---|---|---|---|---|
Array 数组 | O(1) | O(n) | O(n) | O(n) |
Linked List 链表 | O(n) | O(n) | O(1) | O(1) |
0x02 使用场景简介
- LRU cache - 双链表:yallist 源码
- React Dom Tree Diff - 单链表: 源码
- Redis 中的链表类型 - 双链表
0x03 算法分析
第一题:Array 删除数组中重复的项
💡 考察的点
- 前面一直再提时间复杂度,本题主要考察对 空间复杂度 ****的理解;
- 熟练使用 JavaScript 数组提供的 splice 方法,进行原地插入、删除、添加操作;
- 理解的双指针法;
题目:给你一个有序数组
nums
,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4] 思路:
- 如果新增数组,或利用 Map、Set 等特性来对数组进行去重,会引入额外的空间复杂度,不满足题目需求,只能在原数组基础上进行操作;
- 【利用 JavaScript 语言特性】Array 提供的 splice 方法,可以对原数组进行操作,并返回原数组;
- 【通用方法】使用双指针法;
方法一:使用 splice 方法,进行原地删除
var removeDuplicates = function(nums) {
let i = 0;
while(i < nums.length){
if(nums[i] == nums[i+1]) {
nums.splice(i+1 ,1); // 在i+1的位置删除一个元素
} else {
i++
}
}
}
// 时间复杂度:O(n)
// 空间复杂度:O(1)
方法二:双指针法
思路:
- 初始化两个指针:i = 0 ; j = 1
- 每次循环,比较 i,j 对应的元素
- 如果相同:则 i 不变,j 往后移动一位
- 如果不同:则 i + 1 位置的元素等于 j 当前位置的元素,然后 i + 1 , j + 1
- j=5,超出数组长度,停止遍历
var removeDuplicates = function(nums) {
let i = 0, j = 1;
while(j < nums.length){
if(nums[i] !== nums[j]){
nums[i + 1] = nums[j]
i++;
}
j++;
}
return i+1;
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)
第二题:Array 两数之和
💡 考察的点
- 时间复杂度的优化
- 熟悉 Map 的特性
- 可以用空间复杂度来换取时间复杂度的优化
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
题目及答案地址:两数之和 - 力扣(LeetCode)
方法一:暴力破解 思路:两层循环,逐个遍历,缺点是时间复杂度高。
for (i = 0; i < length; i++){
for (j = i + 1; j < length; j++){
if(nums[i] + nums[j] === target) return [i , j]
}
}
// 时间复杂度 O(n^2)
// 空间复杂度 O(1)
方法二:利用 Map 的特性 减少一次循环,时间复杂度由 O(n^2) 降低为 O(n),但空间复杂度由于多了 Map 的开销由 O(1) 增加为 O(n) 思路:
target - nums[i]
存入 Map 中- 第一次循环
- 判断 Map 中是否存在 2
- 不存在则:target=9,nums[i]=2,把 9-2=7 存入 Map中
- 判断 Map 中是否存在 2
- 第二次循环
- 判断 Map 中是否存在 7
- 存在,则返回下标
- 判断 Map 中是否存在 7
- 第一次循环
let myMap = new Map();
for (i = 0; i < length; i++){
if(myMap.has(nums[i])){
return [myMap.get(nums[i]), i]
} else {
myMap.set(target - nums[i], i);
}
}
// 时间复杂度 O(n)
// 空间复杂度 O(n)
第三题:Array 数组的交集
💡 考察的点
- JS Array API 的熟悉程度
- 熟悉 Set 的特性 本地有很多种解决办法,不小于 10 种,下面对众多方法进行分类。
第一类:【通用方法】排序 + 双指针 只需要将 2 个数组先进行 sort 方法进行排序,使用第一题的双指针方法,去掉重复的元素即可。
第二类:【利用 JavaScript 语言特性】 1、使用 Set 中值不重复的特性
var intersection = function(nums1, nums2) {
return Array.from(new Set(nums1.filter(v => nums2.includes(v))))
};
2、使用 includes
判断是否存在。lastIndexOf
判断是否重复
var intersection = function(nums1, nums2) {
return nums1.filter((v, i) => nums2.includes(v) && nums1.lastIndexOf(v) === i)
};
3、使用 indexOf
判断是否存在。lastIndexOf
判断是否重复
var intersection = function(nums1, nums2) {
return nums1.filter((v, i) => nums2.indexOf(v) > -1 && nums1.lastIndexOf(v) === i)
};
4、 等等
总结
- 可以牺牲一定的空间复杂度来对时间复杂度进行提速
- 通用方法:双指针法的应用
- 数组去重
- 链表找环
- 语言特性也可以解决很多问题,日wj常开发中,对 JavaScript 数组的方法一定要熟记,甚至可以做到不查文档知道所有常用 API 及参数
0x04 推荐工具
- 手绘风格 - 画图工具 (文稿中的图都是用这个工具绘制的)
- Big-O 速查表
- 练习题