HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

刷了500道LeetCode,终于明白大厂算法面试到底考什么

算法面试不是考你背了多少题,而是考你能不能在面试官面前把问题拆解清楚、写出正确的代码。

先泼一盆冷水

有个候选人在面试前刷了600道LeetCode,信心满满。

面试时遇到一道Medium难度的题,他说:"这题我做过!"然后飞快地写完代码。

面试官问:"时间复杂度是多少?"

他卡住了。

面试官又问:"如果数据量是10亿呢?"

他彻底懵了。

最后他挂在了一道刷过的题上。

问题出在哪? 他只记住了答案,没有理解解题思路。

这篇文章不是让你背题,而是让你理解:

  1. 面试官出算法题的真实意图
  2. 不同数据结构解决什么问题
  3. 如何分析复杂度、选择合适的方案

一、面试官出算法题的真实目的

先搞清楚规则,才能赢得游戏。

考察点一:编码基本功

// 看这两段代码,哪个更专业?

// 版本A:能跑,但像刚学编程
func f(a []int, t int) int {
    for i := 0; i < len(a); i++ {
        for j := i + 1; j < len(a); j++ {
            if a[i] + a[j] == t {
                return i
            }
        }
    }
    return -1
}

// 版本B:清晰的变量命名、完整的逻辑
func twoSum(nums []int, target int) []int {
    seen := make(map[int]int) // 存储已遍历的数字及其索引

    for i, num := range nums {
        complement := target - num
        if idx, exists := seen[complement]; exists {
            return []int{idx, i}
        }
        seen[num] = i
    }

    return nil // 无解的情况
}

面试官能从代码中看出:

  • 命名规范:变量名是否有意义
  • 边界处理:空数组、无解怎么办
  • 代码结构:逻辑是否清晰

考察点二:思考过程

比写出正确代码更重要的是:你是怎么想到这个解法的。

面试官喜欢看到这样的沟通:

"这道题要找两数之和等于target..."

"暴力解法是双重循环,时间复杂度O(n²)..."

"但如果用哈希表存储已遍历的数字,遍历时查找complement,
就能优化到O(n)..."

"空间复杂度会从O(1)增加到O(n),这是用空间换时间..."

考察点三:应变能力

面试官经常会追问:

  • 如果数据量是10亿呢?(考察分布式思维)
  • 如果允许有重复元素呢?(考察边界情况处理)
  • 如果内存只有1MB呢?(考察时空权衡)

能回答这些追问,才能拿到高分。


二、时间复杂度:决定代码生死的数字

很多人能写出代码,但说不清复杂度。这在面试中是致命伤。

常见复杂度一览

复杂度别名典型场景n=100万时的操作数
O(1)常数哈希表查找、数组下标访问1
O(log n)对数二分查找、平衡树20
O(n)线性一次遍历100万
O(n log n)线性对数快排、归并排序2000万
O(n²)平方双重循环1万亿 ❌
O(2ⁿ)指数暴力递归无穷大 ❌

面试时的黄金法则:

  • 10^6 以内用 O(n)
  • 10^7 以内用 O(n log n)
  • 10^8 以上必须 O(n) 或更优

怎么快速判断复杂度

规则1:看循环嵌套

// O(n) - 一层循环
for i := 0; i < n; i++ {
    // O(1)操作
}

// O(n²) - 两层嵌套
for i := 0; i < n; i++ {
    for j := 0; j < n; j++ {
        // O(1)操作
    }
}

// O(n log n) - 一层循环套二分
for i := 0; i < n; i++ {
    binarySearch(arr, target) // O(log n)
}

规则2:看递归深度

