一、数据结构与算法简介
1、理论
* 数据结构:栈、队列、集合、链表、字典、树、图、堆
* 进阶算法:冒泡算法、选择算法、插入算法、归并算法、快速算法、顺序算法、二分搜索
* 算法设计思想:分而治之、动态规则、贪心、回溯
* 重点关注:数据结构与算法的特点、应用场景、js实现、时间/空间复杂度
2、刷题
* 刷题网站:leetcode
* 重点关注:通用套路、时间/空间复杂度分析和优化
3、数据结构与算法
* 是什么:
- 数据结构:计算机存储、组织数据的方式
- 算法:一系列解决问题的清晰指令
* 程序:数据结构 + 算法
* 关系:数据结构为算法提供服务,算法围绕数据结构操作
二、时间空间复杂度计算
1、时间复杂度计算
* 一个函数,用大O表示,比如O(1)、O(n)、O(logN)...
* 定性描述该算法的运行时间
let i = 0
i += 1
// O(1) + O(n) = O(n)
for (let i = 0; i < n; i++) {
console.log(i)
}
// O(n) * O(n) = O(n^2)
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
console.log(i, j)
}
}
// 如果a的x次方等于N,其中a>0且a不等于1,那么数x叫做以a为底N的对数
// 2的x次方等于N,x等于logN(计算机中2会省略不写,数学中2不可以省略)
let i = 1
while (i < n) {
console.log(i)
i *= 2
}
2、空间复杂度计算
* 一个函数,用大O表示,比如O(1)、O(n)、O(n^2)...
* 算法在运行过程中临时占用存储空间大小的量度
let i = 0
i += 1
const list = []
for (let i = 0; i < n; i++) {
list.push(i)
}
const matrix = []
for (let i = 0; i < n; i++) {
matrix.push([])
for (let j = 0; j < n; j++) {
matrix[i].push(j)
}
}
三、数据结构-栈
1、栈简介
// 1、栈是一种后进先出的数据结构
// 2、栈常用操作:push、pop、stack[stack.length-1]
const stack = []
stack.push(1)
stack.push(2)
const item1 = stack.pop()
const item2 = stack.pop()
2、栈的应用场景
* 十进制转二进制
* 判断字符串的括号是否有效
* 函数调用堆栈
3、力扣解题(20. 有效的括号)
var isValid = function (s) {
const lp = "([{"
if (s.length % 2 === 1 || lp.indexOf(s[0]) === -1) {
return false
}
const stack = []
const rp = ")]}"
for (let i = 0; i < s.length; i++) {
if (lp.indexOf(s[i]) !== -1) {
stack.push(s[i])
} else {
const top = stack.pop()
if (lp.indexOf(top) !== rp.indexOf(s[i])) {
return false
}
}
}
return !stack.length
};
4、力扣解题(144. 二叉树的前序遍历)
// 二叉树前序:根-左-右
// 二叉树中序:左-根-右
// 二叉树后序:左-右-根
var preorderTraversal = function (root) {
let arr = []
let preorder = function (node, arr) {
if (node === null) return arr
arr.push(node.val)
preorder(node.left, arr)
preorder(node.right, arr)
}
preorder(root, arr)
return arr
};
四、数据结构-队列
1、队列简介
// 1、队列是一种先进先出的数据结构
// 2、队列常用操作:push、shift、queue[0]
const queue = []
queue.push(1)
queue.push(2)
const item1 = queue.shift()
const item2 = queue.shift()
2、队列的应用场景
* 食堂排队打饭
* js异步中的任务队列
* 计算最近请求次数
3、力扣解题(933. 最近的请求次数)
var RecentCounter = function () {
this.q = []
};
RecentCounter.prototype.ping = function (t) {
this.q.push(t)
while (this.q[0] < t - 3000) {
this.q.shift()
}
return this.q.length
};
五、数据结构-链表
1、链表简介
/**
* 一、链表是什么?
* - 多个元素组成的列表
* - 元素存储不连续,用next指针连在一起
* 二、数组vs链表
* - 数组:增删非首尾元素时往往需要移动元素
* - 链表:增删非首尾元素,不需要移动元素,只需要更改next的指向即可
* 三、链表常用操作:修改next、遍历链表
*/
const a = {val: "a"}
const b = {val: "b"}
const c = {val: "c"}
a.next = b
b.next = c
// 遍历
let p = a
while (p) {
// console.log(p)
p = p.next
}
// 添加
const d = {val: "d"}
b.next = d
d.next = c
console.log(a)
// 删除
// b.next = c
// console.log(a)
2、力扣解题(237. 删除链表中的节点)
var deleteNode = function (node) {
node.val = node.next.val
node.next = node.next.next
};
3、力扣解题(206. 反转链表)
var reverseList = function(head) {
let p = null
while (head) {
head.prev = head.next
head.next = p
p = head
head = head.prev
}
return p
};
4、力扣解题(2. 两数相加)
var addTwoNumbers = function (l1, l2) {
const l3 = new ListNode(0)
let p = l3
let carry = 0
while (l1 || l2) {
const v1 = l1 ? l1.val : 0
const v2 = l2 ? l2.val : 0
p.next = new ListNode((v1 + v2 + carry) % 10)
carry = Math.floor((v1 + v2 + carry) / 10)
if (l1) {
l1 = l1.next
}
if (l2) {
l2 = l2.next
}
p = p.next
}
if (carry) {
p.next = new ListNode(carry)
}
return l3.next
};
5、力扣解题(83. 删除排序链表中的重复元素)
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
};
6、力扣解题(141. 环形链表)
var hasCycle = function (head) {
while (head) {
if (head.visited) {
return true
}
head.visited = true
head = head.next
}
return false
};
六、数据结构-集合
1、集合简介
/**
* 一、什么是集合?
* - 一种无序且唯一的数据结构
* - 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)))
2、力扣解题(349. 两个数组的交集)
var intersection = function (nums1, nums2) {
return [...new Set(nums1)].filter(item => nums2.includes(item))
};
七、数据结构-字典
1、字典简介
/**
* 一、什么是字典?
* - 与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储
* - es6中有字典,名为Map
* - 字典的常用操作:键值对的增删改查
*/
const m = new Map();
// 增
m.set("a", "aaa")
m.set("b", "bbb")
// 删
m.delete("b")
// m.clear()
// 改
m.set("a", "aaaaaa")
2、力扣解题(1. 两数之和)
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]
} else {
map.set(n, i)
}
}
};
3、力扣解题(3. 无重复字符的最长子串)
var lengthOfLongestSubstring = function (s) {
let l = 0
let res = 0
const map = new Map();
for (let i = 0; i < s.length; i++) {
if (map.has(s[i]) && map.get(s[i]) >= l) {
l = map.get(s[i]) + 1
}
res = Math.max(res, i - l + 1)
map.set(s[i], i)
}
return res
};
4、力扣解题(76. 最小覆盖子串)
var minWindow = function (s, t) {
let l = 0
let r = 0
const need = new Map()
for (const c of t) {
need.set(c, need.has(c) ? need.get(c) + 1 : 1)
}
let needType = need.size
let res = ""
while (r < s.length) {
const c = s[r]
if (need.has(c)) {
need.set(c, need.get(c) - 1)
if (need.get(c) === 0) needType -= 1
}
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 += 1
}
l += 1
}
r += 1
}
return res
};
八、数据结构-树
1、树简介
/**
* 一、什么是树?
* - 一种分层数据的抽象模型
* - 前端工作中常见的树包括:dom树、级联选择、树形控件
* - 树的常用操作:深度/广度优先遍历、先中后序遍历
* 二、什么是深度/广度优先遍历
* - 深度优先遍历:尽可能深的搜索树的分支
* - 广度优先遍历:先访问离根节点最近的节点
*/
const tree = {
val: "a",
children: [{
val: "b",
children: [{
val: "d",
children: []
}, {
val: "e",
children: []
}]
}, {
val: "c",
children: [{
val: "f",
children: []
}, {
val: "g",
children: []
}]
}]
}
/**
* 三、深度优先遍历算法口诀
* - 访问根节点
* - 对根节点的children挨个进行深度优先遍历
*/
const dfs = (root) => {
console.log(root.val)
root.children.forEach(dfs)
}
dfs(tree)
/**
* 四、广度优先遍历算法口诀
* - 新建一个队列,把根节点入队
* - 把对头出队并访问
* - 把对头的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)
2、二叉树的先中后序遍历
/**
* 一、二叉树是什么?
* - 树中每个节点最多只能有两个子节点
*/
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,
},
},
}
/**
* 二、先序遍历算法口诀
* - 访问根节点
* - 对根节点的左子树进行先序遍历
* - 对根节点的右子树进行先序遍历
*/
const preorder = (root) => {
if (root) {
console.log(root.val)
preorder(root.left)
preorder(root.right)
}
}
preorder(bt)
/**
* 三、中序遍历算法口诀
* - 对根节点的左子树进行中序遍历
* - 访问根节点
* - 对根节点的右子树进行中序遍历
*/
const inorder = (root) => {
if (root) {
inorder(root.left)
console.log(root.val)
inorder(root.right)
}
}
inorder(bt)
/**
* 四、后序遍历算法口诀
* - 对根节点的左子树进行后序遍历
* - 对根节点的右子树进行后序遍历
* - 访问根节点
*/
const postorder = (root) => {
if (root) {
postorder(root.left)
postorder(root.right)
console.log(root.val)
}
}
postorder(bt)
3、二叉树的先中后序遍历(非递归版)
const preorder = (root) => {
if (root) {
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)
const inorder = (root) => {
if (root) {
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)
const postorder = (root) => {
if (root) {
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)
4、力扣解题(104. 二叉树的最大深度)
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
};
5、力扣解题(111. 二叉树的最小深度)
var minDepth = function (root) {
if (!root) {
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])
}
};
6、力扣解题(102. 二叉树的层序遍历)
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
};
7、力扣解题(94. 二叉树的中序遍历)
var inorderTraversal = function (root) {
const res = []
const 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
};
8、力扣解题(112. 路径总和)
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
};
九、数据结构-图
1、图简介
/**
* 一、图是什么?
* - 图是网络结构的抽象模型,是一组由边连接的节点
* - 图可以表示任何二元关系,比如道路、航班
* - 图的表示法:领接矩阵,领接表、关联矩阵
* 二、图的常用操作
* - 深度优先遍历:尽可能深的搜索图的分支
* - 广度优先遍历:先访问离根节点最近的节点
*/
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3],
}
/**
* 三、深度优先遍历算法口诀
* - 访问根节点
* - 对根节点的没访问过的相邻节点挨个进行深度优先遍历
*/
/*
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 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)
}
})
}
2、力扣解题(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,},
4: {"digit": 5, "sign": 7,},
5: {"digit": 5,},
6: {"digit": 6, ".": 3, "e": 4,},
7: {"digit": 5,},
}
let state = 0
for (let c of s.trim().toLowerCase()) {
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
};
3、力扣解题(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))
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
};
4、力扣解题(133. 克隆图)
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);
};
十、数据结构-堆
1、堆简介
/**
* 一、堆是什么?
* - 堆是一种特殊的完全二叉树
* - 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
* 二、js中的堆
* - js中通常用数组表示堆
* - 左侧子节点的位置是2*index+1
* - 右侧子节点的位置是2*index+2
* - 父节点位置是(index-1)/2
* 三、堆的应用
* - 堆能高效、快速地找出最大值和最小值,时间复杂度:O(1)
* - 找出第k个最大(小)元素
* 四、第k个最大元素
* - 构建一个最小堆,并将元素依次插入堆中
* - 当堆的容量超过k,就删除堆顶
* - 插入结束后,堆顶就是第k个最大元素
*/
2、javascript实现:最小堆类
/**
* 一、实现步骤
* - 在类里,声明一个数组,用来装元素
* - 主要方法:插入、删除堆顶、获取堆顶、获取堆大小
*/
class MinHeap {
constructor() {
this.heap = []
}
/**
* 二、插入
* - 将值插入堆的底部,即数组的尾部
* - 然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
* - 大小为k的堆中插入元素的时间复杂度为O(logk)
*/
insert(value) {
this.heap.push(value)
this.shiftUp(this.heap.length - 1)
}
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)
}
}
getParentIndex(i) {
// 等价:Math.floor((i - 1) / 2)
return (i - 1) >> 1
}
swap(i1, i2) {
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
/**
* 三、删除堆顶
* - 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
* - 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶
* - 大小为k的堆中删除堆顶的时间复杂度为O(logk)
*/
pop() {
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
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)
}
}
getLeftIndex(i) {
return i * 2 + 1
}
getRightIndex(i) {
return i * 2 + 2
}
/**
* 四、获取堆顶和堆的大小
* - 获取堆顶:返回数组的头部
* - 获取堆的大小:返回数组的长度
*/
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
const h = new MinHeap()
h.insert(3)
h.insert(2)
h.insert(1)
h.pop()
3、力扣解题(215. 数组中的第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()
};
4、力扣解题(347. 前K个高频元素)
// 最小堆类需做改动的方法
class MinHeap {
shiftUp(index) {
if (index === 0) {
return
}
const parentIndex = this.getParentIndex(index)
if (this.heap[parentIndex] && 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)
}
}
}
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)
};
5、力扣解题(23. 合并K个升序链表)
// 最小堆类需做改动的方法
class MinHeap {
shiftUp(index) {
if (index === 0) {
return
}
const parentIndex = this.getParentIndex(index)
if (this.heap[parentIndex] && this.heap[parentIndex].val > this.heap[index].val) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
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
}
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)
}
}
}
var mergeKLists = function (lists) {
const res = new ListNode(0)
let p = res
const h = new MinHeap()
lists.forEach(l => {
if (l) h.insert(l)
})
while (h.size()) {
const n = h.pop()
p.next = n
p = p.next
if (n.next) h.insert(n.next)
}
return res.next
};