跳到正文
Java 入门到面试指南
返回

03-算法刷题路线

阶段:面试篇 | 建议总量:热题 100 + 剑指 Offer 为主,200 题以内 | 节奏:从进阶篇开始每天 2~3 题,面试前保持手感

一、刷题总原则

  • 先专题后套卷:按下面的专题顺序集中攻一类套路,形成肌肉记忆;面试前一个月再刷 Hot 100 混排,模拟真实考试;
  • 每天 2~3 题、限时:简单题 15 分钟、中等题 25 分钟、困难题 40 分钟;到时没思路就看题解,看懂后合上题解默写才算完成;
  • 错题隔天重做:建一个”重刷清单”,3 天后重做仍能独立写出才打勾;面试前只重刷清单里的题;
  • 一题多解:每题先写暴力解,再逐步优化,每个解法都说得出时间/空间复杂度——面试官就是要听你的优化过程;
  • 总量控制:200 题以内、质量优先;刷 500 遘题不如把 150 道经典题刷透;
  • 与指南进度配合:进阶篇开始同步刷数组与链表,03-面试篇期间完成主干专题,冲刺期每天 1 题保手感。

二、Java 刷题常用 API ⭐

// —— List / 数组 ——
List<Integer> list = new ArrayList<>();
list.add(1); list.get(0); list.size();
list.sort(Comparator.comparingInt(a -> a));        // List 自定义升序
Collections.sort(list, (a, b) -> b - a);           // 降序
int[] arr = {3, 1, 2};
Arrays.sort(arr);                                  // 数组排序(基本类型只能升序)
int[][] intervals = {{2, 1}, {3, 2}};
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);     // 二维数组按首元素升序(区间题必用)

// —— Deque:用 ArrayDeque 同时当栈和队列,比 Stack/LinkedList 快 ——
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); stack.peek(); stack.pop();          // 栈:压 / 瞥 / 弹
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); queue.peek(); queue.poll();        // 队列:入 / 瞥 / 出

// —— PriorityQueue 堆:Top K、堆排序题专用 ——
PriorityQueue<Integer> minHeap = new PriorityQueue<>();                  // 小顶堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);   // 大顶堆
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);    // 按元素第 2 位排序
minHeap.offer(1); minHeap.peek(); minHeap.poll();

// —— HashMap:计数、映射、前缀和 ——
Map<Character, Integer> map = new HashMap<>();
map.put('a', 1); map.get('a'); map.containsKey('a');
map.getOrDefault('b', 0);
map.merge('a', 1, Integer::sum);                   // 计数神器:不存在则 1,存在则累加
map.remove('a');

// —— 常用小工具 ——
// Character.isDigit(c)、c - '0' 转 int、Integer.parseInt(s)、String.valueOf(n)
// StringBuilder 拼接;s.toCharArray() 与 new String(chars) 互转
// Integer.MIN_VALUE / MAX_VALUE 做初始极值

二·补、复杂度与算法思想速查 ⭐

面试不只要写出代码,还会追问时间/空间复杂度与”为什么这么设计”。这一节是选择题与口头追问的高频弹药。

常见排序的复杂度(背表):

算法平均最坏额外空间稳定性备注
快速排序O(nlogn)O(n²)O(logn) 栈不稳定最坏:每次划分极不均匀(如已有序数组固定取首元素为基准,递归深度退化成 n)
归并排序O(nlogn)O(nlogn)O(n)稳定唯一”最坏也能保证 nlogn”的常用排序,代价是额外数组
堆排序O(nlogn)O(nlogn)O(1)不稳定原地排序;建堆 O(n)、每次调整 O(logn)
插入排序O(n²)O(n²)O(1)稳定近乎有序时接近 O(n),小数组优于快排
计数/桶排序O(n+k)O(n+k)O(n+k)稳定非比较排序,值域必须可控
  • 口径:“最坏仍 O(nlogn)“只有归并(和堆排序)能保证;快排 O(nlogn) 是平均,最坏 O(n²) 是最常见的时间复杂度误解之一。
  • 改进:三数取中 / 随机化基准 / 小区间切插入排序,可显著降低快排退化概率(工业库都这么做)。

看循环算复杂度:看每层循环的步进方式,不是嵌套层数。

for (int i = 1; i < n; i *= 2)      // i 每次翻倍 → 只执行 log n 次
    for (int j = 0; j < n; j++)     // 内层固定 n 次
        sum++;                       // 合计 O(n log n),不是 O(n²)
  • 记忆:i++ 是 n 次、i *= 2 或 i /= 2 是 logn 次;两层相乘。三层循环若外层是倍增,往往是 O(n²logn) 或 O(nlogn)。

