- 算法教程
- 数据结构Array、HashMap 与 List
- 数据结构Array、HashMap 与 List(原文)
- logN的由来
- 各种算法结构演示
- 前端算法-git
- 前端算法-docs
- 小浩算法
- leetcode原题详解-历史目录
- 各种常见限流算法
- labuladong算法站
- hello 算法
展开更多
只有两种:数组(顺序存储)和链表(链式存储)
队列、栈、哈希表扩展自数组
树、堆、图扩展自链表
线性: for/while/forEach
非线性: 递归
O(1)
- array.push
- array.pop
- array.shift
O(n)
- array.unshift
- array.slice
- array.splice
- 数组中查找/删除某个元素
O(logn)
- 二叉搜索
- 子问题个数乘以解决一个子问题需要的时间
- 用常数1来取代运行时间中所有加法常数。
- 修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
常用技巧
- 前缀和
- s[0]=0
- s[i+1] = nums[0] + nums[1] + ⋯ + nums[i]
- s[i] - s[j]: 序号i到j之间的总和
结构:value、left、right
空间复杂度:
dfs - O(h)
bfs - O(w)
遍历方式:
先序遍历:考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。
- 访问顺序:根节点 → 左子树 → 右子树
- 常用:层级遍历、右视图
中序遍历:考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树,即深度遍历。
- 访问顺序:左子树 → 根节点 → 右子树
- 常用:排序、找最小
后序遍历:考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值。
访问顺序:左子树 → 右子树 → 根节点
常用:求直径
解题思路:
分治:后序遍历,问题不断分解为left和right问题
回溯:先序遍历,处理完当层问题,再解决下层
const fn = () => {
const queue = [root];
while (queue.length) {
let len = queue.length;
const temp = [];
while (len--) {
const node = queue.shift();
temp.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(temp);
}
};- 二叉树-中序遍历
- 二叉树-最大深度
- 二叉树-翻转二叉树
- 二叉树-对称二叉树
- 二叉树-二叉树直径
- 二叉树-层序遍历
- 二叉树-有序数组转为二叉搜索树
- 二叉树-验证二叉搜索树
- 二叉树-二叉搜索树第K小的元素
- 二叉树-二叉树的右视图
- 二叉树-二叉树展开为链表
- 二叉树-从前序和中序遍历构造二叉树
- 二叉树-路径总和三
- 二叉树-最近公共祖先
class Node {
constructor(data, left, right) {}
}
class Tree {
constructor(root) {}
// 根据【概念2】插入节点
insert(node) {}
// 获取最左子节点
getMin() {}
// 获取最右子节点
getMax() {}
// 查找特定node
getNode(data, node) {}
// 获取特定节点的深度(左、右子树的最大深度)
getDeep(node, deep) {}
}节点结构:
数据:value
指针:pre、next
头尾指针:head、tail
适用场景:
高频插入删除:操作栈、队列
动态数据:内存、文件系统
算法问题:LRU、大数相加
class Node {
constructor() {
this.value = null;
this.pre = null;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
}- 链表-相交链表
- 链表-反转链表
- 链表-回文链表
- 链表-环形链表
- 链表-环形链表2
- 链表-合并两个有序链表
- 链表-两数相加
- 链表-删除链表的倒数第N个结点
- 链表-两两交换链表中的节点
- 链表-随机链表的复制
- 链表-排序链表
- 链表-LRU缓存
理念:识别子问题 + 设计状态转移方程
核心思路:
dp含义:总量是i,最小/少是dp[i]
递推公式:子问题如何推到出更大问题的解,比如dp[i] = dp[i - 1] + dp[i - 2] + ...
初始化状态:dp[0] = 0、dp[1] = 1等
遍历顺序:Math.max(...dp[n])
遍历方式:如果是一维数组,可选项在外层,总量在内层;如果是二维数组,则两者可交换
打印结果:dp[i]
- dp-爬楼梯
- dp-杨辉三角
- TODO dp-打家劫舍
- TODO dp-完全平方数
- dp-零钱兑换
- TODO dp-单词拆分
- TODO dp-最长递增子序列长度
- TODO dp-乘积最大子数组
- dp-最长回文子串
- TODO dp-分割等和子集
概念
- 顶点 + 边
存储方式:
朴素存储:一维数组、哈希
邻接表:适合稀疏图,哈希(key是顶点,value是边的集合)
邻接矩阵:适合稠密图,二维数组
广度搜索
适合最短路径、层级遍历、连通检测
function bfs (graph, start) {
const queue = [start];
const visited = new Set();
visited.add(start);
while (queue.length) {
const vertex = queue.shift();
for (const neighbor of graph.vertices.get(vertex)) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
}深度搜索
检测环、路径存在、拓扑排序
function dfs (graph, start) {
const queue = [start];
const visited = new Set();
visited.add(start);
while (queue.length) {
const vertex = queue.pop();
for (const neighbor of graph.vertices.get(vertex)) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
}var preorderTraversal = function (root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length > 0) {
while (current) {
result.push(current.val);
stack.push(current);
current = current.left;
}
current = stack.pop();
current = current.right;
}
return result;
};var inorderTraversal = function (root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length > 0) {
// 左子树优先入栈
while (current) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.push(current.val);
// 右节点再入栈
current = current.right;
}
return result;
};var postorderTraversal = function (root) {
const result = [];
const stack = [];
let last = null; // 标记上一个访问的节点
let current = root;
while (current || stack.length > 0) {
while (current) {
stack.push(current);
current = current.left;
}
current = stack[stack.length - 1];
if (!current.right || current.right == last) {
current = stack.pop();
result.push(current.val);
last = current;
current = null; // 继续弹栈
} else {
current = current.right;
}
}
return result;
}特性:键值存储,常用于两数之和、统计次数、memorize
数据类型:Map、Set
时间复杂度:O(1)
特性:只能在栈顶push和pop,遵循后进先出
时间复杂度:O(1)
边界条件:空栈、栈溢出(无限递归)
即dfs
目标:在决策树中穷举所有可能性
路径选择:
选择:每步一个选择
递归:当前选择进入下个回溯
撤销:回退上个状态,尝试其他可能性
循环条件:
起始点统一,循环体在回溯函数内
起始点不统一,循环体包在回溯函数外
终止条件:当前路径满足条件,保存结果,返回
剪枝:预排除不满足条件的分支
function backtrack(路径, 选择列表) {
if (满足终止条件) {
保存结果;
return;
}
for (const 选择 of 选择列表) {
if (当前选择不合法) continue; // 剪枝
做选择; // 将选择加入路径
backtrack(路径, 新选择列表); // 递归
撤销选择; // 从路径中移除选择(回溯)
}
}前提条件:数组必须有序
确定边界:初始化左右指针,left=0,right=数组length - 1
循环缩小范围:
中间元素:mid = left + (right - left ) >> 1
匹配到目标值:返回index
目标值小于中间元素:left = mid + 1
目标值大于中间元素:right = mid - 1
循环条件:
left和right同时+1-1移动时:left <= right
left和right某个+1-1移动时:left < right
终止条件:left > right
类型:
同向指针:快慢指针同速移动
反向指针:收尾向中间移动
快慢指针:快慢指针以不同速移动(比如2倍)
特点:适合线性、有序(或部分有序)的数据结构
空间复杂度优化:O(n) -> O(1)
理念:每一步都是当前最优解,合起来是全局最优解
适用场景:排序好的数组、遍历
定义:用left和right表示窗口的左右边界
移动策略:
扩大窗口:right不断右移,直至窗口满足条件
收缩窗口:left不断右移,直至窗口不再满足条件
状态存储:哈希
注意点:
有可变窗口和固定窗口
剪支、边界
function slideWindow(字符串, 目标) {
let left = 0;
let right = 0;
let len = 字符串.length;
const result = 结果;
while (right < len) {
right++;
while (满足条件) {
记录结果
left++;
}
}
return 结果;
}