// 二分查找:每次减半,深度 log n,每层 O(1)
// 总复杂度:O(log n)
func binarySearch(arr []int, target int) int {
    left, right := 0, len(arr)-1
    for left <= right {
        mid := left + (right-left)/2
        if arr[mid] == target {
            return mid
        } else if arr[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1
}

// 斐波那契(无优化):每次分两路,深度 n
// 总复杂度:O(2^n) 😱
func fib(n int) int {
    if n <= 1 {
        return n
    }
    return fib(n-1) + fib(n-2)
}

规则3:均摊分析

// 动态数组扩容:偶尔 O(n),大部分 O(1)
// 均摊复杂度:O(1)
arr = append(arr, elem)

三、数据结构:选对工具解决问题

面试中80%的题目都可以用这5种数据结构解决。

1. 数组 Array

核心特性:

  • 随机访问 O(1)
  • 插入删除 O(n)(需要移动元素)

什么时候用:

  • 需要频繁按下标访问
  • 数据大小固定或变化不大

高频题型:

  • 双指针(两数之和、三数之和)
  • 滑动窗口(最长子串、最小覆盖子串)
  • 前缀和(子数组和、区间查询)

经典题目 - 两数之和:

// 题目:找出数组中两个数,使它们的和等于target
// 返回它们的下标

func twoSum(nums []int, target int) []int {
    // 哈希表存储:数值 -> 下标
    seen := make(map[int]int)

    for i, num := range nums {
        // 计算需要的另一个数
        complement := target - num

        // 检查这个数是否已经出现过
        if j, exists := seen[complement]; exists {
            return []int{j, i}
        }

        // 记录当前数字和下标
        seen[num] = i
    }

    return nil
}

// 面试追问:如果要返回所有组合呢?
// 答:用哈希表存储列表,处理重复值的情况

2. 链表 Linked List

核心特性:

  • 随机访问 O(n)
  • 已知位置插入删除 O(1)

什么时候用:

  • 需要频繁在中间插入删除
  • 不需要随机访问

高频题型:

  • 反转链表
  • 快慢指针(找环、找中点)
  • 合并链表

经典题目 - 反转链表:

type ListNode struct {
    Val  int
    Next *ListNode
}

// 迭代版本 - 面试时推荐,不容易栈溢出
func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head

    for curr != nil {
        next := curr.Next  // 先保存下一个节点
        curr.Next = prev   // 反转指针
        prev = curr        // prev 前进
        curr = next        // curr 前进
    }

    return prev
}

// 面试官追问:能用递归实现吗?
func reverseListRecursive(head *ListNode) *ListNode {
    // 递归终止:空节点或只有一个节点
    if head == nil || head.Next == nil {
        return head
    }

    // 递归反转后面的链表
    newHead := reverseListRecursive(head.Next)

    // 把当前节点接到反转后链表的尾部
    head.Next.Next = head
    head.Next = nil

    return newHead
}

快慢指针 - 检测环:

func hasCycle(head *ListNode) bool {
    if head == nil {
        return false
    }

    slow, fast := head, head

    for fast != nil && fast.Next != nil {
        slow = slow.Next      // 慢指针走一步
        fast = fast.Next.Next // 快指针走两步

        if slow == fast {
            return true // 相遇说明有环
        }
    }

    return false
}

// 面试追问:如果有环,找到入口点?
func detectCycle(head *ListNode) *ListNode {
    slow, fast := head, head

    // 第一步:快慢指针找相遇点
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            break
        }
    }

    if fast == nil || fast.Next == nil {
        return nil // 无环
    }

    // 第二步:一个从head出发,一个从相遇点出发
    // 相遇点就是入口
    slow = head
    for slow != fast {
        slow = slow.Next
        fast = fast.Next
    }

    return slow
}

3. 哈希表 Hash Map

核心特性:

  • 查找、插入、删除 O(1) 平均
  • 不保证顺序

什么时候用:

  • 需要快速查找
  • 统计频次
  • 缓存计算结果

高频题型:

  • 两数之和系列
  • 字符串匹配(变位词)
  • LRU缓存

经典题目 - LRU缓存(面试超高频):

// 这题考察:哈希表 + 双向链表的组合使用
// 难度:Medium,但在面试中经常出现

type LRUCache struct {
    capacity int
    cache    map[int]*DLinkedNode // 哈希表快速查找
    head     *DLinkedNode          // 虚拟头节点
    tail     *DLinkedNode          // 虚拟尾节点
}

type DLinkedNode struct {
    key, value int
    prev, next *DLinkedNode
}

