前端算法与手写题集
目标:覆盖前端面试 90% 的算法考点,每道题含思路、代码、复杂度分析。
建议:先自己做 15 分钟再看答案;手写题要能默写。
目录
- 一、使用说明与刷题策略
- [二、数组与字符串(15 题)](#二、数组与字符串(15 题))
- [三、链表(8 题)](#三、链表(8 题))
- [四、二叉树(10 题)](#四、二叉树(10 题))
- [五、动态规划(8 题)](#五、动态规划(8 题))
- [六、排序与二分查找(6 题)](#六、排序与二分查找(6 题))
- [七、回溯与递归(5 题)](#七、回溯与递归(5 题))
- [八、设计题:LRU 缓存](#八、设计题:LRU 缓存)
- [九、前端手写题专题(12 题)](#九、前端手写题专题(12 题))
- [十、综合场景题(5 题)](#十、综合场景题(5 题))
一、使用说明与刷题策略
难度分布(按面试频率)
| 难度 | 占比 | 要求 |
|---|---|---|
| ⭐ 基础 | 30% | 必须秒答(双指针、哈希、基础递归) |
| ⭐⭐ 中等 | 55% | 15 分钟内写出 bug-free 代码 |
| ⭐⭐⭐ 困难 | 15% | 能讲清思路即可,如 LRU、并发控制 |
刷题顺序建议
- 先刷"前端手写题专题"(九)------这是前端面试区别于纯算法岗的核心
- 数组双指针 + 哈希(二)打基础
- 链表 + 二叉树递归套路(三、四)------递归思维是分水岭
- 动态规划(五)掌握 5 个经典模型即可
- 每天 2 题,每题限时 25 分钟,做完立刻复盘写"错因笔记"
面试答题三步法
- 复述 + 确认:复述题目,确认输入输出、边界(空数组、负数、超大数据)
- 讲思路:先说暴力解,再说优化方向("我先想一个 O(n²) 的,然后看能不能用哈希降到 O(n)")
- 写代码:边写边说关键行;写完主动跑两个用例(含边界)
二、数组与字符串
2.1 两数之和 ⭐
题目:给定数组和 target,返回和为 target 的两个下标。
js
// 哈希表:边遍历边存,找 complement
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (map.has(need)) return [map.get(need), i];
map.set(nums[i], i);
}
return [];
}
// 时间 O(n),空间 O(n)
2.2 无重复字符的最长子串 ⭐⭐
题目:字符串中不含重复字符的最长连续子串长度。
js
// 滑动窗口:right 扩张,遇到重复收缩 left 到重复字符之后
function lengthOfLongestSubstring(s) {
const set = new Set();
let left = 0, max = 0;
for (let right = 0; right < s.length; right++) {
while (set.has(s[right])) {
set.delete(s[left++]);
}
set.add(s[right]);
max = Math.max(max, right - left + 1);
}
return max;
}
// 时间 O(n):每个字符最多进出 Set 一次
2.3 合并两个有序数组 ⭐
js
// 双指针从后往前,避免覆盖
function merge(nums1, m, nums2, n) {
let i = m - 1, j = n - 1, k = m + n - 1;
while (j >= 0) {
nums1[k--] = (i >= 0 && nums1[i] > nums2[j]) ? nums1[i--] : nums2[j--];
}
}
2.4 移动零 ⭐
js
// 双指针:非零数前移,末尾补零
function moveZeroes(nums) {
let p = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) [nums[p], nums[i]] = [nums[i], nums[p]], p++;
}
}
2.5 三数之和 ⭐⭐
题目:找出所有和为 0 的不重复三元组。
js
// 排序 + 固定一个数 + 双指针 + 去重
function threeSum(nums) {
nums.sort((a, b) => a - b);
const res = [];
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue; // 去重 i
let l = i + 1, r = nums.length - 1;
while (l < r) {
const sum = nums[i] + nums[l] + nums[r];
if (sum < 0) l++;
else if (sum > 0) r--;
else {
res.push([nums[i], nums[l], nums[r]]);
while (l < r && nums[l] === nums[l + 1]) l++; // 去重 l
while (l < r && nums[r] === nums[r - 1]) r--; // 去重 r
l++; r--;
}
}
}
return res;
}
// 时间 O(n²)
2.6 数组中第 K 大的元素 ⭐⭐
js
// 快速选择:partition 后只递归一边,平均 O(n)
function findKthLargest(nums, k) {
const target = nums.length - k; // 升序下标
let lo = 0, hi = nums.length - 1;
while (lo < hi) {
const p = partition(nums, lo, hi);
if (p === target) return nums[p];
p < target ? lo = p + 1 : hi = p - 1;
}
return nums[lo];
function partition(arr, lo, hi) {
const pivot = arr[hi];
let i = lo;
for (let j = lo; j < hi; j++) {
if (arr[j] < pivot) [arr[i], arr[j]] = [arr[j], arr[i]], i++;
}
[arr[i], arr[hi]] = [arr[hi], arr[i]];
return i;
}
}
2.7 滑动窗口最大值 ⭐⭐⭐
js
// 单调队列:存下标,维护队列内值递减
function maxSlidingWindow(nums, k) {
const deque = []; // 存下标
const res = [];
for (let i = 0; i < nums.length; i++) {
while (deque.length && nums[deque[deque.length - 1]] <= nums[i]) deque.pop();
deque.push(i);
if (deque[0] <= i - k) deque.shift(); // 滑出窗口
if (i >= k - 1) res.push(nums[deque[0]]);
}
return res;
}
2.8 盛最多水的容器 ⭐⭐
js
// 双指针:短板决定高度,移动短板才可能变大
function maxArea(height) {
let l = 0, r = height.length - 1, max = 0;
while (l < r) {
max = Math.max(max, (r - l) * Math.min(height[l], height[r]));
height[l] < height[r] ? l++ : r--;
}
return max;
}
2.9 最大子数组和 ⭐(DP 入门)
js
function maxSubArray(nums) {
let cur = nums[0], max = nums[0];
for (let i = 1; i < nums.length; i++) {
cur = Math.max(nums[i], cur + nums[i]); // 要么另起炉灶,要么接着加
max = Math.max(max, cur);
}
return max;
}
2.10 字符串相关基础 ⭐
js
// 反转字符串(原地)
const reverseString = s => { for (let i = 0, j = s.length - 1; i < j; i++, j--) [s[i], s[j]] = [s[j], s[i]]; };
// 回文判断
const isPalindrome = s => { s = s.replace(/[^a-z0-9]/gi, '').toLowerCase(); return s === [...s].reverse().join(''); };
// 词频统计
const wordCount = str => str.split(/\s+/).reduce((m, w) => (m[w] = (m[w] || 0) + 1, m), {});
2.11 轮转数组 ⭐
js
// 三次翻转:[1,2,3,4,5,6,7] k=3 → [5,6,7,1,2,3,4]
function rotate(nums, k) {
k %= nums.length;
const rev = (l, r) => { while (l < r) [nums[l], nums[r]] = [nums[r], nums[l]], l++, r--; };
rev(0, nums.length - 1); rev(0, k - 1); rev(k, nums.length - 1);
}
2.12 接雨水 ⭐⭐⭐(双指针经典)
js
function trap(height) {
let l = 0, r = height.length - 1, lMax = 0, rMax = 0, water = 0;
while (l < r) {
if (height[l] < height[r]) {
lMax = Math.max(lMax, height[l]);
water += lMax - height[l++];
} else {
rMax = Math.max(rMax, height[r]);
water += rMax - height[r--];
}
}
return water;
}
// 思路:某处能接的水 = min(左右最高) - 当前高度;矮的那边先结算
2.13 找到所有数组中消失的数字 ⭐
js
// 原地标记:把 nums[i]-1 处的数取负
function findDisappearedNumbers(nums) {
nums.forEach(n => { const i = Math.abs(n) - 1; nums[i] = -Math.abs(nums[i]); });
return nums.map((n, i) => n > 0 ? i + 1 : null).filter(Boolean);
}
2.14 和为 K 的子数组个数 ⭐⭐(前缀和 + 哈希)
js
function subarraySum(nums, k) {
const map = new Map([[0, 1]]); // 前缀和 → 出现次数
let sum = 0, count = 0;
for (const n of nums) {
sum += n;
count += map.get(sum - k) || 0; // 之前有多少个前缀和为 sum-k
map.set(sum, (map.get(sum) || 0) + 1);
}
return count;
}
// 时间 O(n),空间 O(n)
2.15 最长连续序列 ⭐⭐
js
// 哈希集合:只从序列起点(num-1 不在集合中)开始统计
function longestConsecutive(nums) {
const set = new Set(nums);
let max = 0;
for (const n of set) {
if (!set.has(n - 1)) {
let cur = n, len = 1;
while (set.has(cur + 1)) cur++, len++;
max = Math.max(max, len);
}
}
return max;
}
// 时间 O(n):每个数最多被访问两次
三、链表
统一节点定义:
function ListNode(val, next) { this.val = val; this.next = next || null; }
3.1 反转链表 ⭐
js
function reverseList(head) {
let prev = null, cur = head;
while (cur) {
[cur.next, prev, cur] = [prev, cur, cur.next]; // 三指针(解构写法)
}
return prev;
}
3.2 判断链表是否有环 ⭐
js
// 快慢指针:快的每次走 2 步,相遇即有环
function hasCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
3.3 链表中环的入口 ⭐⭐
js
// 相遇后,一个指针回到头,两者同速前进,再次相遇即入口
function detectCycle(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next; fast = fast.next.next;
if (slow === fast) {
let p = head;
while (p !== slow) { p = p.next; slow = slow.next; }
return p;
}
}
return null;
}
// 数学:相遇点到入口距离 = 头到入口距离
3.4 合并两个有序链表 ⭐(递归)
js
function mergeTwoLists(l1, l2) {
if (!l1) return l2;
if (!l2) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
3.5 链表的中间节点 ⭐
js
// 快慢指针:快指针到末尾时,慢指针在中间
function middleNode(head) {
let slow = head, fast = head;
while (fast && fast.next) { slow = slow.next; fast = fast.next.next; }
return slow;
}
3.6 删除链表倒数第 N 个节点 ⭐⭐
js
// 哑节点 + 双指针保持 n 距离
function removeNthFromEnd(head, n) {
const dummy = new ListNode(0, head);
let first = dummy, second = dummy;
for (let i = 0; i <= n; i++) first = first.next; // 先走 n+1 步
while (first) { first = first.next; second = second.next; }
second.next = second.next.next;
return dummy.next;
}
3.7 两数相加(逆序存储的数字)⭐⭐
js
function addTwoNumbers(l1, l2) {
const dummy = new ListNode(0);
let cur = dummy, carry = 0;
while (l1 || l2 || carry) {
const sum = (l1?.val || 0) + (l2?.val || 0) + carry;
carry = Math.floor(sum / 10);
cur.next = new ListNode(sum % 10);
cur = cur.next;
l1 = l1?.next; l2 = l2?.next;
}
return dummy.next;
}
3.8 回文链表 ⭐⭐
js
// 三步:找中点 → 反转后半 → 比较
function isPalindrome(head) {
let slow = head, fast = head;
while (fast.next && fast.next.next) { slow = slow.next; fast = fast.next.next; }
// 反转后半段
let prev = null, cur = slow.next;
while (cur) { [cur.next, prev, cur] = [prev, cur, cur.next]; }
// 比较
let p = head, q = prev;
while (q) { if (p.val !== q.val) return false; p = p.next; q = q.next; }
return true;
}
四、二叉树
4.1 前中后序遍历(递归 + 迭代)⭐
js
// 递归
function preorder(root, res = []) {
if (!root) return res;
res.push(root.val);
preorder(root.left, res);
preorder(root.right, res);
return res;
}
// 迭代:前序(栈,先右后左);中序(一路向左入栈,弹出时访问并转右)
function inorderIter(root) {
const res = [], stack = [];
let cur = root;
while (cur || stack.length) {
while (cur) { stack.push(cur); cur = cur.left; }
cur = stack.pop();
res.push(cur.val);
cur = cur.right;
}
return res;
}
4.2 层序遍历(BFS)⭐
js
function levelOrder(root) {
if (!root) return [];
const res = [], queue = [root];
while (queue.length) {
const size = queue.length;
const level = [];
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
}
return res;
}
4.3 二叉树最大深度 ⭐
js
const maxDepth = root => !root ? 0 : 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
4.4 判断平衡二叉树 ⭐⭐
js
// 自底向上:返回高度,-1 表示不平衡
function isBalanced(root) {
const dfs = node => {
if (!node) return 0;
const l = dfs(node.left), r = dfs(node.right);
if (l === -1 || r === -1 || Math.abs(l - r) > 1) return -1;
return Math.max(l, r) + 1;
};
return dfs(root) !== -1;
}
4.5 二叉树最近公共祖先(LCA)⭐⭐
js
// 递归:p、q 在左右子树各一个时,当前节点就是 LCA
function lowestCommonAncestor(root, p, q) {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root;
return left || right;
}
4.6 二叉搜索树(BST)验证 ⭐⭐
js
// BST 中序遍历应为递增序列
function isValidBST(root, min = -Infinity, max = Infinity) {
if (!root) return true;
if (root.val <= min || root.val >= max) return false;
return isValidBST(root.left, min, root.val) && isValidBST(root.right, root.val, max);
}
4.7 BST 中第 K 小的元素 ⭐⭐
js
// 中序遍历到第 k 个
function kthSmallest(root, k) {
const stack = [];
let cur = root;
while (cur || stack.length) {
while (cur) { stack.push(cur); cur = cur.left; }
cur = stack.pop();
if (--k === 0) return cur.val;
cur = cur.right;
}
}
4.8 二叉树的右视图 ⭐⭐
js
// BFS:每层最后一个节点
function rightSideView(root) {
if (!root) return [];
const res = [], queue = [root];
while (queue.length) {
const size = queue.length;
for (let i = 0; i < size; i++) {
const node = queue.shift();
if (i === size - 1) res.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return res;
}
4.9 路径总和 ⭐⭐
js
// 递归:剩余目标值减去当前节点值
function hasPathSum(root, targetSum) {
if (!root) return false;
if (!root.left && !root.right) return root.val === targetSum;
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
4.10 对称二叉树 ⭐⭐
js
function isSymmetric(root) {
const check = (a, b) => {
if (!a && !b) return true;
if (!a || !b || a.val !== b.val) return false;
return check(a.left, b.right) && check(a.right, b.left);
};
return check(root.left, root.right);
}
五、动态规划
解题四步法:1) 定义 dp 数组含义 2) 找状态转移方程 3) 确定初始值 4) 确定遍历顺序
5.1 爬楼梯 ⭐
js
// dp[i] = dp[i-1] + dp[i-2],空间可压缩到 O(1)
function climbStairs(n) {
let a = 1, b = 1;
for (let i = 2; i <= n; i++) [a, b] = [b, a + b];
return b;
}
5.2 斐波那契数列 ⭐
js
const fib = n => { let a = 0, b = 1; for (let i = 0; i < n; i++) [a, b] = [b, a + b]; return a; };
5.3 打家劫舍 ⭐⭐
js
// dp[i] = max(dp[i-1], dp[i-2] + nums[i])
function rob(nums) {
let prev2 = 0, prev1 = 0;
for (const n of nums) [prev2, prev1] = [prev1, Math.max(prev1, prev2 + n)];
return prev1;
}
5.4 最长递增子序列(LIS)⭐⭐
js
// dp[i]:以 nums[i] 结尾的 LIS 长度
function lengthOfLIS(nums) {
const dp = new Array(nums.length).fill(1);
let max = 1;
for (let i = 1; 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);
}
max = Math.max(max, dp[i]);
}
return max;
}
// O(n²);进阶:二分 O(n log n),dp[len] 表示长度为 len 的最小末尾
function lengthOfLISBinary(nums) {
const tails = [];
for (const n of nums) {
let lo = 0, hi = tails.length;
while (lo < hi) { const mid = (lo + hi) >> 1; tails[mid] < n ? lo = mid + 1 : hi = mid; }
tails[lo] = n;
}
return tails.length;
}
5.5 零钱兑换 ⭐⭐⭐
js
// dp[i] = 凑出面额 i 的最少硬币数
function coinChange(coins, amount) {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const c of coins) {
if (c <= i) dp[i] = Math.min(dp[i], dp[i - c] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
5.6 最长公共子序列(LCS)⭐⭐⭐
js
// dp[i][j]:text1 前 i 个 与 text2 前 j 个的 LCS 长度
function longestCommonSubsequence(s1, s2) {
const m = s1.length, n = s2.length;
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = s1[i - 1] === s2[j - 1]
? dp[i - 1][j - 1] + 1
: Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m][n];
}
5.7 编辑距离 ⭐⭐⭐
js
// dp[i][j]:word1 前 i 个变成 word2 前 j 个的最少操作数
function minDistance(word1, word2) {
const m = word1.length, n = word2.length;
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...new Array(n).fill(0)]);
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = word1[i - 1] === word2[j - 1]
? dp[i - 1][j - 1]
: Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1; // 删/增/换
}
}
return dp[m][n];
}
5.8 完全平方数 ⭐⭐
js
// dp[i] = min(dp[i - j*j]) + 1
function numSquares(n) {
const dp = new Array(n + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= n; i++) {
for (let j = 1; j * j <= i; j++) dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
return dp[n];
}
六、排序与二分查找
6.1 快速排序 ⭐⭐(必须手写)
js
function quickSort(arr, lo = 0, hi = arr.length - 1) {
if (lo >= hi) return arr;
const p = partition(arr, lo, hi);
quickSort(arr, lo, p - 1);
quickSort(arr, p + 1, hi);
return arr;
function partition(arr, lo, hi) {
const pivot = arr[hi]; // 取末元素为基准
let i = lo; // i:小于区的下一个位置
for (let j = lo; j < hi; j++) {
if (arr[j] < pivot) [arr[i], arr[j]] = [arr[j], arr[i]], i++;
}
[arr[i], arr[hi]] = [arr[hi], arr[i]];
return i;
}
}
// 平均 O(n log n),最坏 O(n²)(已排序 + 固定基准),不稳定
// 优化:随机基准 / 三数取中 / 小区间插排
6.2 归并排序 ⭐⭐(稳定,链表排序首选)
js
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = arr.length >> 1;
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
function merge(a, b) {
const res = [];
let i = 0, j = 0;
while (i < a.length && j < b.length) res.push(a[i] < b[j] ? a[i++] : b[j++]);
return res.concat(a.slice(i), b.slice(j));
}
}
// O(n log n),稳定,空间 O(n)
6.3 二分查找 ⭐
js
function binarySearch(nums, target) {
let lo = 0, hi = nums.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (nums[mid] === target) return mid;
nums[mid] < target ? lo = mid + 1 : hi = mid - 1;
}
return -1;
}
// O(log n)
6.4 搜索插入位置 ⭐
js
// 找第一个 >= target 的位置( lower_bound )
function searchInsert(nums, target) {
let lo = 0, hi = nums.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
nums[mid] < target ? lo = mid + 1 : hi = mid;
}
return lo;
}
6.5 寻找旋转排序数组中的最小值 ⭐⭐
js
// 与右端点比较:mid > right 说明最小值在右半
function findMin(nums) {
let lo = 0, hi = nums.length - 1;
while (lo < hi) {
const mid = (lo + hi) >> 1;
nums[mid] > nums[hi] ? lo = mid + 1 : hi = mid;
}
return nums[lo];
}
6.6 前 K 个高频元素 ⭐⭐
js
// 哈希计数 + 桶排序(O(n))或最小堆(O(n log k))
function topKFrequent(nums, k) {
const freq = new Map();
nums.forEach(n => freq.set(n, (freq.get(n) || 0) + 1));
const buckets = [];
for (const [n, f] of freq) (buckets[f] ??= []).push(n);
const res = [];
for (let i = buckets.length - 1; i >= 0 && res.length < k; i--) {
if (buckets[i]) res.push(...buckets[i]);
}
return res.slice(0, k);
}
七、回溯与递归
7.1 全排列 ⭐⭐(回溯模板)
js
function permute(nums) {
const res = [], path = [];
const backtrack = used => {
if (path.length === nums.length) { res.push([...path]); return; }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.push(nums[i]);
backtrack(used);
path.pop(); // 撤销选择
used[i] = false;
}
};
backtrack({});
return res;
}
// 回溯三要素:路径、选择列表、结束条件
7.2 子集 ⭐⭐
js
function subsets(nums) {
const res = [], path = [];
const backtrack = start => {
res.push([...path]); // 每个节点都是子集
for (let i = start; i < nums.length; i++) {
path.push(nums[i]);
backtrack(i + 1); // 从 i+1 开始防重复
path.pop();
}
};
backtrack(0);
return res;
}
7.3 电话号码的字母组合 ⭐⭐
js
function letterCombinations(digits) {
if (!digits) return [];
const map = { 2: 'abc', 3: 'def', 4: 'ghi', 5: 'jkl', 6: 'mno', 7: 'pqrs', 8: 'tuv', 9: 'wxyz' };
const res = [];
const backtrack = (idx, path) => {
if (idx === digits.length) { res.push(path); return; }
for (const ch of map[digits[idx]]) backtrack(idx + 1, path + ch);
};
backtrack(0, '');
return res;
}
7.4 N 皇后 ⭐⭐⭐(了解思路)
js
function solveNQueens(n) {
const res = [], cols = new Set(), diag1 = new Set(), diag2 = new Set();
const queens = new Array(n).fill(-1); // queens[r] = c 表示第 r 行皇后在第 c 列
const backtrack = r => {
if (r === n) {
res.push(queens.map(c => '.'.repeat(c) + 'Q' + '.'.repeat(n - c - 1)));
return;
}
for (let c = 0; c < n; c++) {
if (cols.has(c) || diag1.has(r - c) || diag2.has(r + c)) continue;
queens[r] = c; cols.add(c); diag1.add(r - c); diag2.add(r + c);
backtrack(r + 1);
cols.delete(c); diag1.delete(r - c); diag2.delete(r + c);
}
};
backtrack(0);
return res;
}
// 核心技巧:用 r-c 标记主对角线,r+c 标记副对角线
7.5 括号生成 ⭐⭐
js
function generateParenthesis(n) {
const res = [];
const backtrack = (path, open, close) => {
if (path.length === 2 * n) { res.push(path); return; }
if (open < n) backtrack(path + '(', open + 1, close);
if (close < open) backtrack(path + ')', open, close + 1); // 剪枝:close 不能超过 open
};
backtrack('', 0, 0);
return res;
}
八、设计题:LRU 缓存 ⭐⭐⭐
题目:实现 LRUCache,get/put 均为 O(1)。
js
// 数据结构:Map(哈希)+ 双向链表(维护使用顺序)
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
// 哨兵头尾节点,简化边界处理
this.head = { key: null, val: null };
this.tail = { key: null, val: null };
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
this._moveToHead(node); // 访问后移到头部
return node.val;
}
put(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key);
node.val = value;
this._moveToHead(node);
return;
}
if (this.map.size >= this.capacity) {
const tail = this.tail.prev; // 淘汰尾部
this._remove(tail);
this.map.delete(tail.key);
}
const node = { key, val: value };
this._addToHead(node);
this.map.set(key, node);
}
_remove(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
_addToHead(node) {
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
}
_moveToHead(node) {
this._remove(node);
this._addToHead(node);
}
}
// 面试加分点:
// 1. 哨兵节点省去 null 判断;2. JS 中 Map 迭代顺序即插入顺序,
// 可用 Map 简化:"get 时 delete+set 即刷新位置,超出时删第一个 key"
class LRUCacheSimple {
constructor(capacity) { this.capacity = capacity; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return -1;
const v = this.map.get(key);
this.map.delete(key);
this.map.set(key, v); // 重新插入 = 移到最新位置
return v;
}
put(key, value) {
if (this.map.has(key)) this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.capacity) this.map.delete(this.map.keys().next().value);
}
}
九、前端手写题专题(面试必考)
9.1 防抖(Debounce)
js
// 基础版:停止触发 delay 后执行
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 进阶版:支持立即执行(leading)
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args); // trailing
}, delay);
if (callNow) fn.apply(this, args); // leading
};
}
// 考点:this 绑定、参数透传、immediate 边界(间隔小于 delay 时不触发 leading)
9.2 节流(Throttle)
js
// 时间戳版:第一次立即执行
function throttle(fn, interval = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
fn.apply(this, args);
last = now;
}
};
}
// 定时器版:固定间隔执行(trailing)
function throttleTimer(fn, interval = 300) {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
};
}
// 面试问"区别":时间戳版开头触发,定时器版结尾触发,可组合成 leading+trailing
9.3 深拷贝(Deep Clone)
js
// 递归版:处理对象/数组/循环引用/常用内置类型
function deepClone(target, map = new WeakMap()) {
if (typeof target !== 'object' || target === null) return target;
// 处理循环引用
if (map.has(target)) return map.get(target);
// 特殊对象处理
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
if (target instanceof Map) {
const m = new Map();
map.set(target, m);
target.forEach((v, k) => m.set(deepClone(k, map), deepClone(v, map)));
return m;
}
if (target instanceof Set) {
const s = new Set();
map.set(target, s);
target.forEach(v => s.add(deepClone(v, map)));
return s;
}
const clone = Array.isArray(target) ? [] : {};
map.set(target, clone);
for (const key of Reflect.ownKeys(target)) { // 含 Symbol 和不可枚举属性
clone[key] = deepClone(target[key], map);
}
return clone;
}
// 追问点:函数怎么拷贝?(无法完美拷贝闭包,常规做法直接引用或返回原函数)
// 性能更好的方案:structuredClone(target)(原生 API,支持大部分类型,函数除外)
9.4 手写 Promise(简化版)
js
class MyPromise {
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
constructor(executor) {
this.state = MyPromise.PENDING;
this.value = null;
this.reason = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = value => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(cb => cb());
}
};
const reject = reason => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(cb => cb());
}
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };
const promise2 = new MyPromise((resolve, reject) => {
const handle = (callback, data) => {
queueMicrotask(() => {
try {
const x = callback(data);
// 解析 then 的返回值(Promise 解析过程)
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
};
if (this.state === MyPromise.FULFILLED) {
handle(onFulfilled, this.value);
} else if (this.state === MyPromise.REJECTED) {
handle(onRejected, this.reason);
} else {
this.onFulfilledCallbacks.push(() => handle(onFulfilled, this.value));
this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
}
});
return promise2;
}
catch(onRejected) { return this.then(null, onRejected); }
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(r => r(value));
}
static reject(reason) { return new MyPromise((_, r) => r(reason)); }
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
if (promises.length === 0) return resolve([]);
promises.forEach((p, i) => {
MyPromise.resolve(p).then(
v => {
results[i] = v;
if (++count === promises.length) resolve(results);
},
reject
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(p => MyPromise.resolve(p).then(resolve, reject));
});
}
}
// Promise 解析过程:处理 thenable,防止循环引用
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) return reject(new TypeError('循环引用'));
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x,
y => { if (!called) { called = true; resolvePromise(promise2, y, resolve, reject); } },
r => { if (!called) { called = true; reject(r); } }
);
} else {
resolve(x);
}
} catch (e) {
if (!called) { called = true; reject(e); }
}
} else {
resolve(x);
}
}
// 面试关键:1) then 返回新 Promise 实现链式;2) 返回值需递归解析(thenable);
// 3) 回调用微任务异步执行;4) 循环引用检测
9.5 Promise 并发限制器 ⭐⭐⭐(阿里/字节高频)
js
// 限制同时最多 limit 个请求并发
function promisePool(tasks, limit) {
return new Promise(resolve => {
const results = new Array(tasks.length);
let index = 0, running = 0, done = 0;
const run = () => {
if (done === tasks.length) return resolve(results);
while (running < limit && index < tasks.length) {
const i = index++;
running++;
tasks[i]().then(
res => { results[i] = res; },
err => { results[i] = err; } // 或 reject(err) 看需求
).finally(() => {
running--;
done++;
run();
});
}
};
run();
});
}
// ===== 使用 =====
// const urls = [1, 2, 3, 4, 5].map(id => () => fetch(`/api/${id}`));
// promisePool(urls, 2).then(console.log);
9.6 手写 call / apply / bind
js
Function.prototype.myCall = function (context, ...args) {
context = context ?? window;
const key = Symbol('fn');
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
Function.prototype.myApply = function (context, args) {
return this.myCall(context, ...args);
};
Function.prototype.myBind = function (context, ...args1) {
const fn = this;
function bound(...args2) {
// new 绑定优先级最高:通过 instanceof 判断
const isNew = this instanceof bound;
return fn.apply(isNew ? this : context, [...args1, ...args2]);
}
bound.prototype = Object.create(fn.prototype); // 保留原型链
return bound;
};
// 考点:Symbol 防属性名冲突;bind 返回函数支持 new;参数柯里化合并
9.7 手写 new
js
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
// 三步:1) 创建空对象,原型指向构造函数 prototype
// 2) 执行构造函数,this 指向新对象
// 3) 返回值是对象则用返回值,否则用新对象
9.8 手写 instanceof
js
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 原理:沿原型链查找 Constructor.prototype
9.9 函数柯里化
js
// add(1)(2)(3) = 6,且支持 add(1,2)(3)
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...rest) {
return curried.apply(this, [...args, ...rest]);
};
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
9.10 手写 EventEmitter(发布订阅)
js
class EventEmitter {
constructor() { this.events = new Map(); }
on(event, fn) {
if (!this.events.has(event)) this.events.set(event, []);
this.events.get(event).push(fn);
return this; // 链式调用
}
once(event, fn) {
const wrapper = (...args) => {
this.off(event, wrapper);
fn.apply(this, args);
};
wrapper.fn = fn; // 保存引用便于 off
return this.on(event, wrapper);
}
emit(event, ...args) {
const fns = this.events.get(event);
if (fns) fns.slice().forEach(fn => fn.apply(this, args));
return this;
}
off(event, fn) {
const fns = this.events.get(event);
if (!fns) return this;
if (!fn) { this.events.delete(event); return this; }
this.events.set(event, fns.filter(f => f !== fn && f.fn !== fn));
return this;
}
}
// 考点:once 的包裹函数、off 时对 once 的处理、防遍历中修改
9.11 数组扁平化
js
// 递归
const flatten = (arr, depth = Infinity) =>
arr.reduce((acc, cur) =>
Array.isArray(cur) && depth > 0
? acc.concat(flatten(cur, depth - 1))
: acc.concat(cur), []);
// 迭代(注意:严格按深度处理需要分层,下面是 Infinity 版)
const flattenIter = arr => {
const stack = [...arr], res = [];
while (stack.length) {
const item = stack.pop();
Array.isArray(item) ? stack.push(...item) : res.push(item);
}
return res.reverse();
};
// 原生:arr.flat(Infinity)
9.12 手写数组方法(map/filter/reduce)
js
Array.prototype.myMap = function (cb, thisArg) {
const res = new Array(this.length);
for (let i = 0; i < this.length; i++) {
if (i in this) res[i] = cb.call(thisArg, this[i], i, this);
}
return res;
};
Array.prototype.myFilter = function (cb, thisArg) {
const res = [];
for (let i = 0; i < this.length; i++) {
if (i in this && cb.call(thisArg, this[i], i, this)) res.push(this[i]);
}
return res;
};
Array.prototype.myReduce = function (cb, initialValue) {
let acc, startIdx;
if (arguments.length > 1) {
acc = initialValue; startIdx = 0;
} else {
acc = this[0]; startIdx = 1; // 不传初值:第一个元素作初值
}
for (let i = startIdx; i < this.length; i++) {
acc = cb(acc, this[i], i, this);
}
return acc;
};
// 考点:稀疏数组检查(i in this)、thisArg、reduce 无初值时从索引 1 开始
9.13 图片懒加载
js
// IntersectionObserver 版(现代推荐)
function lazyLoad(imgs) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { rootMargin: '100px' }); // 提前 100px 开始加载
imgs.forEach(img => observer.observe(img));
}
9.14 手写 JSON.stringify(简化版,了解思路)
js
function jsonStringify(value) {
if (value === null) return 'null';
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (typeof value === 'string') return `"${value.replace(/"/g, '\\"')}"`;
if (typeof value === 'object') {
if (Array.isArray(value)) {
return `[${value.map(jsonStringify).join(',')}]`;
}
const pairs = Object.entries(value)
.filter(([, v]) => v !== undefined && typeof v !== 'function') // 被跳过
.map(([k, v]) => `"${k}":${jsonStringify(v)}`);
return `{${pairs.join(',')}}`;
}
return undefined; // 函数、symbol、undefined 顶层返回 undefined
}
十、综合场景题(开放题)
10.1 大文件上传 ⭐⭐⭐
要点:
- 切片:
Blob.prototype.slice()按固定大小(如 5MB)切片 - 计算 hash:
crypto.subtle.digest('SHA-256', buffer)(Web Worker 中计算防卡顿),或抽样哈希加速 - 秒传:上传 hash,服务端已有则直接成功
- 断点续传:记录已上传分片列表(localStorage 或服务端查询),重传时过滤
- 并发上传分片(配合 9.5 并发限制)
- 全部分片上传完后通知服务端合并
- 失败重试:单分片失败重传 N 次
10.2 虚拟列表(长列表优化)⭐⭐⭐
js
// 固定行高版核心思路
// 可视区只渲染 startIndex ~ endIndex 的行,上下用占位撑开
function virtualList({ container, itemHeight, total, renderItem }) {
const viewport = document.createElement('div');
viewport.style.cssText = `height:${container.clientHeight}px;overflow:auto`;
const content = document.createElement('div');
content.style.height = total * itemHeight + 'px';
const pool = document.createElement('div');
pool.style.cssText = 'position:relative';
viewport.append(content);
content.append(pool);
container.append(viewport);
const update = () => {
const scrollTop = viewport.scrollTop;
const start = Math.floor(scrollTop / itemHeight);
const count = Math.ceil(viewport.clientHeight / itemHeight) + 1;
const end = Math.min(start + count, total);
pool.innerHTML = '';
for (let i = start; i < end; i++) {
const el = renderItem(i);
el.style.position = 'absolute';
el.style.top = i * itemHeight + 'px';
el.style.height = itemHeight + 'px';
el.style.width = '100%';
pool.append(el);
}
};
viewport.addEventListener('scroll', update, { passive: true });
update();
}
// 面试加分项:不定行高(缓存已测量高度+二分定位)、前后缓冲、DOM 池化复用
10.3 页面有大量图片同时请求,如何优化?
- 图片懒加载(IntersectionObserver)
- 雪碧图/图标字体/iconfont 合并小图
- 图片压缩、WebP、响应式 srcset
- HTTP/2 多路复用 + CDN
- 域名分片(HTTP/1.1 下突破 6 连接限制,HTTP/2 反而不建议)
- 预加载关键图
<link rel="preload">,其余loading="lazy"
10.4 设计一个前端监控系统
- 性能:Performance API(FP/FCP/LCP/INP/CLS)、Resource Timing、sendBeacon 上报
- 错误 :
window.onerror(JS 错误)、unhandledrejection(Promise 错误)、addEventListener('error', ..., true)(资源错误)、重写 console.error - 行为:PV/UV、点击埋点(事件委托)、路由切换
- 数据:采样上报、批量上报(减少请求)、sourcemap 还原堆栈
- 上报方式:
navigator.sendBeacon(页面卸载时也能发)
10.5 实现一个带过期时间的 localStorage
js
const storage = {
set(key, value, expireMs) {
const payload = { value, expire: expireMs ? Date.now() + expireMs : null };
localStorage.setItem(key, JSON.stringify(payload));
},
get(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const { value, expire } = JSON.parse(raw);
if (expire && Date.now() > expire) {
localStorage.removeItem(key);
return null; // 惰性删除
}
return value;
}
};
附录:面试自查清单
手写题(要求能默写)
- 防抖 + 节流(含 immediate/leading 变体)
- 深拷贝(含循环引用、Map/Set/Date)
- Promise.all / allSettled / race(手写 Promise 加分)
- Promise 并发限制器
- call / apply / bind / new / instanceof
- 柯里化、EventEmitter、数组扁平化
- 快排、归并、二分(含 lower_bound 变体)
- LRU 缓存(Map 版 + 双向链表版)
复杂度速查
| 操作 | 数组 | 链表 | 哈希表 | 二叉搜索树 |
|---|---|---|---|---|
| 查找 | O(n) / 有序 O(log n) | O(n) | O(1) | O(log n) |
| 插入/删除 | O(n) | O(1) 已知节点 | O(1) | O(log n) |
常见数据结构选型
- 需要 O(1) 查找 → 哈希表 / Set
- 需要顺序处理 + 首尾操作 → 队列(BFS)/ 栈(DFS、单调栈)
- 找第 K 大 / Top K → 快速选择 / 堆
- 区间最值 → 单调队列
- 最近使用 → LRU(哈希 + 双向链表)