前端中的常用算法:从数据结构到性能优化
这一篇聊聊前端开发中常用的算法和数据结构:
从数组操作、字符串处理,到树和图的遍历,再到缓存、防抖节流等性能优化算法。
数组操作算法
数组去重
1// 方法1:使用 Set(最简单,但会丢失对象引用)
2function unique1(arr) {
3 return [...new Set(arr)];
4}
5
6// 方法2:使用 filter + indexOf
7function unique2(arr) {
8 return arr.filter((item, index) => arr.indexOf(item) === index);
9}
10
11// 方法3:使用 reduce
12function unique3(arr) {
13 return arr.reduce((acc, item) => {
14 if (!acc.includes(item)) {
15 acc.push(item);
16 }
17 return acc;
18 }, []);
19}
20
21// 方法4:对象去重(根据某个属性)
22function uniqueBy(arr, key) {
23 const seen = new Set();
24 return arr.filter(item => {
25 const value = item[key];
26 if (seen.has(value)) {
27 return false;
28 }
29 seen.add(value);
30 return true;
31 });
32}
数组扁平化
1// 方法1:使用 flat(ES2019)
2function flatten1(arr) {
3 return arr.flat(Infinity);
4}
5
6// 方法2:递归
7function flatten2(arr) {
8 const result = [];
9 for (const item of arr) {
10 if (Array.isArray(item)) {
11 result.push(...flatten2(item));
12 } else {
13 result.push(item);
14 }
15 }
16 return result;
17}
18
19// 方法3:使用 reduce
20function flatten3(arr) {
21 return arr.reduce((acc, item) => {
22 return acc.concat(Array.isArray(item) ? flatten3(item) : item);
23 }, []);
24}
25
26// 方法4:迭代(避免栈溢出)
27function flatten4(arr) {
28 const stack = [...arr];
29 const result = [];
30 while (stack.length > 0) {
31 const item = stack.pop();
32 if (Array.isArray(item)) {
33 stack.push(...item);
34 } else {
35 result.unshift(item);
36 }
37 }
38 return result;
39}
数组分组
1// 按条件分组
2function groupBy(arr, fn) {
3 return arr.reduce((acc, item) => {
4 const key = typeof fn === 'function' ? fn(item) : item[fn];
5 if (!acc[key]) {
6 acc[key] = [];
7 }
8 acc[key].push(item);
9 return acc;
10 }, {});
11}
12
13// 使用
14const users = [
15 { name: 'John', age: 20 },
16 { name: 'Jane', age: 25 },
17 { name: 'Bob', age: 20 }
18];
19
20groupBy(users, user => user.age);
21// { 20: [{ name: 'John', age: 20 }, { name: 'Bob', age: 20 }], 25: [{ name: 'Jane', age: 25 }] }
数组分块
1// 将数组分成指定大小的块
2function chunk(arr, size) {
3 const result = [];
4 for (let i = 0; i < arr.length; i += size) {
5 result.push(arr.slice(i, i + size));
6 }
7 return result;
8}
9
10// 使用
11chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
数组交集、并集、差集
1// 交集
2function intersection(arr1, arr2) {
3 const set2 = new Set(arr2);
4 return arr1.filter(item => set2.has(item));
5}
6
7// 并集
8function union(arr1, arr2) {
9 return [...new Set([...arr1, ...arr2])];
10}
11
12// 差集(arr1 有但 arr2 没有的)
13function difference(arr1, arr2) {
14 const set2 = new Set(arr2);
15 return arr1.filter(item => !set2.has(item));
16}
排序算法
快速排序
1function quickSort(arr) {
2 if (arr.length <= 1) return arr;
3
4 const pivot = arr[Math.floor(arr.length / 2)];
5 const left = [];
6 const middle = [];
7 const right = [];
8
9 for (const item of arr) {
10 if (item < pivot) {
11 left.push(item);
12 } else if (item > pivot) {
13 right.push(item);
14 } else {
15 middle.push(item);
16 }
17 }
18
19 return [...quickSort(left), ...middle, ...quickSort(right)];
20}
归并排序
1function mergeSort(arr) {
2 if (arr.length <= 1) return arr;
3
4 const mid = Math.floor(arr.length / 2);
5 const left = mergeSort(arr.slice(0, mid));
6 const right = mergeSort(arr.slice(mid));
7
8 return merge(left, right);
9}
10
11function merge(left, right) {
12 const result = [];
13 let i = 0, j = 0;
14
15 while (i < left.length && j < right.length) {
16 if (left[i] <= right[j]) {
17 result.push(left[i++]);
18 } else {
19 result.push(right[j++]);
20 }
21 }
22
23 return result.concat(left.slice(i)).concat(right.slice(j));
24}
堆排序
1function heapSort(arr) {
2 // 构建最大堆
3 for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
4 heapify(arr, arr.length, i);
5 }
6
7 // 逐个取出堆顶元素
8 for (let i = arr.length - 1; i > 0; i--) {
9 [arr[0], arr[i]] = [arr[i], arr[0]];
10 heapify(arr, i, 0);
11 }
12
13 return arr;
14}
15
16function heapify(arr, n, i) {
17 let largest = i;
18 const left = 2 * i + 1;
19 const right = 2 * i + 2;
20
21 if (left < n && arr[left] > arr[largest]) {
22 largest = left;
23 }
24
25 if (right < n && arr[right] > arr[largest]) {
26 largest = right;
27 }
28
29 if (largest !== i) {
30 [arr[i], arr[largest]] = [arr[largest], arr[i]];
31 heapify(arr, n, largest);
32 }
33}
查找算法
二分查找
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left <= right) {
6 const mid = Math.floor((left + right) / 2);
7
8 if (arr[mid] === target) {
9 return mid;
10 } else if (arr[mid] < target) {
11 left = mid + 1;
12 } else {
13 right = mid - 1;
14 }
15 }
16
17 return -1;
18}
19
20// 查找第一个大于等于 target 的位置
21function lowerBound(arr, target) {
22 let left = 0;
23 let right = arr.length;
24
25 while (left < right) {
26 const mid = Math.floor((left + right) / 2);
27 if (arr[mid] < target) {
28 left = mid + 1;
29 } else {
30 right = mid;
31 }
32 }
33
34 return left;
35}
深度优先搜索(DFS)
1// 图的 DFS
2function dfs(graph, start, visited = new Set()) {
3 visited.add(start);
4 console.log(start);
5
6 for (const neighbor of graph[start] || []) {
7 if (!visited.has(neighbor)) {
8 dfs(graph, neighbor, visited);
9 }
10 }
11}
12
13// 迭代版本
14function dfsIterative(graph, start) {
15 const stack = [start];
16 const visited = new Set();
17
18 while (stack.length > 0) {
19 const node = stack.pop();
20 if (!visited.has(node)) {
21 visited.add(node);
22 console.log(node);
23
24 for (const neighbor of graph[node] || []) {
25 if (!visited.has(neighbor)) {
26 stack.push(neighbor);
27 }
28 }
29 }
30 }
31}
广度优先搜索(BFS)
1function bfs(graph, start) {
2 const queue = [start];
3 const visited = new Set([start]);
4
5 while (queue.length > 0) {
6 const node = queue.shift();
7 console.log(node);
8
9 for (const neighbor of graph[node] || []) {
10 if (!visited.has(neighbor)) {
11 visited.add(neighbor);
12 queue.push(neighbor);
13 }
14 }
15 }
16}
字符串算法
字符串匹配:KMP 算法
1function kmpSearch(text, pattern) {
2 const lps = computeLPS(pattern);
3 let i = 0; // text 的索引
4 let j = 0; // pattern 的索引
5
6 while (i < text.length) {
7 if (text[i] === pattern[j]) {
8 i++;
9 j++;
10 }
11
12 if (j === pattern.length) {
13 return i - j; // 找到匹配
14 } else if (i < text.length && text[i] !== pattern[j]) {
15 if (j !== 0) {
16 j = lps[j - 1];
17 } else {
18 i++;
19 }
20 }
21 }
22
23 return -1;
24}
25
26function computeLPS(pattern) {
27 const lps = new Array(pattern.length).fill(0);
28 let len = 0;
29 let i = 1;
30
31 while (i < pattern.length) {
32 if (pattern[i] === pattern[len]) {
33 len++;
34 lps[i] = len;
35 i++;
36 } else {
37 if (len !== 0) {
38 len = lps[len - 1];
39 } else {
40 lps[i] = 0;
41 i++;
42 }
43 }
44 }
45
46 return lps;
47}
最长公共子序列(LCS)
1function lcs(str1, str2) {
2 const m = str1.length;
3 const n = str2.length;
4 const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
5
6 for (let i = 1; i <= m; i++) {
7 for (let j = 1; j <= n; j++) {
8 if (str1[i - 1] === str2[j - 1]) {
9 dp[i][j] = dp[i - 1][j - 1] + 1;
10 } else {
11 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
12 }
13 }
14 }
15
16 // 回溯构建 LCS
17 let i = m, j = n;
18 const result = [];
19 while (i > 0 && j > 0) {
20 if (str1[i - 1] === str2[j - 1]) {
21 result.unshift(str1[i - 1]);
22 i--;
23 j--;
24 } else if (dp[i - 1][j] > dp[i][j - 1]) {
25 i--;
26 } else {
27 j--;
28 }
29 }
30
31 return result.join('');
32}
编辑距离(Levenshtein Distance)
1function editDistance(str1, str2) {
2 const m = str1.length;
3 const n = str2.length;
4 const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
5
6 // 初始化
7 for (let i = 0; i <= m; i++) {
8 dp[i][0] = i;
9 }
10 for (let j = 0; j <= n; j++) {
11 dp[0][j] = j;
12 }
13
14 // 填充 dp 表
15 for (let i = 1; i <= m; i++) {
16 for (let j = 1; j <= n; j++) {
17 if (str1[i - 1] === str2[j - 1]) {
18 dp[i][j] = dp[i - 1][j - 1];
19 } else {
20 dp[i][j] = Math.min(
21 dp[i - 1][j] + 1, // 删除
22 dp[i][j - 1] + 1, // 插入
23 dp[i - 1][j - 1] + 1 // 替换
24 );
25 }
26 }
27 }
28
29 return dp[m][n];
30}
树和图的算法
二叉树遍历
1class TreeNode {
2 constructor(val) {
3 this.val = val;
4 this.left = null;
5 this.right = null;
6 }
7}
8
9// 前序遍历(根-左-右)
10function preorderTraversal(root) {
11 if (!root) return [];
12 return [
13 root.val,
14 ...preorderTraversal(root.left),
15 ...preorderTraversal(root.right)
16 ];
17}
18
19// 中序遍历(左-根-右)
20function inorderTraversal(root) {
21 if (!root) return [];
22 return [
23 ...inorderTraversal(root.left),
24 root.val,
25 ...inorderTraversal(root.right)
26 ];
27}
28
29// 后序遍历(左-右-根)
30function postorderTraversal(root) {
31 if (!root) return [];
32 return [
33 ...postorderTraversal(root.left),
34 ...postorderTraversal(root.right),
35 root.val
36 ];
37}
38
39// 层序遍历(BFS)
40function levelOrderTraversal(root) {
41 if (!root) return [];
42
43 const result = [];
44 const queue = [root];
45
46 while (queue.length > 0) {
47 const level = [];
48 const size = queue.length;
49
50 for (let i = 0; i < size; i++) {
51 const node = queue.shift();
52 level.push(node.val);
53
54 if (node.left) queue.push(node.left);
55 if (node.right) queue.push(node.right);
56 }
57
58 result.push(level);
59 }
60
61 return result;
62}
DOM 树遍历
1// 深度优先遍历 DOM 树
2function traverseDOM(node, callback) {
3 callback(node);
4
5 for (const child of node.children) {
6 traverseDOM(child, callback);
7 }
8}
9
10// 查找所有匹配选择器的元素
11function querySelectorAll(root, selector) {
12 const result = [];
13
14 function matches(node, selector) {
15 // 简化的匹配逻辑(实际应该用浏览器 API)
16 return node.matches && node.matches(selector);
17 }
18
19 function traverse(node) {
20 if (matches(node, selector)) {
21 result.push(node);
22 }
23
24 for (const child of node.children) {
25 traverse(child);
26 }
27 }
28
29 traverse(root);
30 return result;
31}
虚拟 DOM Diff 算法(简化版)
1function diff(oldTree, newTree) {
2 const patches = {};
3 let index = 0;
4
5 walk(oldTree, newTree, index, patches);
6 return patches;
7}
8
9function walk(oldNode, newNode, index, patches) {
10 const currentPatches = [];
11
12 if (!newNode) {
13 // 节点被删除
14 currentPatches.push({ type: 'REMOVE', index });
15 } else if (typeof oldNode === 'string' && typeof newNode === 'string') {
16 // 文本节点
17 if (oldNode !== newNode) {
18 currentPatches.push({ type: 'TEXT', content: newNode });
19 }
20 } else if (oldNode.type === newNode.type) {
21 // 相同类型的节点,比较属性和子节点
22 const attrs = diffAttrs(oldNode.props, newNode.props);
23 if (Object.keys(attrs).length > 0) {
24 currentPatches.push({ type: 'ATTR', attrs });
25 }
26
27 diffChildren(oldNode.children, newNode.children, index, patches);
28 } else {
29 // 节点类型不同,直接替换
30 currentPatches.push({ type: 'REPLACE', newNode });
31 }
32
33 if (currentPatches.length > 0) {
34 patches[index] = currentPatches;
35 }
36}
37
38function diffAttrs(oldAttrs, newAttrs) {
39 const patches = {};
40
41 // 找出变化的属性
42 for (const key in newAttrs) {
43 if (oldAttrs[key] !== newAttrs[key]) {
44 patches[key] = newAttrs[key];
45 }
46 }
47
48 // 找出被删除的属性
49 for (const key in oldAttrs) {
50 if (!(key in newAttrs)) {
51 patches[key] = undefined;
52 }
53 }
54
55 return patches;
56}
57
58function diffChildren(oldChildren, newChildren, index, patches) {
59 let currentIndex = index;
60
61 oldChildren.forEach((oldChild, i) => {
62 const newChild = newChildren[i];
63 currentIndex++;
64 walk(oldChild, newChild, currentIndex, patches);
65 });
66}
缓存算法
LRU 缓存(Least Recently Used)
1class LRUCache {
2 constructor(capacity) {
3 this.capacity = capacity;
4 this.cache = new Map();
5 }
6
7 get(key) {
8 if (this.cache.has(key)) {
9 // 将访问的项移到末尾(最近使用)
10 const value = this.cache.get(key);
11 this.cache.delete(key);
12 this.cache.set(key, value);
13 return value;
14 }
15 return -1;
16 }
17
18 put(key, value) {
19 if (this.cache.has(key)) {
20 // 更新已存在的项
21 this.cache.delete(key);
22 } else if (this.cache.size >= this.capacity) {
23 // 删除最久未使用的项(第一个)
24 const firstKey = this.cache.keys().next().value;
25 this.cache.delete(firstKey);
26 }
27
28 this.cache.set(key, value);
29 }
30}
LFU 缓存(Least Frequently Used)
1class LFUCache {
2 constructor(capacity) {
3 this.capacity = capacity;
4 this.cache = new Map(); // key -> { value, frequency }
5 this.freqMap = new Map(); // frequency -> Set of keys
6 this.minFreq = 0;
7 }
8
9 get(key) {
10 if (!this.cache.has(key)) return -1;
11
12 const item = this.cache.get(key);
13 this.updateFrequency(key);
14 return item.value;
15 }
16
17 put(key, value) {
18 if (this.capacity === 0) return;
19
20 if (this.cache.has(key)) {
21 this.cache.get(key).value = value;
22 this.updateFrequency(key);
23 } else {
24 if (this.cache.size >= this.capacity) {
25 this.evict();
26 }
27
28 this.cache.set(key, { value, frequency: 1 });
29 this.addToFreqMap(key, 1);
30 this.minFreq = 1;
31 }
32 }
33
34 updateFrequency(key) {
35 const item = this.cache.get(key);
36 const oldFreq = item.frequency;
37 item.frequency++;
38
39 this.removeFromFreqMap(key, oldFreq);
40 this.addToFreqMap(key, oldFreq + 1);
41
42 if (oldFreq === this.minFreq && (!this.freqMap.get(oldFreq) || this.freqMap.get(oldFreq).size === 0)) {
43 this.minFreq++;
44 }
45 }
46
47 addToFreqMap(key, freq) {
48 if (!this.freqMap.has(freq)) {
49 this.freqMap.set(freq, new Set());
50 }
51 this.freqMap.get(freq).add(key);
52 }
53
54 removeFromFreqMap(key, freq) {
55 const set = this.freqMap.get(freq);
56 if (set) {
57 set.delete(key);
58 if (set.size === 0) {
59 this.freqMap.delete(freq);
60 }
61 }
62 }
63
64 evict() {
65 const keys = this.freqMap.get(this.minFreq);
66 const keyToRemove = keys.values().next().value;
67 keys.delete(keyToRemove);
68 this.cache.delete(keyToRemove);
69
70 if (keys.size === 0) {
71 this.freqMap.delete(this.minFreq);
72 }
73 }
74}
性能优化算法
防抖(Debounce)
1function debounce(func, delay) {
2 let timeoutId;
3 return function (...args) {
4 clearTimeout(timeoutId);
5 timeoutId = setTimeout(() => func.apply(this, args), delay);
6 };
7}
8
9// 立即执行版本
10function debounceImmediate(func, delay) {
11 let timeoutId;
12 let immediate = true;
13
14 return function (...args) {
15 if (immediate) {
16 func.apply(this, args);
17 immediate = false;
18 }
19
20 clearTimeout(timeoutId);
21 timeoutId = setTimeout(() => {
22 immediate = true;
23 }, delay);
24 };
25}
节流(Throttle)
1// 时间戳版本
2function throttle(func, delay) {
3 let lastCall = 0;
4 return function (...args) {
5 const now = Date.now();
6 if (now - lastCall >= delay) {
7 lastCall = now;
8 func.apply(this, args);
9 }
10 };
11}
12
13// 定时器版本
14function throttleTimer(func, delay) {
15 let timeoutId;
16 return function (...args) {
17 if (!timeoutId) {
18 timeoutId = setTimeout(() => {
19 func.apply(this, args);
20 timeoutId = null;
21 }, delay);
22 }
23 };
24}
25
26// 结合版本(第一次和最后一次都会执行)
27function throttleBoth(func, delay) {
28 let timeoutId;
29 let lastCall = 0;
30
31 return function (...args) {
32 const now = Date.now();
33 const remaining = delay - (now - lastCall);
34
35 if (remaining <= 0) {
36 lastCall = now;
37 func.apply(this, args);
38 } else if (!timeoutId) {
39 timeoutId = setTimeout(() => {
40 lastCall = Date.now();
41 timeoutId = null;
42 func.apply(this, args);
43 }, remaining);
44 }
45 };
46}
函数记忆化(Memoization)
1function memoize(fn) {
2 const cache = new Map();
3
4 return function (...args) {
5 const key = JSON.stringify(args);
6
7 if (cache.has(key)) {
8 return cache.get(key);
9 }
10
11 const result = fn.apply(this, args);
12 cache.set(key, result);
13 return result;
14 };
15}
16
17// 带过期时间的记忆化
18function memoizeWithTTL(fn, ttl = 60000) {
19 const cache = new Map();
20
21 return function (...args) {
22 const key = JSON.stringify(args);
23 const cached = cache.get(key);
24
25 if (cached && Date.now() - cached.timestamp < ttl) {
26 return cached.value;
27 }
28
29 const result = fn.apply(this, args);
30 cache.set(key, {
31 value: result,
32 timestamp: Date.now()
33 });
34
35 return result;
36 };
37}
其他实用算法
洗牌算法(Fisher-Yates)
1function shuffle(arr) {
2 const result = [...arr];
3 for (let i = result.length - 1; i > 0; i--) {
4 const j = Math.floor(Math.random() * (i + 1));
5 [result[i], result[j]] = [result[j], result[i]];
6 }
7 return result;
8}
加权随机选择
1function weightedRandom(items, weights) {
2 const totalWeight = weights.reduce((sum, w) => sum + w, 0);
3 let random = Math.random() * totalWeight;
4
5 for (let i = 0; i < items.length; i++) {
6 random -= weights[i];
7 if (random <= 0) {
8 return items[i];
9 }
10 }
11
12 return items[items.length - 1];
13}
滑动窗口
1// 最大子数组和
2function maxSubarraySum(arr, k) {
3 let maxSum = 0;
4 let windowSum = 0;
5
6 // 计算第一个窗口
7 for (let i = 0; i < k; i++) {
8 windowSum += arr[i];
9 }
10 maxSum = windowSum;
11
12 // 滑动窗口
13 for (let i = k; i < arr.length; i++) {
14 windowSum = windowSum - arr[i - k] + arr[i];
15 maxSum = Math.max(maxSum, windowSum);
16 }
17
18 return maxSum;
19}
小结
前端开发中常用的算法和数据结构:
- 数组操作:去重、扁平化、分组、分块、集合运算
- 排序算法:快速排序、归并排序、堆排序
- 查找算法:二分查找、DFS、BFS
- 字符串算法:KMP、LCS、编辑距离
- 树和图:遍历、DOM 操作、虚拟 DOM Diff
- 缓存算法:LRU、LFU
- 性能优化:防抖、节流、记忆化
- 其他实用算法:洗牌、加权随机、滑动窗口
理解这些算法,可以帮助你:
- 写出更高效的代码
- 解决复杂的数据处理问题
- 优化前端性能
- 在面试和实际项目中游刃有余
在实际开发中,很多场景都可以用这些算法来解决,比如:
- 列表去重和分组
- 搜索和过滤
- 虚拟滚动(需要计算可见区域)
- 缓存管理
- 性能优化(防抖节流)