一、基础排序算法
- 冒泡排序
- 重复遍历数组,每次比较相邻元素并交换,将最大/最小值逐步"冒泡"到两端。
function bubbleSort(arr) {
const n = arr.length;
// 外层循环控制遍历次数,最后一次无需比较
for (let i = 0; i < n - 1; i++) {
// 内层循环进行相邻元素的比较和交换
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
- 快速排序
- 通过"分治"策略,选择一个基准值(pivot)将数组分为左右两部分,递归排序。
- 关键点:分区(Hoare/Pivot-Last)和递归终止条件。
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return arr;
// 选择最右侧元素作为pivot
const pivot = arr[right];
let i = left - 1; // 较小元素的索引
for (let j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]]; // 将较小元素移到左侧
}
}
// 将pivot放到正确的位置
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
// 递归排序左右子数组
return quickSort(arr, left, i).concat(
quickSort(arr, i + 2, right)
);
}
- 归并排序
- 将数组拆分为最小单元后合并,合并时保持有序。
function mergeSort(arr) {
if (arr.length <= 1) return arr;
// 分割数组为左右两部分
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
// 合并两个有序数组
return merge(left, right);
}
function merge(left, right) {
let result = [];
let i = 0; // 左数组指针
let j = 0; // 右数组指针
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
// 添加剩余元素
return result.concat(left.slice(i)).concat(right.slice(j));
}
二、基础数据结构特性与操作
| 数据结构 | 核心特点 | 典型操作 | 适用场景 |
|---|---|---|---|
| 数组 | 固定大小,连续内存,随机访问快 | 插入/删除 O(n),索引访问 O(1) | 需要快速随机访问的场景 |
| 链表 | 动态分配,非连续内存,插入/删除 O(1) | 访问 O(n),头尾指针操作 | 频繁插入删除的场景 |
| 栈 | 后进先出(LIFO) | Push/Pop | 函数调用栈、撤销操作 |
| 队列 | 先进先出(FIFO) | Enqueue/Dequeue | BFS、任务调度 |
三、数据结构实战应用
- 搜索框历史记录存储
- 使用 双向链表 + 哈希表 实现高效增删改查:
- 哈希表保存键值对(索引→节点),支持 O(1) 快速定位。
- 双向链表维护插入顺序,支持 O(1) 头尾插入和中间删除。
- 示例:浏览器历史记录的前进/后退功能。
四、数组去重方法
- Set 数据结构法
const unique = [...new Set(arr)];
- 时间复杂度:O(n),空间复杂度:O(n)。
- 特点:简洁高效,保留原顺序(ES6 Set 遍历顺序不确定)。
- Filter + indexOf 法
const unique = arr.filter((item, index) => index === arr.indexOf(item));
- 时间复杂度:O(n²),空间复杂度:O(n)。
- 特点:兼容旧环境,不改变原数组。
- 对象键值法
const seen = {}; return arr.filter(item => { return seen.hasOwnProperty(item) ? false : (seen[item] = true); });
五、深度优先搜索(DFS) vs 广度优先搜索(BFS)
| 对比项 | DFS | BFS |
|---|---|---|
| 实现方式 | 递归/栈 | 队列 |
| 探索顺序 | 节点深入到底层再回溯 | 层级逐层扩展 |
| 空间复杂度 | O(n)(最坏情况堆栈) | O(n)(队列存储) |
| 典型场景 | 文件系统遍历、路径查找 | 最短路径问题、层级分析 |
六、深拷贝实现
- JSON 方法
const deepCopy = JSON.parse(JSON.stringify(obj));
- 局限:无法拷贝函数、Symbol、循环引用对象。
- 递归实现
function deepCopy(obj) { if (obj === null || typeof obj !== 'object') return obj; const copy = Array.isArray(obj) ? [] : {}; for (let key in obj) { copy[key] = deepCopy(obj[key]); } return copy; }
- 关键点:处理循环引用(需记录已访问对象)。
七、动态规划解法
1. 最长递增子序列(LIS)
问题描述:在一个无序数组中找出最长递增子序列的长度。 动态规划解法:
function lengthOfLIS(nums) {
const dp = new Array(nums.length).fill(1); // dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp); // 或者直接返回 dp.reduce((a,b)=>max(a,b))
}
// 示例:nums = [10,9,2,5,3,7,101,18] → 输出 4 (对应 [2,5,7,101])
- 时间复杂度:O(n²)
- 优化版本:使用二分查找将时间复杂度降至 O(n log n)
2. 背包问题(Knapsack)
问题描述:给定物品重量和价值,在背包容量限制下最大化总价值。 动态规划解法(0-1背包):
function knapsack(weights, values, capacity) {
const dp = new Array(capacity + 1).fill(0); // dp[j] 表示容量为 j 时的最大价值
for (let i = 0; i < weights.length; i++) {
for (let j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[capacity];
}
// 示例:weights=[2,3,4], values=[3,4,5], capacity=5 → 输出 7 (选2+3)
- 前端应用场景:资源加载优化(如根据网络状况选择加载图片尺寸)
3. 编辑距离(Levenshtein Distance)
问题描述:计算两个字符串通过插入、删除、替换操作的最小步数。 动态规划解法:
function minEditDistance(str1, str2) {
const m = str1.length, n = str2.length;
const dp = [[0]*(n+1)].concat([...Array(m)].map(() => [0]*(n+1)));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i-1] === str2[j-1]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(
dp[i-1][j] + 1, // 删除
dp[i][j-1] + 1, // 插入
dp[i-1][j-1] + 1 // 替换
);
}
}
}
return dp[m][n];
}
// 示例:str1="kitten", str2="sitting" → 输出 3
- 前端应用场景:文本编辑器中的拼写检查、输入法候选词匹配
4. 矩阵链乘法
问题描述:通过加括号优化多个矩阵相乘的计算顺序。 动态规划解法:
function matrixChainOrder(p) {
const n = p.length - 1;
const dp = [[0]*n for _ in range(n)];
for (let chainLength = 2; chainLength <= n; chainLength++) { // 包含的矩阵个数
for (let i = 0; i < n - chainLength + 1; i++) {
const j = i + chainLength - 1;
dp[i][j] = Infinity;
for (let k = i; k < j; k++) {
dp[i][j] = Math.min(
dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1],
dp[i][j]
);
}
}
}
return dp[0][n-1];
}
// 示例:p = [10,20,30,40] → 输出 30000
七、防抖(Debounce)和节流(Throttle)
1. 防抖(Debounce)
核心思想:事件触发后,等待一段时间内不再触发事件,才执行函数。如果在这段时间内再次触发,则重新计时。
适用场景:搜索框输入联想、窗口大小调整(resize)、文本编辑器保存。
参数说明:
func:需要防抖的函数。wait:等待时间(毫秒)。immediate:是否立即执行(true 表示首次触发立即执行,后续等待结束后再执行)。
实现代码:
// 防抖函数实现
function debounce(func, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
2. 节流(Throttle)
核心思想:在一段时间内,无论事件触发多少次,函数最多执行一次。
适用场景:滚动事件(scroll)、鼠标移动(mousemove)、抢购按钮点击。
参数说明:
func:需要节流的函数。delay:执行间隔时间(毫秒)。
实现代码:
// 节流函数实现
function throttle(func, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (!timer) {
func.apply(context, args);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
两者的核心区别
| 防抖(Debounce) | 节流(Throttle) | |
|---|---|---|
| 执行时机 | 事件停止后执行 | 固定时间间隔执行 |
| 类比 | 电梯等人(最后一个人进后关门) | 地铁发车(每隔一段时间发一班) |
使用示例
// 防抖:输入停止 500ms 后执行搜索
const searchInput = document.getElementById("search");
searchInput.addEventListener("input", debounce(searchHandler, 500));
// 节流:每隔 200ms 处理一次滚动事件
window.addEventListener("scroll", throttle(scrollHandler, 200));