【面试】前端面试高频算法题 Top 10

前端面试里的算法题并不追求冷门,而是考察你对常见模板的掌握。本文给出 Top 10 高频题型、解法思路、复杂度和面试表达模板。

11 分钟阅读
小明

【面试】前端面试高频算法题 Top 10

前端算法面试有个规律:

  • 题目看起来不同
  • 底层模板高度重复

所以高效准备方式不是“海量刷题”,而是:

  1. 先按模板分组
  2. 每组打穿 1~2 道代表题
  3. 练习口述思路与复杂度

这篇给你一份前端场景下最常见的 Top 10。


1. 两数之和(哈希表)

  • 关键词:target、数组、一次遍历
  • 核心:边遍历边查 target - nums[i]
  • 复杂度:时间 $O(n)$,空间 $O(n)$
function twoSum(nums: number[], target: number): number[] {
  const map = new Map<number, number>()
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i]
    if (map.has(complement)) {
      return [map.get(complement)!, i]
    }
    map.set(nums[i], i)
  }
  return []
}

面试补充:讲清为什么这样比两层 for 快——哈希表查询是 O(1),代价是多用空间。


2. 无重复字符的最长子串(滑动窗口)

  • 关键词:最长、子串、无重复
  • 核心:右指针扩张,遇重复时左指针收缩
  • 复杂度:时间 $O(n)$,空间 $O(k)$(字符集大小)