func Constructor(capacity int) LRUCache {
    l := LRUCache{
        capacity: capacity,
        cache:    make(map[int]*DLinkedNode),
        head:     &DLinkedNode{},
        tail:     &DLinkedNode{},
    }
    l.head.next = l.tail
    l.tail.prev = l.head
    return l
}

func (l *LRUCache) Get(key int) int {
    if node, ok := l.cache[key]; ok {
        // 访问了,移到头部(最近使用)
        l.moveToHead(node)
        return node.value
    }
    return -1
}

func (l *LRUCache) Put(key int, value int) {
    if node, ok := l.cache[key]; ok {
        // key存在,更新值并移到头部
        node.value = value
        l.moveToHead(node)
    } else {
        // key不存在,新建节点
        newNode := &DLinkedNode{key: key, value: value}
        l.cache[key] = newNode
        l.addToHead(newNode)

        // 超过容量,删除尾部节点(最久未使用)
        if len(l.cache) > l.capacity {
            removed := l.removeTail()
            delete(l.cache, removed.key)
        }
    }
}

// 辅助方法
func (l *LRUCache) addToHead(node *DLinkedNode) {
    node.prev = l.head
    node.next = l.head.next
    l.head.next.prev = node
    l.head.next = node
}

func (l *LRUCache) removeNode(node *DLinkedNode) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (l *LRUCache) moveToHead(node *DLinkedNode) {
    l.removeNode(node)
    l.addToHead(node)
}

func (l *LRUCache) removeTail() *DLinkedNode {
    node := l.tail.prev
    l.removeNode(node)
    return node
}

4. 栈 Stack

核心特性:

  • 后进先出(LIFO)
  • 只能从栈顶操作

什么时候用:

  • 括号匹配
  • 表达式求值
  • 单调栈(下一个更大元素)

经典题目 - 有效括号:

func isValid(s string) bool {
    stack := []rune{}
    pairs := map[rune]rune{
        ')': '(',
        ']': '[',
        '}': '{',
    }

    for _, char := range s {
        // 左括号入栈
        if char == '(' || char == '[' || char == '{' {
            stack = append(stack, char)
        } else {
            // 右括号:检查栈顶是否匹配
            if len(stack) == 0 || stack[len(stack)-1] != pairs[char] {
                return false
            }
            stack = stack[:len(stack)-1] // 弹出栈顶
        }
    }

    return len(stack) == 0 // 栈空说明全部匹配
}

单调栈 - 每日温度:

// 题目:给定温度数组,返回每天需要等多少天才会有更高温度

func dailyTemperatures(temperatures []int) []int {
    n := len(temperatures)
    result := make([]int, n)
    stack := []int{} // 存储下标,保持对应温度单调递减

    for i := 0; i < n; i++ {
        // 当前温度比栈顶高,说明栈顶元素找到了答案
        for len(stack) > 0 && temperatures[i] > temperatures[stack[len(stack)-1]] {
            prevIdx := stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            result[prevIdx] = i - prevIdx
        }
        stack = append(stack, i)
    }

    return result
}

5. 队列 Queue

核心特性:

  • 先进先出(FIFO)
  • 两端操作

什么时候用:

  • BFS 广度优先搜索
  • 滑动窗口(用双端队列)

经典题目 - 二叉树层序遍历:

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

