【面试】前端面试高频算法题 Top 10
前端面试里的算法题并不追求冷门,而是考察你对常见模板的掌握。本文给出 Top 10 高频题型、解法思路、复杂度和面试表达模板。
【面试】前端面试高频算法题 Top 10
前端算法面试有个规律:
- 题目看起来不同
- 底层模板高度重复
所以高效准备方式不是“海量刷题”,而是:
- 先按模板分组
- 每组打穿 1~2 道代表题
- 练习口述思路与复杂度
这篇给你一份前端场景下最常见的 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 句话讲清思路:
- “这题属于 XX 模板(窗口/哈希/栈/树遍历)。”
- “我维护的状态是 XX 不变量。”
- “每个元素最多处理 X 次,所以时间复杂度是 XX。”
- “边界要测 空输入/重复元素/极端值。”
面试官往往比“秒出代码”更看重这套过程。
总结
前端算法 Top 10 的核心不是“记答案”,而是把模板练成条件反射。
把这 10 类打穿后,你会发现很多变体题都只是“换皮题”。