function lengthOfLongestSubstring(s: string): number {
  const map = new Map<string, number>()
  let left = 0, max = 0
  
  for (let right = 0; right < s.length; right++) {
    if (map.has(s[right])

```ts
function isValid(s: string): boolean {
  const stack: string[] = []
  const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }
  
  for (const char of s) {
    if (pairs[char]) {
      if (stack.pop() !== pairs[char]) return false
    } else {
      stack.push(char)
    }
  }
  
  return stack.length === 0
}

注意:栈空时 pop 要返回 undefined,所以要加判断。) { // 左指针跳到上次出现位置之后 left = Math.max(left, map.get(sright)! + 1) } map.set(sright, right) max = Math.max(max, right - left + 1) }

return max }

interface ListNode {
  val: number
  next: ListNode | null
}

function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null {
  const dummy = new ListNode(0)
  let current = dummy
  
  while (l1 && l2) {
    if (l1.val <= l2.val) {
      current.next = l1
      l1 = l1.next
    } else {
      current.next = l2
      l2 = l2.next
    }
    current = current.next
  }
  
  current.next = l1 || l2
  return dummy.next
}

技巧:用 dummy 节点避免处理头指针的特殊情况。


陷阱:不是 `left = map.get(s[right]) + 1`,而要用 `Math.max` 防止左指针往回退。

---

## 3. 有效括号(栈)

- 关键词:括号匹配
- 核心:左括号入栈,右括号检查并弹栈
- 复杂度:时间 $O(n)$,空间 $O(n)$

---

## 4. 合并两个有序链表(双指针)

- 关键词:链表、有序、合并
- 核心:维护尾指针,比较后接入较小节点
- 复杂度:时间 $O(m+n)$,空间 $O(1)$

---

## 5. 二叉树层序遍历(BFS)

- 关键词:按层输出
- 核心:队列逐层处理
- 复杂度:时间 $O(n)$,空间 $O(n)$

```ts
function levelOrder(root: TreeNode | null): number[][] {
  if (!root) return []
  const result: number[][] = []
  const queue: TreeNode[] = [root]
  
  while (queue.length) {
    const levelSize = queue.length
    const level: number[] = []
    
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()!
      level.push(node.val)
      if (node.left) queue.push(node.left)
      if (node.right) queue.push(node.right)
    }
    
    result.push(level)
  }
  
  return result
}

核心:先记 levelSize,这样能区分不同层的节点。


6. 反转链表(迭代)

  • 关键词:原地反转
  • 核心:prev/curr/next 三指针
  • 复杂度:时间 $O(n)$,空间 $O(1)$
function reverseList(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null
  let curr = head
  
  while (curr) {
    const next = curr.next  // 先保存下一个
    curr.next = prev        // 反转
    prev = curr             // 移动 prev
    curr = next             // 移动 curr
  }
  
  return prev
}

陷阱:忘记保存 next 导致链表断掉。


7. 最长递增子序列(DP)

  • 关键词:子序列、最长
  • 核心:dp[i] 表示以 i 结尾的 LIS 长度
  • 复杂度:朴素 $O(n^2)$,可优化到 $O(n\log n)$
// 朴素 DP:易懂但 O(n^2)
function lengthOfLIS(nums: number[]): number {
  const dp = new Array(nums.length).fill(1)
  
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {

```ts
function numIslands(grid: string[][]): number {
  let count = 0
  
  function dfs(i: number, j: number) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return
    if (grid[i][j] !== '1') return
    
    grid[i][j] = '0'  // 标记为已访问(原地修改)
    dfs(i + 1, j)
    dfs(i - 1, j)
    dfs(i, j + 1)
    dfs(i, j - 1)
  }
  
  for (let i = 0; i < grid.length; i++) {
    for (let j = 0; j < grid[0].length; j++) {
      if (grid[i][j] === '1') {
        dfs(i, j)
        count++
      }
    }
  }
  
  return count
}

技巧:不用额外的 visited 数组,直接改原数组。 if (numsj < numsi) { dpi = Math.max(dpi, dpj + 1) } } }

return Math.max(...dp) }

// 优化版:用二分查找 O(n log n) function lengthOfLISOptimized(nums: number): number { const tails: number =

for (const num of nums)

function subarraySum(nums: number[], k: number): number {
  const map = new Map<number, number>()
  map.set(0, 1)  // 初始状态:和为 0 出现了 1 次
  
  let prefixSum = 0
  let count = 0

```ts
function topKFrequent(nums: number[], k: number): number[] {
  // 第一步:统计频率
  const map = new Map<number, number>()
  for (const num of nums) {
    map.set(num, (map.get(num) ?? 0) + 1)
  }
  
  // 第二步:用数组代替堆(JavaScript 中堆库不如其他语言方便)
  const entries = Array.from(map.entries())
  entries.sort((a, b) => b[1] - a[1])
  
  return entries.slice(0, k).map(entry => entry[0])
}

最优做法:用小根堆维护前 K 个元素,时间 $O(n\log k)$ 比排序快。

for (const num of nums) { prefixSum += num

// 找是否存在 prefixSum - k,即前面有段和为 k
if (map.has(prefixSum - k)) {
  count += map.get(prefixSum - k)!
}

map.set(prefixSum, (map.get(prefixSum) ?? 0) + 1)

}

return count }


核心思路:`map.set(0, 1)` 处理从头开始的子数组。 {
    let left = 0, right = tails.length
    while (left < right) {
      const mid = Math.floor((left + right) / 2)
      if (tails[mid] < num) left = mid + 1
      else right = mid
    }
    tails[left] = num
  }
  
  return tails.length
}

面试可以先讲 O(n²) 的思路,再提 O(n log n) 的优化。


8. 岛屿数量(DFS/BFS)

  • 关键词:网格、连通块
  • 核心:遇到陆地就做一次淹没遍历
  • 复杂度:时间 $O(mn)$,空间 $O(mn)$(递归栈/队列)

9. 子数组和为 K(前缀和 + 哈希)

  • 关键词:连续子数组、和为 K
  • 核心:若 pre[j]-pre[i]=k,则存在一段满足
  • 复杂度:时间 $O(n)$,空间 $O(n)$

10. Top K 高频元素(堆/桶)

  • 关键词:TopK、频次
  • 核心:统计频次后用小根堆或桶排序
  • 复杂度:时间 $O(n\log k)$(堆),空间 $O(n)$

面试表达模板

不用秒出代码,用这 4 句话讲清思路:

  1. “这题属于 XX 模板(窗口/哈希/栈/树遍历)。”
  2. “我维护的状态是 XX 不变量。”
  3. “每个元素最多处理 X 次,所以时间复杂度是 XX。”
  4. “边界要测 空输入/重复元素/极端值。”

面试官往往比“秒出代码”更看重这套过程。


总结

前端算法 Top 10 的核心不是“记答案”,而是把模板练成条件反射。

把这 10 类打穿后,你会发现很多变体题都只是“换皮题”。