前端算法与手写题集

前端算法与手写题集

目标:覆盖前端面试 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、并发控制

刷题顺序建议

  1. 先刷"前端手写题专题"(九)------这是前端面试区别于纯算法岗的核心
  2. 数组双指针 + 哈希(二)打基础
  3. 链表 + 二叉树递归套路(三、四)------递归思维是分水岭
  4. 动态规划(五)掌握 5 个经典模型即可
  5. 每天 2 题,每题限时 25 分钟,做完立刻复盘写"错因笔记"

面试答题三步法

  1. 复述 + 确认:复述题目,确认输入输出、边界(空数组、负数、超大数据)
  2. 讲思路:先说暴力解,再说优化方向("我先想一个 O(n²) 的,然后看能不能用哈希降到 O(n)")
  3. 写代码:边写边说关键行;写完主动跑两个用例(含边界)

二、数组与字符串

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 大文件上传 ⭐⭐⭐

要点

  1. 切片:Blob.prototype.slice() 按固定大小(如 5MB)切片
  2. 计算 hash:crypto.subtle.digest('SHA-256', buffer)(Web Worker 中计算防卡顿),或抽样哈希加速
  3. 秒传:上传 hash,服务端已有则直接成功
  4. 断点续传:记录已上传分片列表(localStorage 或服务端查询),重传时过滤
  5. 并发上传分片(配合 9.5 并发限制)
  6. 全部分片上传完后通知服务端合并
  7. 失败重试:单分片失败重传 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 页面有大量图片同时请求,如何优化?

  1. 图片懒加载(IntersectionObserver)
  2. 雪碧图/图标字体/iconfont 合并小图
  3. 图片压缩、WebP、响应式 srcset
  4. HTTP/2 多路复用 + CDN
  5. 域名分片(HTTP/1.1 下突破 6 连接限制,HTTP/2 反而不建议)
  6. 预加载关键图 <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(哈希 + 双向链表)
相关推荐
徐小夕1 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
程序员阿鹏1 小时前
为什么MySQL InnoDB选择B+树?
数据结构·数据库·b树·sql·mysql·算法·缓存
AI 思录2 小时前
Prompt 事故档案(八):日常表达被标为“待校准”,AI 的爹味语法从哪里来
大数据·人工智能·算法·prompt·用户体验·ai合规
月光船幽幽2 小时前
跨范式映射的稳定接口设计
人工智能·python·算法
萧鼎3 小时前
Python 高性能Web框架神器 FastAPI:自动生成API文、基于Pydant、异步请求处理全搞定
前端·python·fastapi
2601_965742223 小时前
全媒体运营与短视频代运营,两者有什么区别?
大数据·数据结构·人工智能·算法·ai·媒体
IT_陈寒3 小时前
Redis误用keys命令把生产环境搞崩了,血的教训
前端·人工智能·后端
计算机魔术师4 小时前
5000亿估值冲刺科创板,DeepSeek 为何急着上市?
前端
我血条子呢4 小时前
前端解析word方案
前端·word