func levelOrder(root *TreeNode) [][]int {
    if root == nil {
        return nil
    }

    result := [][]int{}
    queue := []*TreeNode{root}

    for len(queue) > 0 {
        levelSize := len(queue) // 当前层的节点数
        level := []int{}

        // 处理当前层的所有节点
        for i := 0; i < levelSize; i++ {
            node := queue[0]
            queue = queue[1:] // 出队

            level = append(level, node.Val)

            // 子节点入队(下一层)
            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        result = append(result, level)
    }

    return result
}

四、选择数据结构的决策树

面试时遇到问题,按这个思路选择:

                 需要什么操作?
                      │
        ┌─────────────┼─────────────┐
        │             │             │
    快速查找    保持顺序/排序    模拟过程
        │             │             │
    哈希表        ┌────┴────┐    ┌──┴──┐
                 │         │    │     │
              有序数组    树   栈     队列
              (二分)          (括号)  (BFS)

实战例子:

问题选择原因
两数之和哈希表需要快速查找补数
排序后的数组查找二分有序 + 快速查找
括号匹配栈后进先出特性
层序遍历队列按层处理,先进先出
前K大元素堆维护有序的前K个

五、刷题策略:效率比数量更重要

推荐刷题顺序

第1周:数组和哈希表(最常考)

  • 两数之和
  • 最大子数组和
  • 合并区间
  • 字母异位词分组

第2周:链表(必考)

  • 反转链表
  • 合并有序链表
  • 环形链表
  • 删除链表倒数第N个节点

第3周:栈和队列

  • 有效括号
  • 每日温度
  • 用栈实现队列
  • 滑动窗口最大值

第4周:二叉树(高频)

  • 层序遍历
  • 最大深度
  • 验证二叉搜索树
  • 最近公共祖先

如何高效刷题

错误做法:

  • 看题5分钟就看答案
  • 刷完就过,不复盘
  • 只追求数量,不总结套路

正确做法:

1. 先思考15-20分钟,想到思路再写
2. 写完后分析复杂度,思考是否有更优解
3. 记录解题思路,过几天再做一遍
4. 同类型题目放一起,总结模板

必背模板

二分查找模板:

func binarySearch(nums []int, target int) int {
    left, right := 0, len(nums)-1

    for left <= right {
        mid := left + (right-left)/2 // 防止溢出

        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }

    return -1
}

BFS模板:

func bfs(start Node) {
    queue := []Node{start}
    visited := make(map[Node]bool)
    visited[start] = true

    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]

        // 处理当前节点
        process(node)

        // 将相邻节点加入队列
        for _, neighbor := range node.neighbors {
            if !visited[neighbor] {
                visited[neighbor] = true
                queue = append(queue, neighbor)
            }
        }
    }
}

DFS模板:

func dfs(node Node, visited map[Node]bool) {
    if node == nil || visited[node] {
        return
    }

    visited[node] = true
    process(node)

    for _, neighbor := range node.neighbors {
        dfs(neighbor, visited)
    }
}

六、面试中常犯的错误

错误1:上来就写代码

正确做法: 先和面试官确认理解是否正确

"让我确认一下,输入是一个整数数组..."
"数组元素可以重复吗?"
"数据范围大概是多少?10^6 级别吗?"

错误2:只想到暴力解

正确做法: 先说暴力解,再优化

"暴力解法是O(n²),遍历所有组合..."
"但如果用哈希表存储已遍历的元素..."
"可以优化到O(n),空间复杂度增加到O(n)..."

错误3:代码写完就结束

正确做法: 主动测试

"我用这个用例走一遍:[2, 7, 11, 15], target=9..."
"边界情况:空数组会返回nil..."
"再测一个无解的情况..."

七、本章小结

一张图总结算法面试:

             ┌─────────────────────┐
             │   面试官在考察什么?   │
             └─────────────────────┘
                       │
         ┌─────────────┼─────────────┐
         │             │             │
    ┌────┴────┐   ┌────┴────┐   ┌────┴────┐
    │ 编码能力 │   │ 思考过程 │   │ 沟通表达 │
    └─────────┘   └─────────┘   └─────────┘
         │             │             │
    代码规范      说清思路       主动沟通
    边界处理      分析复杂度     确认理解
    测试意识      权衡方案       解释决策

记住这三点:

  1. 面试不是默写答案,是展示思考过程
  2. 复杂度分析是必考点,要能脱口而出
  3. 数据结构的选择比算法技巧更重要

下期预告

《双指针与滑动窗口:一个套路解决10道高频题》

内容预告:

  • 双指针的三种类型
  • 滑动窗口的万能模板
  • 实战:三数之和、最长无重复子串

关注公众号,明天见👆


你觉得最难的算法类型是什么?欢迎在评论区留言,高赞问题会在后续详细讲解。

Prev
8年面试官告诉你:90%的简历在第一轮就被刷掉了
Next
高频算法题精讲-双指针与滑动窗口