一、复杂度计算
时间复杂度: 一个函数,用大O表示,比如O(1),O(n),O(logN)..., 用来定性描述该算法的运行时间。
空间复杂度:一个函数,用大O表示,比如O(1),O(n),O(n²)..., 用来定性描述该算法在运行时临时占用存储空间大小的度量。
二、栈
概念
栈 是一个后进先出的数据结构。放进拿出对应的方法分别是push()、pop(), JavaScript中没有栈数据结构,但可以用Array实现栈的所有功能。
const stack = []
stack.push(1)
stack.push(2)
const item1 = stack.pop() // pop():移除一个元素并返回它
console.log(item1)
使用场景
需要后进先出的场景
如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈(js的执行上下文栈)...
十进制转二进制
判断字符串的括号是否有效
函数调用堆栈
题目
20. 有效的括号
利用栈的思想,后进先出:
- 1.新建一个栈(用数组模拟栈)
- 2.扫描字符串,遇到左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配直接判定为不合法返回false
- 3.最后检查栈空了就合法,否则不合法
- 如果字符串长度为奇数,直接返回false
var isValid = function(s) {
if(s.length % 2 !== 0) {
return false
}
let stack = []
for (let i = 0; i <s.length; i++) {
if(s[i] === '(' || s[i] === '[' || s[i] === '{') {
stack.push(s[i]);
} else {
const top = stack[stack.length - 1]; // 获取栈顶元素
// 因为括号种类不多,直接使用暴力枚举
if(
(top === '(' && s[i] === ')') ||
(top === '[' && s[i] === ']') ||
(top === '{' && s[i] === '}')){
stack.pop(stack.length - 1)
} else {
return false
}
}
}
return stack.length === 0
}
时间复杂度:O(n) ,空间复杂度:O(n)
本题可利用字典来优化。
二、队列
概念
队列 是一个先进先出的数据结构。放进拿出对应的方法分别是push()、shift(), JavaScript中没有队列数据结构,但可以用Array实现队列的所有功能。
const queue = [];
queue.push(1);
queue.push(2);
const item1 = queue.shift();
const item2 = queue.shift();
使用场景
需要先进先出的场景
如:食堂排队打饭、JS异步中的任务队列、计算最近请求次数... JS异步中的任务队列
计算最近请求次数
题目
933. 最近的请求次数
队列思想:
- 1.有新请求就入队,3000ms前发出的请求出队
- 2.队列的长度就是最近请求次数
var RecentCounter = function() {
this.q = [];
};
/**
* @param {number} t
* @return {number}
*/
RecentCounter.prototype.ping = function(t) {
this.q.push(t);
while(this.q[0] < t - 3000){
this.q.shift();
}
return this.q.length;
};
时间复杂度:O(n) ,空间复杂度:O(n)
三、链表
概念
链表是由多个元素组成的列表。元素存储不连续,用next指针连在一起。JavaScript中没有链表数据结构,但可以用Object模拟链表。
数组VS链表
数组:增删非首尾元素时,往往需要移动数组里面的元素。
链表:增删非首尾元素时,不需要移动元素,仅需要改动next指针。
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 (p) {
console.log(p.val);
p = p.next;
}
// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;
// 删除
c.next = d;
题目
237. 删除链表中的节点
链表思想(js中使用object实现):无法直接获取被删除节点的上一个节点,那么将被删除的节点转移到下一个节点(即 交换值)。
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
时间复杂度O(1), 空间复杂度O(1)
206. 反转链表
反转两个节点:将n+1的next指向n
反转多个节点:双指针遍历链表,重复上述操作。
步骤:
- 1.双指针一前一后遍历链表
- 2.反转双指针(改变next指针的指向)
var reverseList = function(head) {
let p1 = head,
p2 = null
while(p1) {
const temp = p1.next
p1.next = p2
p2 = p1
p1 = temp
}
return p2
};
时间复杂度O(n), 空间复杂度O(1)
2. 两数相加
遍历链表
- 1.新建一个空链表
- 2.遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加。
var addTwoNumbers = function(l1, l2) {
let l3 = new ListNode(0) // 题目中定义的链表结构, 新建空链表
let p1 = l1, p2 = l2, p3 = l3, carry = 0
while(p1 || p2) {
let v1 = p1 ? p1.val : 0
let v2 = p2 ? p2.val : 0
let value = v1 + v2 + carry
carry = Math.floor(value / 10)
p3.next = new ListNode(value % 10)
if(p1) p1 = p1.next
if(p2) p2 = p2.next
p3 = p3.next
}
if(carry) p3.next = new ListNode(carry)
return l3.next
};
时间复杂度 O(n), 空间复杂度O(n)
83. 删除排序链表中的重复元素
双指针:
- 1.因为链表有序, 所以重复元素一定相邻
- 2.遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值(改变next指向)
- 3.遍历结束后,返回原链表头部
var deleteDuplicates = function(head) {
let p = head;
while(p && p.next) {
if(p.val === p.next.val) {
p.next = p.next.next; // 如果p节点与它的下一个节点的值相等 就删除下一个节点(更改next指向便可以实现删除)
}else{ // 思考为什么要放在else里面
p = p.next; // 当遇见新的值时 才移动p指针
}
}
return head;
};
时间复杂度O(n),空间复杂度O(1)
141. 环形链表
快慢指针思想:
- 1.用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有环,返回true
- 2.遍历结束,还没相逢就返回false
p1 = p1.next; // 慢指针一次走一步p2 = p2.next.next; // 快指针一次走两步 注意保证p2.next有值
var hasCycle = function(head) {
if(!head) return false
let p1 = head, p2 = head
while(p1&&p2&&p2.next) {
p1 = p1.next
p2 = p2.next.next // 注意,不能形成null.next的情况,这样会出错,所以需要控制p2.next不为空
if(p1 === p2) {
return true
}
}
return false
};
时间复杂度 O(n) ,空间复杂度O(1)
原型链
var foo = {},
F = function() {}
Object.prototype.a = 'Aa'
Function.prototype.b = 'Bb'
console.log(foo.a)
console.log(foo.b)
console.log(F.a)
console.log(F.b)
// Aa
// undefined
// Aa
// Bb
使用链表指针获取JSON的节点值
const json = {
a: { b: { c: 1 } },
d: { e: 2 },
};
const path = ['a', 'b', 'c'];
let p = json;
path.forEach((k) => {
p=p[k];
});
四、集合
概念
集合是一种无序且唯一的数据结构。ES6中有集合数据结构:Set,集合常用操作:去重、判断某元素是否在集合中、求交集等。
// 去重
const arr = [1, 1, 2, 2];
const arr2 = [...new Set(arr)];
// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);
// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));
题目
349. 两个数组的交集
使用集合的思想: 求交集,无序且唯一, 所以 , 可以先对某一个数组nums1进行去重处理,然后再遍历nums1,求交集筛选出num2也包含的值,都可使用Set数据结构
return [...new Set(nums1)].filter(item => new Set(nums2).has(item));
这种方法的时间复杂度和空间复杂度高,表现不优秀,其实没必要将nums2变成集合, 使用数组原生的方法includes判断可节省时间空间
var intersection = function(nums1, nums2) {
// return [...new Set(nums1)].filter(item => new Set(nums2).has(item));
return [...new Set(nums1)].filter(item => nums2.includes(item));
};
时间复杂度:O(n²) ,因为嵌套filter函数及includes函数
空间复杂度:O(n)
还可以使用Map字典来解决 ,更优
使用ES6的Set
Set对象常用方法:new、add()、delete()、has()、size 迭代Set:多种迭代方法、Set与Array互转、求交集/差集
let mySet = new Set();
// 添加
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 }); // 引用数据类型,只是表面属性名/值一样,可以添加
// 判断是否含有某元素
const has = mySet.has(o);
// 删除某元素
mySet.delete(5);
// 迭代
// for...of直接遍历,.keys(), .values(), .entries(), forEach()方法都可以迭代
for(let [key, value] of mySet.entries()) console.log(key, value);
// 转为数组
const myArr = Array.from(mySet);
// 转为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)));
五、字典
概念
与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式存储。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')
题目
349. 两个数组的交集
用字典建立一个映射关系,记录num1里面有的值,遍历num2,找出nums1里面也有的值。
- 1.新建一个字典,遍历nums1,填充字典。
- 2.遍历nums2,遇到字典里的值就选出,加入结果数组,并从字典中删除(防止重复添加)
var intersection = function(nums1, nums2) {
const map = new Map()
nums1.forEach(n => {
map.set(n, true)
})
const res = []
nums2.forEach(n => {
if(map.has(n)) {
res.push(n)
map.delete(n)
}
})
return res
};
时间复杂度:O(m+n) ,空间复杂度:O(m)
20. 有效的括号
利用字典的思想:
- 1.新建一个字典
- 2.把括号存在字典里,左括号为key,右括号为value
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++) {
const c = s[i]
if (map.has(c)) {
stack.push(s[i])
} else {
const top = stack[stack.length - 1]
if (map.get(t) === c) {
stack.pop()
} else {
return false
}
}
}
return stack.length === 0
}
时间复杂度:O(n) ,空间复杂度:O(n)
1. 两数之和
把nums想象成相亲者,把target想象成匹配条件,用字典建立一个婚姻介绍所, 存储相亲者的数字和下标
步骤:
- 1、新建一个字典作为婚姻介绍所
- 2、nums里的值,逐个来介绍所找对象,没有合适的就先登记着,有合适的就牵手成功
var twoSum = function(nums, target) {
const map = new Map();
for (let i = 0; i<nums.length; i++) {
const n = nums[i];
const n2 = target - n;
if(map.has(n2)) {
return [map.get(n2), i] // 获取对象(key)的下标(value)以及自己的下标
} else{
map.set(n, i) // 否则就登记自己的信息 值(key)+下标(value)
}
}
};
时间复杂度O(n),空间复杂度O(n)
优化: 可以用时间换空间的方式 来优化空间复杂度, 思路是 利用二分查找。
3. 无重复字符的最长子串
思想: 先找出所有的不包含重复字符的子串,找出长度最大的那个字串 返回其长度即可。
步骤:
- 1、 用双指针维护一个滑动窗口,用来剪切子串
- 2、 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位
- 3、过程中,记录所有窗口的长度,并返回最大值
注意控制下标是否包含在滑动窗口里的条件。
var lengthOfLongestSubstring = function(s) {
let l = 0;
let res = 0; // 用来记录最大窗口长度
const map = new Map(); // 用来记录是否为重复字符
for (let r = 0; r < s.length;r += 1) {
if(map.has(s[r]) && map.get(s[r]) >= l) { // map.get(s[r]) >= l控制右指针在l的右边或等于l,否则就不移动左指针
l = map.get(s[r]) + 1; // 移动下标
}
res = Math.max(res, r-l+1); // 历史的长度与当前ls长度差比较,取较大的一个
map.set(s[r], r); // 记录右指针指向的字符和下标
}
return res
};
时间复杂度O(n),空间复杂度O(m), m为字符中不重复字符的个数
76. 最小覆盖子串
思路: 先找出所有的包含T的子串,找出长度最小那个字串,返回即可
步骤:
- 1、用双指针维护一个滑动窗口,用来枚举所有子串
- 2、移动右指针,找到包含T的子串,移动左指针,尽量减少包含T的子串的长度(右指针用以寻找,左指针用以减少子串长度)
var minWindow = function(s, t) {
let l = 0;
let r = 0;
const need = new Map();
for(let c of t){ // 遍历t
need.set(c, need.has(c)? need.get(c) + 1 : 1); // 每一个字符(key)及字符的个数(value),如‘ABBC’ ,‘A’=>1,'B'=>2,'C'=>1
}
let needType = need.size;// 需要的类型数量
let res = ''; // 用来存放结果字符串
while(r < s.length) {
const d = s[r];
if (need.has(d)) {
need.set(d, need.get(d)-1); // 如果字典里有这样的字符,就将它需要的数量-1
if (need.get(d) === 0) needType -= 1; // 如果字典里此字符的数量为0,则需要类型的数量就减少1
}
while (needType === 0){ // 如果已经包含了t,那么就使用l寻找最小的包含t的子串
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); // 因为l需要左移,而如果要跳过的是我们需要的字符,则需要将其记录在字典里,将它的需求+1
if (need.get(c2) === 1) needType += 1; // 如果needType不为0,则会跳出循环,使用r去寻找需求字符
}
l += 1;
}
r += 1;
}
return res;
};
时间复杂度O(m+n),空间复杂度O(m),m是t里面不同字符的个数
六、树
概念
树是一种分层数据的抽象模型。前端工作中常见的树包括:Dom树、级联选择、树形控件等。
JS中没有树数据结构,可以使用Object和Array来构建树。
树的常用操作:深度/广度优先遍历、二叉树的先中后序遍历。
深度/广度优先遍历
深度优先遍历:1. 访问根节点 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(dfs);
};
dfs(tree);
广度优先遍历:1.新建一个队列,把根节点入队 2. 把队头出队并访问 3. 把队头的children挨个入队 4. 重复第二、三步,直到队列为空
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);
二叉树的先中后序遍历
二叉树中每个节点最多只能有两个子节点,在JS中通常用Object来模拟二叉树。
先序遍历:1. 访问根节点 2. 对根节点的左子树进行先序遍历 3. 对根节点的右子树进行先序遍历 (根 ->左 -> 右)
// bt.js
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;
// preorder.js
const bt = require('./bt');
const preorder = (root) => {
if (!root) { return; }
console.log(root.val);
preorder(root.left);
preorder(root.right);
};
// 非递归版:首先访问根节点 再分别往栈中放入左右子树
// 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);
// }
// };
preorder(bt);
中序遍历:1. 对根节点的左子树进行中序遍历 2. 访问根节点 3. 对根节点的右子树进行中序遍历 (左 ->根 -> 右)
const bt = require('./bt');
// 递归版
const inorder = (root) => {
if (!root) { return; }
inorder(root.left);
console.log(root.val);
inorder(root.right);
};
// 非递归版:首先把所有左子树都放入栈
// const inorder = (root) => {
// if (!root) { return; }
// const stack = [];
// let p = root;
// while (stack.length || p) {
// while (p) {
// stack.push(p);
// p = p.left;
// }
// const n = stack.pop(); // 弹出栈顶
// console.log(n.val);
// p = n.right;
// }
// };
inorder(bt);
后序遍历:1. 对根节点的左子树进行后序遍历 2. 对根节点的右子树进行后序遍历 3. 访问根节点(左 -> 右 -> 根)
const bt = require('./bt');
const postorder = (root) => {
if (!root) { return; }
postorder(root.left);
postorder(root.right);
console.log(root.val);
};
// 非递归版:改造先序遍历,利用栈的先进后出特性
// const postorder = (root) => {
// 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);
// }
// };
postorder(bt);
题目
104. 二叉树的最大深度
思路:
步骤:
- 1、 新建一个变量,记录最大深度
- 2、 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
- 3、遍历结束返回最大深度这个变量
var maxDepth = function(root) {
let res = 0; //记录深度
const dfs = (n, level) => {
if (!n) return;
// console.log(n.val, level);
if (!n.left && !n.right) {
res = Math.max(res, level); // 在叶子节点处才刷新深度 减少不必要的刷新
}
dfs(n.left, level+1);
dfs(n.right, level+1);
};
dfs(root, 1);
return res;
};
时间复杂度O(n),空间复杂度O(logn)
111. 二叉树的最小深度
思路: 求最小深度,考虑使用广度优先遍历。在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级。
Step1:广度优先遍历整棵树,并记录每个节点的层级
Step2:遇到叶子节点,返回节点层级 ,停止遍历。
var minDepth = function(root) {
// 遇到叶子节点便返回 后续不必再遍历
if(!root) {return 0;}
const queen = [[root,1]];
while (queen.length){
const [n, level] = queen.shift();
if(!n.left && !n.right){
return level;
}
if (n.left) queen.push([n.left, level+1]);
if (n.right) queen.push([n.right, level+1]);
}
};
时间复杂度O(n) , 空间复杂度O(n)
102. 二叉树的层序遍历
层序即广度优先遍历,区别是,层序遍历时需要记录当前节点所在的层级,方便将其添加到不同的数组中。(广度不知道所处层级)
Step1:广度优先遍历二叉树
Step2:遍历过程中,记录每个节点的层级,并将其添加到不同的数组中
// 方式一
var levelOrder = function(root) {
if(!root) return []
const q = [[root, 0]]
let res = []
while (q.length) {
const [n, l] = q.shift()
// console.log(n.val, l)
res[l] ? res[l].push(n.val) : res.push([n.val])
if(n.left) q.push([n.left, l + 1])
if(n.right) q.push([n.right, l + 1])
}
return res
};
// 方式二
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
};
O(n)/O(n)
94. 二叉树的中序遍历
// 递归方式
var inorderTraversal = function(root) {
let res = []
const inorder = (root) => {
if (!root) { return; }
inorder(root.left);
res.push(root.val)
inorder(root.right);
};
inorder(root)
return res
};
// 迭代方式
var inorderTraversal = function(root) {
let res = []
let stack = []
let p = root
while(stack.length || p) {
while(p) {
stack.push(p)
p = p.left
}
const n = stack.pop()
res.push(n.val)
p = n.right
}
return res
};
时间复杂度O(n), 空间复杂度O(n)
112. 路径总和
思路:
var hasPathSum = function(root, targetSum) {
if (!root) return false
let res = 0
const dfs = (root, targetSum) => {
if (!root) return
if(!root.left && !root.right) {
if(targetSum === root.val) {
res = 1
}
}
dfs(root.left, targetSum - root.val)
dfs(root.right, targetSum - root.val)
}
dfs(root, targetSum)
return res
};
时间复杂度:O(n), 空间复杂度最坏O(n) ,平均O(logn) ,使用递归会调用函数调用堆栈,产生空间消耗
七、图
概念
图是网络结构的抽象模型,是一组由边连接的节点。 图可以表示任何二元关系,比如:道路、航班...
JS中没有图,可以利用Object和Array来构建图。图的表示法:邻接矩阵、邻接表、关联矩阵...
邻接矩阵:
邻接表:
图的常用操作:深度优先遍历、广度优先遍历
深度优先遍历
// graph.js
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
};
module.exports = graph;
//bfs.js
const graph = require('./graph');
const visited = new Set();
const dfs = (n) => {
console.log(n);
visited.add(n);
graph[n].forEach(c => {
if(!visited.has(c)){
dfs(c);
}
});
};
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(c)) {
q.push(c);
visited.add(c);
}
});
}
题目
65. 有效数字
步骤:
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, 'E': 4},
4: {'digit': 5, 'sign': 7},
5: {'digit': 5},
6: {'digit': 6, '.': 3, 'e': 4, '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'
}
state = graph[state][c]
if (state === undefined){
return false
}
}
if (state === 3 || state === 5 || state === 6) {
return true
}
return false
};
O(n)/O(1)
417. 太平洋大西洋水流问题
思路: 把矩阵想象成图,从海岸线逆流而上遍历图,所到之处就是可以留到某个大洋的坐标。
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)) // 构建矩阵并填充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 += 1){
dfs(r, 0, flow1)
dfs(r, n-1, flow2)
}
for (let c=0; c<=n; c += 1) {
dfs(0, c, flow1)
dfs(m-1, c, flow2)
}
// 收集能流到两个大洋的坐标
const res = []
for (let r = 0; r< m; r += 1) {
for (let c=0; c<n; c += 1) {
if (flow1[r][c] && flow2[r][c]) {
res.push([r,c])
}
}
}
return res
};
时间复杂度O(m×n) ,空间复杂度O(m×n)
133. 克隆图
思路: 拷贝所有节点;拷贝所有的边。
// 深度优先遍历
var cloneGraph = function(node) {
if (!node) return
const visited = new Map()
const dfs = (n) => {
// console.log(n.val)
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)
};
// 广度优先遍历
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)
};
O(n)/O(n)
八、堆
概念
堆是一种特殊的完全二叉树,所有的节点都大于等于(最大堆)或者小于等于(最小堆)它的子节点。
JS中使用数组表示堆。左侧子节点的位置是2 * index + 1;右侧子节点的位置是2 * index + 2;父节点位置是 (index - 1) / 2
堆的应用: 堆能高效、快速地找出最大值和最小值,时间复杂度:O(1); 找出第k个最大(小)元素。
JS实现:最小堆类
插入操作:
删除堆顶:
class MinHeap {
constructor() {
this.heap = []
}
getParentIndex(i) { // 获取父节点的位置
// return Math.floor((i-1)/2)
return (i-1) >> 1 // 位运算,二进制往右移一位
}
getLeftIndex(i) { //获取左侧子节点的位置
return i*2 + 1
}
getRightIndex(i) { //获取右侧子节点的位置
return i*2 + 2
}
swap (p, c) {
const temp = this.heap[p]
this.heap[p] = this.heap[c]
this.heap[c] = temp
}
shiftUp(index) { // 上移操作
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
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)
}
pop() {
this.heap[0] = this.heap.pop()
this.shifDown(0)
}
}
获取堆顶和堆的大小:
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
题目
215. 数组中的第K个最大元素
思路: 看到“第k个最大元素”,考虑选择使用最小堆
// 方法一:利用sort()
var findKthLargest = function(nums, k) {
function func(a,b) {
return a-b
}
var arr = nums.sort(func).reverse()
// console.log(arr)
return arr[k-1]
};
// 方法二:最小堆实现
var findKthLargest = function(nums, k) {
const h = new MinHeap()
nums.forEach(n => {
h.insert(n)
if(h.size() > k) {
h.pop()
}
})
return h.peak()
};
O(n * logk) / O(k)
347. 前 K 个高频元素
// 方法一: 使用map
var topKFrequent = function(nums, k) {
const map = new Map()
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n) + 1 : 1)
})
const list = Array.from(map).sort((a, b) => b[1] - a[1]) // 对频率降序排列
return list.slice(0,k).map(n => n[0])
};
// 时间复杂度O(nlogn)
// 方法二:最小堆
class MinHeap {
constructor() {
this.heap = []
}
getParentIndex(i) { // 获取父节点的位置
// return Math.floor((i-1)/2)
return (i-1) >> 1 // 位运算,二进制往右移一位
}
getLeftIndex(i) { //获取左侧子节点的位置
return i*2 + 1
}
getRightIndex(i) { //获取右侧子节点的位置
return i*2 + 2
}
swap (p, c) {
const temp = this.heap[p]
this.heap[p] = this.heap[c]
this.heap[c] = temp
}
shiftUp(index) { // 上移操作
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
shiftDown(index) { // 下移操作
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
insert (value) {
this.heap.push(value)
this.shiftUp(this.heap.length - 1)
}
pop() {
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
var topKFrequent = function(nums, k) {
const map = new Map()
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n) + 1 : 1)
})
const h = new MinHeap()
map.forEach((value, key) => {
h.insert({value, key})
if(h.size() > k) {
h.pop()
}
})
return h.heap.map(a => a.key)
};
// 时间复杂度O(nlogk) , 空间复杂度O(n)
23. 合并K个升序链表
思路: 新链表的下一个节点一定是k个链表头中的最小节点; 考虑选择使用最小堆
class MinHeap {
constructor() {
this.heap = []
}
getParentIndex(i) { // 获取父节点的位置
// return Math.floor((i-1)/2)
return (i-1) >> 1 // 位运算,二进制往右移一位
}
getLeftIndex(i) { //获取左侧子节点的位置
return i*2 + 1
}
getRightIndex(i) { //获取右侧子节点的位置
return i*2 + 2
}
swap (p, c) {
const temp = this.heap[p]
this.heap[p] = this.heap[c]
this.heap[c] = temp
}
shiftUp(index) { // 上移操作
if(index == 0) return
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex].val > this.heap[index].val) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
shiftDown(index) { // 下移操作
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] && this.heap[leftIndex].val < this.heap[index].val) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
insert (value) {
this.heap.push(value)
this.shiftUp(this.heap.length - 1)
}
pop() {
if(this.size() === 1) return this.heap.shift()
const top = this.heap[0]
this.heap[0] = this.heap.pop()
this.shiftDown(0)
return top
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
var mergeKLists = function(lists) {
const res = new ListNode(0)
let p = res
const h = new MinHeap()
lists.forEach(l => {
if(l) h.insert(l)
})
// console.log(h)
while(h.size()) {
const n = h.pop()
p.next = n
p = p.next
if (n.next) h.insert(n.next)
}
return res.next
};
时间复杂度O(nlogk) ,空间复杂度O(k)
九、排序与搜索
概念
排序:把某个乱序的数组变成升序或者降序的数组。 JS中的排序:数组的sort方法。
排序算法包括:冒泡排序、选择排序、插入排序、归并排序、快速排序等。
搜索: 找出数组中某个元素的下标。 JS中的搜索:数组的indexOf方法。
搜索算法包括:顺序搜索、二分搜索...
JS实现:冒泡排序
思路:
Array.prototype.bubbleSort = function() {
for(let i = 0; i< this.length - 1; i += 1) {
for(let j = 0; j< this.length - 1 - i; j += 1) {
if(this[j] > this[j+1]) {
const temp = this[j]
this[j] = this[j + 1]
this[j + 1] = temp
}
}
}
// console.log(this)
}
const arr = [5,4,3,2,1]
arr.bubbleSort()
两个嵌套循环,时间复杂度O(n²)
JS实现:选择排序
思路:
Array.prototype.selectSort = function() {
for(let i = 0; i< this.length - 1; i += 1) {
let indexMin = i
for(let j = i; j< this.length; j += 1) {
if(this[j] < this[indexMin]) {
indexMin = j
}
}
if (indexMin !== i) {
const temp = this[i]
this[i] = this[indexMin]
this[indexMin] = temp
}
}
// console.log(this)
}
const arr = [5,4,3,2,1]
arr.selectSort()
两个嵌套循环,时间复杂度O(n²)
JS实现:插入排序
思路: 从第二个数开始往前比; 比它大就往后排; 以此类推进行到最后一个数。
Array.prototype.insertSort = function() {
for(let i = 0; i < this.length; i += 1) {
const temp = this[i]
let j = i
while(j > 0) {
if (this[j - 1] > temp) {
this[j] = this[j-1]
} else {
break
}
j -= 1
}
this[j] = temp
}
// console.log(this)
}
const arr = [5,4,3,2,1]
arr.insertSort()
两个嵌套循环,时间复杂度O(n²)
JS实现:归并排序
思路:
Array.prototype.mergeSort = function() {
const rec = (arr) => {
if (arr.length === 1) return arr
// "分"
const mid = Math.floor(arr.length / 2)
const left = arr.slice(0, mid)
const right = arr.slice(mid, arr.length)
const orderLeft = rec(left)
const orderRight = rec(right)
// "合"
const res = []
while (orderLeft.length || orderRight.length) {
if (orderLeft.length && orderRight.length) {
res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift())
} else if(orderLeft.length) {
res.push(orderLeft.shift())
} else if(orderRight.length) {
res.push(orderRight.shift())
}
}
return res
}
const res = rec(this)
// 拷贝回原数组
res.forEach((n, i) => { this[i] = n})
}
const arr = [5,4,3,2,1]
arr.mergeSort()
时间复杂度O(nlogn)
JS实现:快速排序
思路:
Array.prototype.quickSort = function () {
const rec = (arr) => {
if (arr.length === 1) return arr
const left = []
const right = []
const mid = arr[0]
for (let i = 1; i < arr.length; i += 1) {
if (arr[i] < mid) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...rec(left), mid, ...rec(right)]
}
const res = rec(this)
// 拷贝回原数组
res.forEach((n, i) => {
this[i] = n
})
// console.log(this)
}
const arr = [2, 4, 5, 3, 1]
arr.quickSort()
时间复杂度O(nlogn)
JS实现:顺序搜索
思路:
Array.prototype.sequentialSearch = function (item) {
for (let i = 0; i < this.length; i += 1) {
if (this[i] === item) {
return i
}
}
return -1
}
const res = [1, 2, 3, 4, 5].sequentialSearch(3)
遍历数组是一个循环操作,时间复杂度O(n)
JS实现:二分搜索
思路:
Array.prototype.binarySearch = function (item) {
let low = 0
let high = this.length - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
const element = this[mid]
if(element<item) {
low = mid +1
} else if(element>item) {
high = mid-1
} else {
return mid
}
}
return -1
}
const res = [1, 2, 3, 4, 5].binarySearch(3)
在数组无序的情况下,需要对数组进行排序。
每一次比较都使搜索范围缩小一半,时间复杂度O(logn),效率高于顺序搜搜。
题目
21. 合并两个有序链表
解题思路: 与归并排序中的合并两个有序数组很相似,将数组替换成链表即可。
步骤:
var mergeTwoLists = function (list1, list2) {
const res = new ListNode(0)
let p = res,
p1 = list1,
p2 = list2 // 建立三个指针并指定指向
while (p1 && p2) {
if (p1.val < p2.val) {
p.next = p1
p1 = p1.next
} else {
p.next = p2
p2 = p2.next
}
p = p.next
}
if (p1) {
p.next = p1
}
if (p2) {
p.next = p2
}
return res.next
};
时间复杂度O(n), 空间复杂度O(1)
374. 猜数字大小
解题思路:利用二分搜索;调用guess函数,来判断中间元素是否是目标值。
步骤:
var guessNumber = function (n) {
let low = 0
let high = n
while (low <= high) {
const mid = Math.floor((low + high) / 2)
const res = guess(mid)
if (res === 0) {
return mid
} else if (res === -1) {
high = mid - 1
} else if (res === 1) {
low = mid + 1
}
}
};
时间复杂度O(logn), 空间复杂度O(1)
十、分而治之
概念
分而治之是算法设计中的一种方法,并不是具体的数据结构或某个算法,它是一种思想。 它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
场景一: 归并排序
场景二: 快速排序
题目
374. 猜数字大小
步骤:
var guessNumber = function (n) {
const rec = (low, high) => {
if (low > high) return
const mid = Math.floor((low + high) / 2)
const res = guess(mid)
if (res === 0) {
return mid
} else if (res === -1) {
return rec(1, mid - 1)
} else if (res === 1) {
return rec(mid + 1, high)
}
}
return rec(1, n)
};
时间复杂度O(logn), 空间复杂度O(logn)
226. 翻转二叉树
思路:先翻转左右子树,再将子树换个位置;符合“分、解、合”特性,考虑分而治之。
步骤:
var invertTree = function(root) {
if(!root) return null
return {
val: root.val,
left: invertTree(root.right),
right: invertTree(root.left)
}
};
时间复杂度O(n), 空间复杂度O(n)
100. 相同的树
var isSameTree = function(p, q) {
if(!p && !q) return true
if((!p && q) || (p && !q)) return false
return p.val === q.val ? (isSameTree(p.left, q.left) && isSameTree(p.right, q.right)) : false
};
时间复杂度O(n), 空间复杂度O(n)
101. 对称二叉树
步骤:
var isSymmetric = function(root) {
if(!root) return true
const rec = (l, r) => {
if(!l && !r) return true
if(l && r && l.val === r.val &&
rec(l.left, r.right) &&
rec(l.right, r.left)){
return true
}
return false
}
return rec(root.left, root.right)
};
时间复杂度O(n), 空间复杂度O(n)
十一、动态规划
概念
动态规划是算法设计中的一种方法,它将一个问题分解为相互重叠的子问题(分而治之的子问题是相互独立的),通过反复求解子问题,来解决原来的问题。
典型例子:斐波那契数列问题
题目
70. 爬楼梯
思路:
var climbStairs = function (n) {
if (n < 2) return 1
const dp = [1,1]
for (let i = 2; i <= n; i += 1) {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n]
};
时间复杂度O(n), 空间复杂度O(n),可定义两个变量代替dp数组,优化空间复杂度为O(1)
198. 打家劫舍
思路:
步骤:
var rob = function (nums) {
if(!nums.length) return 0
const dp = [0, nums[0]] // 定义dp数组,记录每一个房间能打劫的最大金额
for (let i = 2; i <= nums.length; i += 1) {
dp[i] = Math.max(dp[i-2]+nums[i-1], dp[i-1]) // 注意nums的下标
}
return dp[nums.length]
};
时间复杂度O(n), 空间复杂度O(n),可定义两个变量代替dp数组,优化空间复杂度为O(1)
213. 打家劫舍 II
var rob = function (nums) {
if(!nums.length) return 0
if(nums.length === 1) return nums[0]
const dp0 = [0, nums[0]]
const dp1 = [0, nums[1]]
for (let i = 2; i < nums.length; i += 1) {
dp0[i] = Math.max(dp0[i-2]+nums[i-1], dp0[i-1])
}
for (let i = 2; i < nums.length; i += 1) {
dp1[i] = Math.max(dp1[i-2]+nums[i], dp1[i-1])
}
return Math.max(dp0[nums.length - 1], dp1[nums.length - 1])
};
十二、贪心算法
概念
贪心算法是算法设计中的一种方法(算法设计思想),它期盼通过每个阶段按的局部最优选择,从而达到全局的最优。结果并不一定是最优的。
题目
455. 分发饼干
思路: 局部最优: 既能满足孩子,还能消耗最少;先将“较小的饼干”分给“胃口”最小的孩子。
步骤:
var findContentChildren = function(g, s) {
const sortFunc = (a,b) => {
return a-b
}
g.sort(sortFunc) // 按照自定义的方式排序
s.sort(sortFunc)
let i = 0
for(let n = 0; n < s.length; n++) {
if(s[n] >= g[i]) {
i += 1
}
}
return i
};
时间复杂度O(nlogn), 空间复杂度O(1)
122. 买卖股票的最佳时机 II
思路:
步骤:
var maxProfit = function(prices) {
let res = 0
for(let n = 1; n < prices.length; n++) {
if(prices[n] > prices[n-1]) {
res += prices[n] - prices[n-1]
}
}
return res
};
时间复杂度O(n), 空间复杂度O(1)
十三、回溯算法
概念
回溯算法也是算法设计中的一种方法,它是一种渐进式寻找并构建问题解决方式的策略,它会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到解决问题。
什么问题适合用回溯算法解决: 有很多路,这些路里,有死路也有出路,通常需要递归来模拟所有的出路。
如全排列问题:
题目
46. 全排列
思路:要求1、所有排列情况 2、没有重复元素;有出路有死路;考虑使用回溯算法。
步骤:
到达终点的情况即path长度为数组长度(图示中为3)
var permute = function(nums) {
const res = []
const backtrack = (path) => {
if(path.length === nums.length) {
res.push(path)
return
}
nums.forEach(element => {
if(path.includes(element)) return // 死路回溯
backtrack(path.concat(element)) // 加入路径
});
}
backtrack([])
return res
};
时间复杂度O(n!), 空间复杂度O(n)
78. 子集
步骤:
达到终点的情况:从0遍历到数组长度,每一种情况下都进行一遍回溯
var subsets = function (nums) {
const res = []
const backtrack = (path, l, start) => {
if (path.length === l) {
res.push(path)
return
}
for (let i = start; i < nums.length; i++) {
backtrack(path.concat(nums[i]), l, i + 1) // 加入路径
}
// if(path.includes(element)) return // 死路回溯
}
for (let i = 0; i <= nums.length; i += 1) {
backtrack([], i, 0) // 将路径、长度、起始下标传进去
}
return res
};
时间复杂度O(2^n), 空间复杂度O(n)