建堆是 O(n),不是 O(nlogn)(上界 ≠ 紧界):自底向上建堆时,n 次 sift-down 的上界是 n×O(logn),但实际代价按层求和——大部分节点在底层、下沉高度很小,Σ(每层节点数 × 该层最大下沉高度) 收敛于 O(n)。

  • 对比:逐个 insert 建堆才是 O(nlogn);这也是堆排序整体复杂度仍为 O(nlogn) 的原因(建堆 O(n) + n 次取堆顶并调整 O(nlogn))。
  • 另一个复杂度反直觉点:HashMap 平均 O(1)、最坏 O(n)(所有 key 冲突到同一桶;JDK 8 起链表超阈值转红黑树,最坏降为 O(logn),详见《03-集合与泛型》)。

TopK / 第 K 大:小顶堆 O(n log K):遍历 n 个数是 O(n),每次入堆/出堆 O(logK),合计 O(nlogK);K 远小于 n 时明显优于整体排序的 O(nlogn),且只需 O(K) 空间——“海量数据取 Top K”的标准解。选小顶堆是为了让堆顶正好是当前第 K 大,超出容量就弹掉堆顶。

二分的两种取整方向必须与收缩方向配套(死循环高发区):下取整 mid = (l + r) / 2 在区间只剩两个元素时会等于 left,若用 left = mid 收缩则 left 永不前进 → 死循环。两种修法:① 改成 left = mid + 1(配 r = mid);② 把 mid 改成上取整 (l + r + 1) / 2 再配 left = mid。背模板时两者必须成对记(模板 2 见下)。

三、专题路线表(顺序学习)⭐

顺序专题天数代表题(LC 题号 + 题名)核心技巧
1数组与双指针3LC1 两数之和、LC15 三数之和、LC11 盛最多水的容器、LC283 移动零、LC26 删除有序数组中的重复项、LC121 买卖股票的最佳时机、LC53 最大子数组和、LC42 接雨水排序后左右夹逼;原地双指针;前缀和
2链表3LC206 反转链表、LC21 合并两个有序链表、LC141 环形链表、LC142 环形链表 II、LC19 删除链表的倒数第 N 个结点、LC160 相交链表、LC234 回文链表、LC25 K 个一组翻转链表虚拟头节点省边界判断;快慢指针找中点/判环
3栈与队列2LC20 有效的括号、LC155 最小栈、LC232 用栈实现队列、LC739 每日温度、LC84 柱状图中最大的矩形、LC239 滑动窗口最大值单调栈/单调队列:“找下一个更大/更小”就想到它
4哈希表2LC217 存在重复元素、LC49 字母异位词分组、LC383 赎金信、LC128 最长连续序列、LC560 和为 K 的子数组、LC1 两数之和(哈希解法重做)空间换时间;前缀和 + 哈希计”和为 K 的区间”
5二叉树与递归4LC104 二叉树的最大深度、LC226 翻转二叉树、LC101 对称二叉树、LC110 平衡二叉树、LC236 二叉树的最近公共祖先、LC105 从前序与中序遍历序列构造二叉树、LC98 验证二叉搜索树、LC124 二叉树中的最大路径和递归三要素:终止条件 / 本级处理 / 返回值;分清前中后序位置
6BFS/DFS3LC102 二叉树的层序遍历、LC200 岛屿数量、LC695 岛屿的最大面积、LC994 腐烂的橘子、LC207 课程表、LC210 课程表 II层序模板固定 size 分层;网格 DFS 标记已访问;拓扑排序数入度
7回溯3LC46 全排列、LC78 子集、LC77 组合、LC39 组合总和、LC22 括号生成、LC131 分割回文串、LC51 N 皇后决策树:做选择 → 递归 → 撤销;used 数组与剪枝去重
8贪心2LC55 跳跃游戏、LC45 跳跃游戏 II、LC122 买卖股票的最佳时机 II、LC435 无重叠区间、LC452 用最少数量的箭引爆气球、LC134 加油站先排序找局部最优,再论证推出全局最优
9二分查找2LC704 二分查找、LC35 搜索插入位置、LC34 在排序数组中查找元素的第一个和最后一个位置、LC33 搜索旋转排序数组、LC153 寻找旋转排序数组中的最小值、LC162 寻找峰值先问单调性,再定区间开闭与收缩方向,边界不猜
10动态规划·基础4LC70 爬楼梯、LC118 杨辉三角、LC198 打家劫舍、LC213 打家劫舍 II、LC62 不同路径、LC64 最小路径和、LC343 整数拆分先定状态与转移方程,再抠初始化与遍历顺序
11动态规划·背包与子序列4LC416 分割等和子集(0-1 背包)、LC322 零钱兑换、LC518 零钱兑换 II(完全背包)、LC300 最长递增子序列、LC1143 最长公共子序列、LC72 编辑距离、LC5 最长回文子串背包按”容量维度”展开;子序列想”以 i/j 结尾”的定义
12排序与堆2LC912 排序数组、LC215 数组中的第 K 个最大元素、LC347 前 K 个高频元素、LC23 合并 K 个升序链表、LC56 合并区间手写快排/归并应付”手撕排序”;Top K 用堆

四、手撕代码模板 ⭐(背诵级,面试默写)

模板 1:反转链表(LC206)

