本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 笔记 系列第 1 篇,关注专栏
VScode 调试
- js文件代码调试,设置断点,按
F5
进入调试模式
。
栈
- 是一个
后进先出
的数据结构。 - js中没有栈,可以通过
数组
来实现。 - 栈常用操作:
push
、pop
、stack[stack.length - 1]
。
const stack = []
stack.push(1)
stack.push(1)
const item1 = stack.pop()
const item2 = stack.pop()
队列
- 是一个
先进先出
的数据结构。 - js中没有队列,可以通过
数组
来实现。 - 队列常用操作:
push、shift。
const queue = []
queue.push(1)
queue.push(1)
const item1 = queue.shift()
const item2 = queue.shift()
链表
- 链表里的元素存储
不是连续的,之间通过 next 连接
。 - avaScript 中没有链表,但可以用
Object
模拟链表。 - 链表常用操作:
修改 next、遍历链表。
- 使用场景:
- JS中的原型链也是一个链表。
- 使用链表指针可以获取JSON的节点值。
- 反转链表
- 反转两个节点:
将 n+1 的 next 指向 n
。 - 反转多个节点:
双指针遍历链表,重复上述操作
。
- 反转两个节点:
const a = { val: 1 }
const b = { val: 2 }
const c = { val: 3 }
a.next = b
b.next = c
c.next = null
// 遍历链表
let p = a
while (p) {
console.log(p.val)
p = p.next
}
// 插入
const d = { val: 4 }
b.next = d
d.next = c
// 删除 (修改 next)
b.next = c
// 手写 instanceOf
const instanceOf = (A, B) => {
let p = A
while (p) {
if (p === B.prototype) {
return true
}
p = p.__proto__
}
return false
}
instanceOf([], Array)
instanceOf({}, Object)
// 根据路径获取 值
const json = {
a: { b: { c: 1 } },
d: { e: 1 },
}
const path = ['a', 'b', 'c']
let p = json
path.forEach((key) => {
p = p[key]
})
集合
- 集合是一种
无需且唯一
的数据结构。 - ES6中有集合,名为
Set
。 - 集合常用操作:
去重、判断某元素是否在集合中、求交集
。
// 去重
const arr = [1, 1, 2, 2]
const newArr = [...new Set(arr)]
// 判断元素是否在集合中
const set = new Set(arr)
const has = set.has(4)
// 求交集
const set2 = new Set([2,3])
const set3 = new Set([...set].filter(item => set2.has(item)))
// set 操作
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 has1 = mySet.has(o)
mySet.delete(5)
// 数组转 set,set 转 数组
const arr = Array.from(mySet)
const mySet2 = new Set([1,2,3])
// 交集
const intersection = new Set([...mySet].filtet(item => mySet2.has(item)))
// 不同集
const difference = new Set([...mySet].filtet(item => !mySet2.has(item)))
字典
- 与集合类似,字典也是一种
存储唯一值
的数据结构,但它是以键值对的形式来存储。 - 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')
树
- 树是一种
分层数据
的抽象模型。 - 树的常用操作,
深度/广度优先遍历、先中后序遍历
。 - 深度优先遍历
- 尽可能深的搜索树的分支。
- 类似看书,一页页看过去就是深度优先。
- 算法步骤
dfs(深度缩写)
- 访问根节点。
- 对根节点的 children 挨个进行深度优先遍历。
- 广度优先遍历
- 先访问离根节点最近的节点。
- 类似看书,先看目录,再深入了解每个小节。
- 算法步骤
bfs(广度缩写)
- 新建一个队列,把根节点入队。
- 把对头出队并访问。
- 把对头的 children 挨个入队。
- 重复第二、三步,直到队列为空。
// 深度优先遍历
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, [])
// 深度优先
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 bfs = (root) => {
const q = [root] // 1 新队列
while (q.length) {
const n = q.shift() // 2 出队
console.log(n.val) // 2 访问
n.children.forEach((child) => {
q.push(child) // 3 入队
})
}
}
bfs(tree)
二叉树
- 树中每个节点
最多只能有两个子节点
。 - 先序遍历
访问根节点
。- 对根节点的左子树进行
先序遍历
。 - 对根节点的右子树进行
先序遍历
。
- 中序遍历
- 对根节点的左子树进行
中序遍历
。 访问根节点
。- 对根节点的右子树进行
中序遍历
。
- 对根节点的左子树进行
- 右序遍历
- 对根节点的左子树进行
后序遍历
。 - 对根节点的右子树进行
后序遍历
。 访问根节点
。
- 对根节点的左子树进行
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) return
console.log(root.val)
preorder(root.left)
preorder(root.right)
}
//preorder(bt)
// 先序遍历 非递归版
const preorder1 = (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)
}
}
//preorder1(bt)
// 中序
const inorder = (root) => {
if (!root) return
inorder(root.left)
console.log(root.val)
inorder(root.right)
}
//inorder(bt)
// 中序遍历 非递归版
const inorder1 = (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
}
}
//inorder1(bt)
// 后序
const postorder = (root) => {
if (!root) return
postorder(root.left)
postorder(root.right)
console.log(root.val)
}
//postorder(bt)
// 后续遍历 非递归版
const postorder1 = (root) => {
if (!root) return
const stack = [root]
const outputStack = []
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)
}
}
postorder1(bt)
图
- 图是
网络结构
的抽象模型,是一组由边连接的节点。 - 图可以表示任何二元关系,比如道路、航班。
- js中没有图,可以使用
Object
和Array
来实现。 - 图的表示法:
邻接矩阵、邻接表、关联矩阵。
- 图的常用操作:
深度优先遍历、广度优先遍历。
- 图的深度优先遍历
- 访问根节点。
- 对根节点的
没访问过的相邻节点
挨个进行深度优先遍历(防止死循环)。
- 图的广度优先遍历
- 新建一个队列,把根节点入队。
- 把队头出队并访问。
- 把队头的
没访问过的相邻节点
入队。 - 重复第二、三步,直到队列为空。
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()
const q = [2]
visited.add(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)
}
})
}
堆
- 堆是一种特殊的
完全二叉树
。 - 所有的节点都
大于等于(最大堆)或小于等于(最小堆)
它的子节点。 - js中通常用数组表示堆
- 左侧子节点的位置是
2 * index + 1
。 - 左侧子节点的位置是
2 * index + 2
。 - 父节点位置是
(index - 1) / 2
。
- 左侧子节点的位置是
- 堆的应用
高效、快速地找出最大值和最小值
,时间复杂度: O(1)。- 找出第 K 个最大(小)元素
- 构建一个最小堆,并将元素一次插入堆中。
- 当堆的容量超过 K,就删除堆顶。
- 插入结束后,堆顶就是第 K 个最大元素。
- 实现
- 在类里,
声明一个数组
,用来装元素。 - 主要方法:
插入、删除堆顶、获取堆顶、获取堆大小
。 - 插入
- 将值插入堆的底部,即数组的尾部。
- 然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值。
- 大小为 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
}
// 获取父索引
getParentIndex(i) {
// >> 二进制 取商
return (i - 1) >> 1 //Math.floor((i - 1) / 2)
}
// 获取左侧索引
getLeftIndex(i) {
return i * 2 + 1
}
// 获取右侧索引
getRightIndex(i) {
return i * 2 + 2
}
// 上移
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.shiftDown(0)
}
// 获取堆顶
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()
排序和搜索
- 排序:
把某个乱序的数组变成升序或者降序的数组
。 - 搜索:
找出数组中某个元素的下标
。 - 冒泡排序 O(n ^ 2)
- 比较所有相邻元素,如果第一个比第二个大,则交换它们。
- 一轮下来,可以保证最后一个数是最大的。
- 执行 n - 1 轮,就可以完成排序。
// 冒泡排序
Array.prototype.bubbleSort = function () {
for (let i = 0; i < this.length - 1; i++) {
// this.length - 1 - i 逐步减小区间
for (let j = 0; j < this.length - 1 - i; j++) {
if (this[j] > this[j + 1]) {
const temp = this[j]
this[j] = this[j + 1]
this[j + 1] = temp
}
}
}
}
const arr = [5, 4, 3, 2, 1]
arr.bubbleSort()
- 选择排序 O(n ^ 2)
- 找到数组中的最小值,选中它并将其放置在第一位。
- 接着找到第二小的值,选中它并将其放置在第二位。
- 以此类推,执行 n - 1 轮。
// 选择排序
Array.prototype.selectionSort = function () {
for (let i = 0; i < this.length - 1; i++) {
let indexMin = i
for (let j = i; j < this.length; j++) {
if (this[j] < this[indexMin]) {
indexMin = j
}
}
if (indexMin !== i) {
const temp = this[i]
this[i] = this[indexMin]
this[indexMin] = temp
}
}
}
const arr = [5, 4, 3, 2, 1]
arr.selectionSort()
- 插入排序 O(n ^ 2)
- 从第二个数开始
往前比
。 - 比它大就
往后排
。 - 以此类推进行到最后一个数。
- 从第二个数开始
// 插入排序
Array.prototype.insertionSort = function () {
for (let i = 1; i < this.length; i++) {
const temp = this[i]
let j = i
while (j > 0) {
if (this[j - 1] > temp) {
this[j] = this[j - 1]
} else {
break
}
j--
}
this[j] = temp
}
}
const arr = [5, 4, 3, 2, 1]
arr.insertionSort()
- 归并排序 O(n * logN)
- 分O(logN):
把数组劈成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数
。 - 合O(n):
把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组
。 - 合并两个有序数组
- 新建一个空数组 res,用于存放最终排序后的数组。
- 比较两个有序数组的头部,较小者出队并推入 res 中。
- 如果两个数组还有只,就重复第二步。
- 分O(logN):
// 归并排序
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 orferRight = rec(right)
const res = []
while (orderLeft.length || orferRight.length) {
if (orderLeft.length && orferRight.length) {
res.push(
orderLeft[0] < orferRight[0] ? orderLeft.shift() : orferRight.shift()
)
} else if (orderLeft.length) {
res.push(orderLeft.shift())
} else if (orferRight.length) {
res.push(orferRight.shift())
}
}
return res
}
const res = rec(this)
res.forEach((n, i) => {
this[i] = n
})
}
const arr = [5, 4, 3, 2, 1]
arr.mergeSort()
- 快速排序 O(n * logN)(性能比前几个都好)
- 分区O(n):
从数组中任意选择一个“基准”
,所有比基准小
的元素放在基准前面
,比基准大
的元素放在基准后面
。 - 递归O(logN):递归地对基准前后的子数组进行分区。
- 分区O(n):
// 快速排序
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++) {
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
})
}
const arr = [5, 4, 3, 2, 1]
arr.quickSort()
- 顺序搜索 O(n) 效率低
遍历数组
。- 找到跟目标相等的元素,就
返回它的下标
。 - 遍历结束后,如果没有搜索到目标值,就
返回 -1
。
// 顺序搜索
Array.prototype.sequentialSearch = function (item) {
for (let i = 0; i < this.length; i++) {
if (this[i] === item) {
return i
}
}
return -1
}
const res = [1, 2, 3, 4, 5].sequentialSearch(3)
- 二分搜索 O(logN)
从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束
。如果目标值大于或小于中间元素,则在大于或小于中间元素的那一半数组中搜索
。
// 二分搜索
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)
分而治之
算法设计
中的一种方法,或者说是一种思想。- 它将一个问题
分
成多个和原问题相似的小问题,递归解决
小问题,再将结果合
并以解决原来的问题。 分、递归、合
是分而治之三步骤。- 场景一:
归并排序
- 分:把数组从中间一分为二。
- 解:递归地对两个子数组进行归并排序。
- 合:合并有序子数组。
- 场景二:
快速排序
- 分:选基准,按基准把数组分成两个子数组。
- 解:递归地对两个子数组进行快速排序。
- 合:对两个子数组进行合并。
动态规划
算法设计
中的一种方法。- 它将一个问题分解为
相互重叠
的子问题,通过反复求解子问题,来解决原来的问题。 - 场景:
斐波那契数列
- 前两个之和等于后一个,依此类推(0,1,1,2,3,5,8,13,21,34)。
- 定义子问题:F(n) = F(n - 1) + F(n - 2)。
- 反复执行:从 2 循环到 n , 执行上述公式。
贪心算法
算法设计
中的一种方法。- 期盼通过每个阶段的
局部最优
选择,从而达到全局的最优。 - 结果并
不一定是最优。
回溯算法
算法设计
中的一种方法。- 回溯算法是一种
渐进式
寻找并构建问题解决方式的策略。 - 回溯算法会先从一个可能得动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。
- 什么问题适合用回溯算法解决?
- 有很多路。
- 这些路里,有思路,也有出路。
- 通常需要递归来模拟所有的路。
- 场景:全排列
- 用递归模拟出所有情况。
- 遇到包含重复元素的情况,就回溯。
- 收集所有到底递归终点的情况,并返回。