算法与设计模式 2026 面试题汇总
聚焦前端面试高频算法题(LeetCode 中等及以下)和 JavaScript 设计模式实现;排除竞赛算法、图论复杂题目
目录
复杂度分析
1. 什么是时间复杂度和空间复杂度?如何分析?
答案要点:
时间复杂度:算法执行次数随输入规模 n 增长的趋势(忽略常数和低阶项)
常见复杂度从低到高:
| 复杂度 | 名称 | 示例 |
|---|---|---|
| O(1) | 常数 | 数组下标访问、哈希表查找 |
| O(log n) | 对数 | 二分查找、平衡BST操作 |
| O(n) | 线性 | 线性遍历 |
| O(n log n) | 线性对数 | 归并排序、快速排序(平均) |
| O(n²) | 平方 | 冒泡、选择、插入排序 |
| O(2ⁿ) | 指数 | 暴力递归、子集枚举 |
分析技巧:
// O(1) - 固定次数操作
function getFirst(arr) { return arr[0] }
// O(n) - 单层循环
function sum(arr) {
let total = 0
for (const x of arr) total += x // n 次
return total
}
// O(n²) - 嵌套循环
function bubbleSort(arr) {
for (let i = 0; i < arr.length; i++) // n 次
for (let j = 0; j < arr.length - i; j++) // n 次
if (arr[j] > arr[j+1]) [arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
// O(log n) - 每次缩小一半
function binarySearch(arr, target) {
let left = 0, right = arr.length - 1
while (left <= right) {
const mid = (left + right) >> 1
if (arr[mid] === target) return mid
else if (arr[mid] < target) left = mid + 1
else right = mid - 1
}
return -1
}追问:递归算法的时间复杂度如何分析?
数组与字符串
2. 两数之和(高频)
题目:给定数组和目标值,找出和为 target 的两个数的下标。
答案要点:
// 暴力 O(n²) → 哈希表 O(n)
function twoSum(nums, target) {
const map = new Map() // 值 → 下标
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 []
}
// twoSum([2, 7, 11, 15], 9) → [0, 1]追问:如果数组已排序,如何用 O(1) 空间解决?→ 双指针
3. 滑动窗口:无重复字符的最长子串
题目:找出字符串中不含重复字符的最长子串长度。
答案要点:
function lengthOfLongestSubstring(s) {
const charIndex = new Map() // 字符 → 最新下标
let maxLen = 0
let left = 0 // 窗口左边界
for (let right = 0; right < s.length; right++) {
const char = s[right]
// 如果字符重复且在窗口内,移动左边界
if (charIndex.has(char) && charIndex.get(char) >= left) {
left = charIndex.get(char) + 1
}
charIndex.set(char, right)
maxLen = Math.max(maxLen, right - left + 1)
}
return maxLen
}
// "abcabcbb" → 3 ("abc")
// "pwwkew" → 3 ("wke")核心思路:用哈希表记录字符最新位置,遇到重复则收缩左边界。
追问:如何找出所有最长无重复子串?
4. 二分查找变体:搜索旋转排序数组
题目:在旋转排序数组中搜索目标值,要求 O(log n)。
答案要点:
function search(nums, target) {
let left = 0, right = nums.length - 1
while (left <= right) {
const mid = (left + right) >> 1
if (nums[mid] === target) return mid
// 判断哪半段是有序的
if (nums[left] <= nums[mid]) {
// 左半段有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1 // target 在左半段
} else {
left = mid + 1 // target 在右半段
}
} else {
// 右半段有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1 // target 在右半段
} else {
right = mid - 1 // target 在左半段
}
}
}
return -1
}
// [4,5,6,7,0,1,2], target=0 → 4追问:如果数组中有重复元素,如何处理?
5. 双指针:合并两个有序数组
答案要点:
// 从后往前归并,避免覆盖问题
function merge(nums1, m, nums2, n) {
let p1 = m - 1 // nums1 有效末尾
let p2 = n - 1 // nums2 末尾
let tail = m + n - 1 // 合并后末尾
while (p2 >= 0) {
if (p1 >= 0 && nums1[p1] > nums2[p2]) {
nums1[tail--] = nums1[p1--]
} else {
nums1[tail--] = nums2[p2--]
}
}
}追问:为什么从后往前而不是从前往后?
链表
6. 反转链表(高频)
答案要点:
// 迭代(推荐)
function reverseList(head) {
let prev = null
let curr = head
while (curr) {
const next = curr.next // 保存下一个节点
curr.next = prev // 反转指针
prev = curr // prev 前进
curr = next // curr 前进
}
return prev // prev 是新头节点
}
// 递归
function reverseListRecursive(head) {
if (!head || !head.next) return head
const newHead = reverseListRecursive(head.next)
head.next.next = head // 后一个节点指向当前节点
head.next = null // 当前节点的 next 置空
return newHead
}追问:如何反转链表的前 k 个节点?如何反转 m 到 n 范围内的节点?
7. 检测链表中的环
答案要点:
// 快慢指针(Floyd 判圈算法)
function hasCycle(head) {
let slow = head
let fast = head
while (fast && fast.next) {
slow = slow.next // 每次走1步
fast = fast.next.next // 每次走2步
if (slow === fast) return true // 相遇则有环
}
return false
}
// 找到环的起始节点
function detectCycle(head) {
let slow = head, fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
// 相遇后,一个指针从 head 出发,另一个从相遇点出发
// 它们会在环入口相遇
let p = head
while (p !== slow) {
p = p.next
slow = slow.next
}
return p
}
}
return null
}追问:为什么快慢指针一定会在环内相遇?
8. 合并 K 个有序链表
答案要点:
// 使用最小堆(优先队列)- O(n log k)
class MinHeap {
constructor() { this.heap = [] }
push(node) {
this.heap.push(node)
this.heap.sort((a, b) => a.val - b.val) // 简化实现
}
pop() { return this.heap.shift() }
get size() { return this.heap.length }
}
function mergeKLists(lists) {
const heap = new MinHeap()
const dummy = { next: null }
let curr = dummy
// 将每个链表的头节点入堆
for (const head of lists) {
if (head) heap.push(head)
}
while (heap.size > 0) {
const node = heap.pop()
curr.next = node
curr = curr.next
if (node.next) heap.push(node.next)
}
return dummy.next
}追问:分治法如何实现合并 K 个有序链表?
树与递归
9. 二叉树的三种遍历(迭代实现)
答案要点:
// 前序遍历:根-左-右(迭代)
function preorderTraversal(root) {
if (!root) return []
const result = [], stack = [root]
while (stack.length) {
const node = stack.pop()
result.push(node.val)
if (node.right) stack.push(node.right) // 先压右
if (node.left) stack.push(node.left) // 再压左(后出)
}
return result
}
// 中序遍历:左-根-右(迭代)
function inorderTraversal(root) {
const result = [], stack = []
let curr = root
while (curr || stack.length) {
while (curr) { stack.push(curr); curr = curr.left }
curr = stack.pop()
result.push(curr.val)
curr = curr.right
}
return result
}
// 层序遍历(BFS)
function levelOrder(root) {
if (!root) return []
const result = [], queue = [root]
while (queue.length) {
const levelSize = queue.length
const level = []
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
}追问:后序遍历如何用迭代实现?
10. 二叉树最大深度与路径和
答案要点:
// 最大深度
function maxDepth(root) {
if (!root) return 0
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right))
}
// 路径总和(是否存在根到叶子和为 targetSum 的路径)
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)
}
// 二叉树中最大路径和(可以不经过根节点)
function maxPathSum(root) {
let maxSum = -Infinity
function dfs(node) {
if (!node) return 0
const left = Math.max(dfs(node.left), 0) // 负数不要
const right = Math.max(dfs(node.right), 0)
maxSum = Math.max(maxSum, node.val + left + right) // 经过该节点的最大路径
return node.val + Math.max(left, right) // 向上只能返回一条路
}
dfs(root)
return maxSum
}追问:如何判断一棵树是否是平衡二叉树?
11. 最近公共祖先(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)
// 左右都找到:当前节点就是 LCA
if (left && right) return root
// 只有一边找到:返回找到的那边
return left || right
}追问:如果是 BST,如何优化 LCA 查找?
动态规划
12. 爬楼梯(DP 入门)
题目:n 级台阶,每次可以走 1 或 2 步,共有多少种走法?
答案要点:
// 递推:f(n) = f(n-1) + f(n-2)
function climbStairs(n) {
if (n <= 2) return n
let prev = 1, curr = 2
for (let i = 3; i <= n; i++) {
[prev, curr] = [curr, prev + curr]
}
return curr
}
// 空间优化到 O(1),时间 O(n)DP 三要素:
- 状态定义:
dp[i]= 到第 i 级台阶的走法数 - 状态转移:
dp[i] = dp[i-1] + dp[i-2] - 边界条件:
dp[1] = 1, dp[2] = 2
追问:如果每次可以走 1、2 或 3 步,如何修改?
13. 最长公共子序列(LCS)
答案要点:
function longestCommonSubsequence(text1, text2) {
const m = text1.length, n = text2.length
// dp[i][j] = text1前i个字符与text2前j个字符的LCS长度
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++) {
if (text1[i-1] === text2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
}
}
}
return dp[m][n]
}
// "abcde", "ace" → 3追问:LCS 和 LIS(最长递增子序列)有什么区别?
14. 背包问题(0-1 背包)
答案要点:
// n 个物品,容量 W 的背包,最大价值
function knapsack(weights, values, W) {
const n = weights.length
// dp[j] = 容量为 j 时的最大价值
const dp = new Array(W + 1).fill(0)
for (let i = 0; i < n; i++) {
// 从后往前遍历,确保每个物品只用一次
for (let j = W; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i])
}
}
return dp[W]
}追问:完全背包(每个物品可以无限取)如何修改?→ 内层循环从前往后遍历
排序算法
15. 常见排序算法对比
答案要点:
| 算法 | 时间(平均) | 时间(最坏) | 空间 | 稳定 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | ✅ |
| 选择排序 | O(n²) | O(n²) | O(1) | ❌ |
| 插入排序 | O(n²) | O(n²) | O(1) | ✅ |
| 快速排序 | O(n log n) | O(n²) | O(log n) | ❌ |
| 归并排序 | O(n log n) | O(n log n) | O(n) | ✅ |
| 堆排序 | O(n log n) | O(n log n) | O(1) | ❌ |
// 快速排序
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return
const pivotIdx = partition(arr, left, right)
quickSort(arr, left, pivotIdx - 1)
quickSort(arr, pivotIdx + 1, right)
}
function partition(arr, left, right) {
const pivot = arr[right]
let i = left - 1
for (let j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++
;[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
;[arr[i+1], arr[right]] = [arr[right], arr[i+1]]
return i + 1
}
// 归并排序
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 mergeSorted(left, right)
}
function mergeSorted(left, right) {
const result = []
let i = 0, j = 0
while (i < left.length && j < right.length) {
result.push(left[i] <= right[j] ? left[i++] : right[j++])
}
return result.concat(left.slice(i), right.slice(j))
}追问:为什么 JavaScript 的 Array.sort() 使用 TimSort?
设计模式
16. 单例模式(Singleton)
答案要点:
// 方式一:类实现
class Database {
private static instance: Database | null = null
private constructor(private readonly url: string) {}
static getInstance(url: string): Database {
if (!Database.instance) {
Database.instance = new Database(url)
}
return Database.instance
}
query(sql: string) { /* ... */ }
}
const db1 = Database.getInstance('postgres://localhost/mydb')
const db2 = Database.getInstance('postgres://localhost/mydb')
console.log(db1 === db2) // true
// 方式二:模块级单例(ES Module 天然单例)
// store.ts
let count = 0
export function getCount() { return count }
export function increment() { count++ }使用场景:全局状态管理、数据库连接池、日志实例、配置管理
追问:前端框架中哪里用到了单例模式?
17. 观察者模式 vs 发布订阅模式
答案要点:
// 观察者模式(直接耦合)
class EventEmitter {
private listeners: Map<string, Function[]> = new Map()
on(event: string, fn: Function) {
const fns = this.listeners.get(event) || []
this.listeners.set(event, [...fns, fn])
return () => this.off(event, fn) // 返回取消订阅函数
}
off(event: string, fn: Function) {
const fns = this.listeners.get(event) || []
this.listeners.set(event, fns.filter(f => f !== fn))
}
emit(event: string, ...args: any[]) {
const fns = this.listeners.get(event) || []
fns.forEach(fn => fn(...args))
}
once(event: string, fn: Function) {
const wrapper = (...args: any[]) => {
fn(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
}
}
// 使用
const emitter = new EventEmitter()
const unsubscribe = emitter.on('data', (data) => console.log(data))
emitter.emit('data', { id: 1 })
unsubscribe() // 取消订阅区别:
- 观察者模式:Subject 直接通知 Observer,有直接依赖
- 发布订阅模式:通过中间的事件总线(EventBus/MessageBroker)解耦,发布者和订阅者互不知道对方
应用:Vue 响应式系统、Node.js EventEmitter、DOM 事件、Redux
追问:如何实现一个支持优先级的事件系统?
18. 策略模式(Strategy)
答案要点:
// 不使用策略模式(if-else 地狱)
function calculateDiscount(type: string, price: number) {
if (type === 'vip') return price * 0.8
if (type === 'super-vip') return price * 0.6
if (type === 'member') return price * 0.9
return price
}
// 使用策略模式
type DiscountStrategy = (price: number) => number
const discountStrategies: Record<string, DiscountStrategy> = {
vip: (price) => price * 0.8,
'super-vip': (price) => price * 0.6,
member: (price) => price * 0.9,
default: (price) => price,
}
function calculateDiscount(type: string, price: number) {
const strategy = discountStrategies[type] || discountStrategies.default
return strategy(price)
}
// 动态扩展,不需要修改原有代码(符合开闭原则)
discountStrategies['new-member'] = (price) => price * 0.95应用场景:表单验证规则、价格计算、排序策略、支付方式
追问:策略模式和状态模式的区别是什么?
19. 装饰器模式(Decorator)
答案要点:
// 函数装饰器:添加日志和缓存
function withLogger<T extends (...args: any[]) => any>(fn: T, name: string): T {
return function(...args: Parameters<T>): ReturnType<T> {
console.log(`[${name}] 调用参数:`, args)
const result = fn.apply(this, args)
console.log(`[${name}] 返回值:`, result)
return result
} as T
}
function withCache<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>()
return function(...args: Parameters<T>): ReturnType<T> {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)!
const result = fn.apply(this, args)
cache.set(key, result)
return result
} as T
}
// 组合使用
function expensiveCalc(n: number) { return n * n }
const cachedCalc = withCache(withLogger(expensiveCalc, 'expensiveCalc'))
cachedCalc(5) // 计算并缓存
cachedCalc(5) // 直接返回缓存
// TypeScript 类装饰器
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false
return descriptor
}
class Circle {
@readonly
PI = 3.14159
}应用:React HOC、Next.js withAuth、日志、缓存、权限控制
追问:装饰器模式和继承有什么区别?
20. 代理模式(Proxy)
答案要点:
// ES6 Proxy 实现数据响应式
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, key, receiver) {
console.log(`读取 ${String(key)}`)
track(obj, key) // 依赖收集
return Reflect.get(obj, key, receiver)
},
set(obj, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`)
const result = Reflect.set(obj, key, value, receiver)
trigger(obj, key) // 触发更新
return result
}
})
}
const state = reactive({ count: 0, name: 'Vue' })
state.count = 1 // 触发 setter → 视图更新
// 虚拟代理:图片懒加载
class LazyImage {
private realImg: HTMLImageElement | null = null
constructor(private src: string) {}
load() {
if (!this.realImg) {
this.realImg = new Image()
this.realImg.src = this.src
}
return this.realImg
}
}应用:Vue 3 响应式(Proxy 替代 Object.defineProperty)、数据验证、权限控制、懒加载
追问:Vue 2 用 Object.defineProperty,Vue 3 为什么改用 Proxy?
前端常见场景题
21. 实现 LRU 缓存(Least Recently Used)
答案要点:
class LRUCache {
private capacity: number
private cache: Map<number, number> // Map 保持插入顺序
constructor(capacity: number) {
this.capacity = capacity
this.cache = new Map()
}
get(key: number): number {
if (!this.cache.has(key)) return -1
// 将访问的 key 移到末尾(最近使用)
const val = this.cache.get(key)!
this.cache.delete(key)
this.cache.set(key, val)
return val
}
put(key: number, value: number): void {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的(Map 的第一个元素)
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
cache.get(1) // 1(访问后 1 变为最近使用)
cache.put(3, 3) // 淘汰 2(最久未使用)
cache.get(2) // -1(已被淘汰)追问:如何给 LRU 缓存增加过期时间?
22. 实现防抖和节流
答案要点:
// 防抖:最后一次触发后延迟执行
function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
let timer: ReturnType<typeof setTimeout> | null = null
return function(this: any, ...args: Parameters<T>) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
// 节流:固定时间间隔内只执行一次
function throttle<T extends (...args: any[]) => any>(fn: T, interval: number) {
let lastTime = 0
return function(this: any, ...args: Parameters<T>) {
const now = Date.now()
if (now - lastTime >= interval) {
fn.apply(this, args)
lastTime = now
}
}
}
// 使用场景
const handleSearch = debounce((query: string) => {
fetch(`/api/search?q=${query}`)
}, 300) // 输入停止 300ms 后才搜索
const handleScroll = throttle(() => {
updateScrollPosition()
}, 100) // 每 100ms 最多执行一次区别:
- 防抖:频繁触发时只执行最后一次(搜索框、resize)
- 节流:固定频率执行(scroll、mousemove、游戏循环)
追问:如何实现带 immediate 选项的防抖(立即执行第一次)?
23. 实现深拷贝
答案要点:
function deepClone<T>(value: T, seen = new WeakMap()): T {
// 处理基本类型、null、函数
if (value === null || typeof value !== 'object') return value
// 处理循环引用
if (seen.has(value as object)) return seen.get(value as object)
// 处理特殊类型
if (value instanceof Date) return new Date(value.getTime()) as T
if (value instanceof RegExp) return new RegExp(value.source, value.flags) as T
if (value instanceof Map) {
const map = new Map()
seen.set(value as object, map)
value.forEach((v, k) => map.set(deepClone(k, seen), deepClone(v, seen)))
return map as T
}
if (value instanceof Set) {
const set = new Set()
seen.set(value as object, set)
value.forEach(v => set.add(deepClone(v, seen)))
return set as T
}
// 处理数组和普通对象
const clone = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
seen.set(value as object, clone)
for (const key of Reflect.ownKeys(value as object)) {
clone[key] = deepClone((value as any)[key], seen)
}
return clone as T
}追问:JSON.parse(JSON.stringify(obj)) 深拷贝有哪些缺陷?
无法处理:函数、
undefined、Symbol、循环引用、Date(变字符串)、Map/Set
总结
高频题型:
| 类型 | 高频题 | 核心技巧 |
|---|---|---|
| 数组 | 两数之和、三数之和 | 哈希表、双指针 |
| 字符串 | 最长无重复子串、回文 | 滑动窗口 |
| 链表 | 反转、检测环、合并 | 快慢指针、dummy节点 |
| 树 | 遍历、最大深度、LCA | 递归、BFS |
| DP | 爬楼梯、LCS、背包 | 状态定义、转移方程 |
| 设计模式 | 单例、观察者、策略、代理 | JS 实现 + 前端应用场景 |
| 场景题 | LRU、防抖节流、深拷贝 | 考察综合能力 |