public ListNode reverseList(ListNode head) {
    ListNode prev = null, curr = head;
    while (curr != null) {
        ListNode next = curr.next;   // 先存后继
        curr.next = prev;            // 反转指向
        prev = curr;                 // 双指针前移
        curr = next;
    }
    return prev;                     // prev 即新头
}

模板 2:二分查找(LC704,左闭右闭)

public int search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {                    // 区间合法才继续
        int 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;
}

模板 3:快速排序

public void quickSort(int[] nums, int left, int right) {
    if (left >= right) return;
    int pivot = nums[left], i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= pivot) j--;   // 右侧找小的
        while (i < j && nums[i] <= pivot) i++;   // 左侧找大的
        swap(nums, i, j);
    }
    swap(nums, left, i);                          // 基准归位
    quickSort(nums, left, i - 1);
    quickSort(nums, i + 1, right);
}
private void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }

模板 4:归并排序

public int[] mergeSort(int[] nums, int left, int right) {
    if (left == right) return new int[]{nums[left]};
    int mid = left + (right - left) / 2;
    int[] l = mergeSort(nums, left, mid), r = mergeSort(nums, mid + 1, right);
    int[] res = new int[l.length + r.length];
    int i = 0, j = 0, k = 0;
    while (i < l.length && j < r.length) res[k++] = l[i] <= r[j] ? l[i++] : r[j++];
    while (i < l.length) res[k++] = l[i++];
    while (j < r.length) res[k++] = r[j++];
    return res;
}

模板 5:BFS 层序遍历(LC102,网格 BFS 同骨架)

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) return res;
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        int size = queue.size();               // 固定本层节点数,实现"分层"
        List<Integer> level = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        res.add(level);
    }
    return res;
}

模板 6:DFS 全排列(LC46,一切回溯题的骨架)

public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    dfs(nums, new boolean[nums.length], new ArrayList<>(), res);
    return res;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> res) {
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));        // 必须拷贝快照
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        used[i] = true; path.add(nums[i]);     // 做选择
        dfs(nums, used, path, res);
        path.remove(path.size() - 1); used[i] = false;   // 撤销选择
    }
}

模板 7:LRU 缓存(LC146)——先说 LinkedHashMap 思路版

class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);          // 第三个参数 accessOrder=true = 按访问顺序排序(false 则按插入序)
        this.capacity = capacity;
    }
    public int get(int key) { return super.getOrDefault(key, -1); }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;              // 超容量淘汰最久未访问
    }
}

模板 7(进阶):手写 HashMap + 双向链表版

class LRUCache {
    private final int capacity;
    private final Map<Integer, Node> map = new HashMap<>();
    private final Node head = new Node(0, 0), tail = new Node(0, 0);  // 哨兵

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail; tail.prev = head;
    }
    public int get(int key) {
        Node node = map.get(key);
        if (node == null) return -1;
        moveToHead(node);                      // 刚访问的提到最前
        return node.value;
    }
    public void put(int key, int value) {
        Node node = map.get(key);
        if (node != null) { node.value = value; moveToHead(node); return; }
        if (map.size() == capacity) {          // 淘汰链表尾部(最久未用)
            Node last = tail.prev;
            remove(last); map.remove(last.key);
        }
        Node fresh = new Node(key, value);
        map.put(key, fresh); addToHead(fresh);
    }
    private void addToHead(Node n) { n.next = head.next; n.prev = head; head.next.prev = n; head.next = n; }
    private void remove(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }
    private void moveToHead(Node n) { remove(n); addToHead(n); }
    static class Node { int key, value; Node prev, next; Node(int k, int v) { key = k; value = v; } }
}

五、面试场景应对

五步法(按顺序执行,全程开口讲):

  1. 澄清题意:数据规模多大、是否有序/有重复、边界情况(空、单元素、负数),复述题目与面试官确认;
  2. 说思路:先讲暴力解再讲优化方向,征求面试官同意后再动手,避免闷头写偏;
  3. 报复杂度:时间/空间复杂度各是多少、为什么,这是面试官判断你是否受过训练的关键;
  4. 动手写:变量命名规范、保持缩进,边写边讲;卡住就先写伪代码骨架再填实现;
  5. 自测用例:写完主动跑一遍空输入、单元素、重复值等用例,自己先抓 bug 比被抓强。

平台注意事项:

  • 白板/纸面:先写函数骨架再填细节,保持布局整洁,写完站在面试官角度”通读”一遍;
  • 牛客 ACM 模式:要自己处理输入输出,提前练 Scanner/BufferedReader 读多行数据的标准套路;
  • 共享文档(无编译器):靠脑内编译,写完口头逐行过一遍用例,主动说”这里我检查一下越界”;
  • 卡壳时:绝不沉默——大声思考,从 n=1、n=2 的小例子找规律,面试官往往在等你的思考过程而不是答案。

推荐资料

资料说明链接
pdai 数据结构与算法导读算法知识体系与各专题的索引入口https://pdai.tech/md/outline/x-outline.html
JavaGuide 后端面试通关计划算法在整体求职准备中的位置与节奏https://javaguide.cn/interview-preparation/backend-interview-plan.html