Skip to content

算法与设计模式 2026 面试题汇总

聚焦前端面试高频算法题(LeetCode 中等及以下)和 JavaScript 设计模式实现;排除竞赛算法、图论复杂题目

目录


复杂度分析

1. 什么是时间复杂度和空间复杂度?如何分析?

答案要点

时间复杂度:算法执行次数随输入规模 n 增长的趋势(忽略常数和低阶项)

常见复杂度从低到高:

复杂度名称示例
O(1)常数数组下标访问、哈希表查找
O(log n)对数二分查找、平衡BST操作
O(n)线性线性遍历
O(n log n)线性对数归并排序、快速排序(平均)
O(n²)平方冒泡、选择、插入排序
O(2ⁿ)指数暴力递归、子集枚举

分析技巧

js
// 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 的两个数的下标。

答案要点

js
// 暴力 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. 滑动窗口:无重复字符的最长子串

题目:找出字符串中不含重复字符的最长子串长度。

答案要点

js
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)。

答案要点

js
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. 双指针:合并两个有序数组

答案要点

js
// 从后往前归并,避免覆盖问题
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. 反转链表(高频)

答案要点

js
// 迭代(推荐)
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. 检测链表中的环

答案要点

js
// 快慢指针(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 个有序链表

答案要点

js
// 使用最小堆(优先队列)- 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. 二叉树的三种遍历(迭代实现)

答案要点

js
// 前序遍历:根-左-右(迭代)
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. 二叉树最大深度与路径和

答案要点

js
// 最大深度
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)

答案要点

js
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 步,共有多少种走法?

答案要点

js
// 递推: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 三要素

  1. 状态定义dp[i] = 到第 i 级台阶的走法数
  2. 状态转移dp[i] = dp[i-1] + dp[i-2]
  3. 边界条件dp[1] = 1, dp[2] = 2

追问:如果每次可以走 1、2 或 3 步,如何修改?

13. 最长公共子序列(LCS)

答案要点

js
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 背包)

答案要点

js
// 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)
js
// 快速排序
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)

答案要点

ts
// 方式一:类实现
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 发布订阅模式

答案要点

ts
// 观察者模式(直接耦合)
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)

答案要点

ts
// 不使用策略模式(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)

答案要点

ts
// 函数装饰器:添加日志和缓存
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)

答案要点

ts
// 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)

答案要点

ts
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. 实现防抖和节流

答案要点

ts
// 防抖:最后一次触发后延迟执行
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. 实现深拷贝

答案要点

ts
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)) 深拷贝有哪些缺陷?

无法处理:函数、undefinedSymbol、循环引用、Date(变字符串)、Map/Set


总结

高频题型

类型高频题核心技巧
数组两数之和、三数之和哈希表、双指针
字符串最长无重复子串、回文滑动窗口
链表反转、检测环、合并快慢指针、dummy节点
遍历、最大深度、LCA递归、BFS
DP爬楼梯、LCS、背包状态定义、转移方程
设计模式单例、观察者、策略、代理JS 实现 + 前端应用场景
场景题LRU、防抖节流、深拷贝考察综合能力