🎓 毕业项目:面试模拟 — 综合6道LeetCode Hard

6道Hard综合实战,模拟真实面试流程——从审题到AC的完整思维链

📖 毕业项目说明

这是算法面试通关课程的最后一课。我们将模拟真实的面试场景,精选6道Hard题目,覆盖40课中最重要的算法思想。每道题我们会按照面试中的答题流程来分析:

面试答题流程 (每题约25分钟): Step 1: 审题理解 (2分钟) → 确认输入输出、边界条件、约束 → 画图理解示例 Step 2: 暴力解法 (3分钟) → 先说最直觉的做法 → 分析时间和空间复杂度 Step 3: 优化思路 (5分钟) → 识别可以用哪种算法思想 → 从暴力到最优的优化路径 Step 4: 编码实现 (10分钟) → 边写边注释 → 处理边界条件 Step 5: 验证测试 (5分钟) → 用示例手动验证 → 考虑边界用例 6道Hard覆盖: 1. LC 42 接雨水 → 双指针/单调栈/DP 2. LC 124 二叉树最大路径和 → 树形DP 3. LC 32 最长有效括号 → 栈/DP 4. LC 23 合并K个链表 → 堆/分治 5. LC 76 最小覆盖子串 → 滑动窗口 6. LC 312 戳气球 → 区间DP

🎯 第1题:接雨水 (LC 42) — 双指针思维

难度:Hard  |  标签:双指针、单调栈、DP  |  通过率:63%

面试时间分配:审题2min → 暴力3min → 优化5min → 编码10min → 验证5min

面试模拟对话: 你: "这道题要求计算柱子间能接多少雨水。 每个位置能接的水取决于它左边和右边的最高柱子。" 面试官: "先说一个暴力做法?" 你: "对每个位置i,向左找最高left_max,向右找最高right_max, water[i] = min(left_max, right_max) - height[i]。 时间O(n²),空间O(1)。" 面试官: "能优化吗?" 你: "可以预处理left_max和right_max数组,时间O(n),空间O(n)。 还可以用双指针优化到O(1)空间—— 从两端向中间移动,哪边小就处理哪边。"

双指针解法(最优)

class Solution:
    def trap(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        left_max = right_max = 0
        water = 0
        
        while left < right:
            if height[left] < height[right]:
                left_max = max(left_max, height[left])
                water += left_max - height[left]
                left += 1
            else:
                right_max = max(right_max, height[right])
                water += right_max - height[right]
                right -= 1
        
        return water

时间复杂度:O(n),空间复杂度:O(1)

面试要点:(1) 先说暴力解展示思维过程;(2) DP预处理是自然的优化;(3) 双指针是最终优化。三种解法代表三个思维层次,面试官最喜欢看到这个递进过程。

🎯 第2题:二叉树中的最大路径和 (LC 124) — 树形DP

难度:Hard  |  标签:树形DP、DFS  |  通过率:40%

面试模拟对话: 你: "路径可以从任意节点开始到任意节点结束, 但必须沿着父子连接走,不能重复经过节点。" 面试官: "怎么定义子问题?" 你: "对于节点node,经过它的最大路径有两种: 1. node作为路径端点: node.val + max(左贡献, 右贡献) 2. node作为路径中间: node.val + 左贡献 + 右贡献 递归时返回'作为端点的最大贡献'给父节点用, 同时用全局变量更新'作为中间节点'的最大路径和。" 关键: 贡献为负时可以不选(取0) -10 / \ 9 20 / \ 15 7 node=15: 端点贡献=15, 中间路径=15 node=7: 端点贡献=7, 中间路径=7 node=20: 端点贡献=20+15=35, 中间路径=20+15+7=42 ← 最优 node=9: 端点贡献=9, 中间路径=9 node=-10:端点贡献=-10+35=25, 中间路径=-10+35+9=34 最大路径和 = 42

代码实现

class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        max_sum = float('-inf')
        
        def dfs(node):
            nonlocal max_sum
            if not node:
                return 0
            
            # 递归获取左右子树的最大贡献(负贡献取0,不选)
            left = max(dfs(node.left), 0)
            right = max(dfs(node.right), 0)
            
            # 以当前节点为中间点的路径
            max_sum = max(max_sum, node.val + left + right)
            
            # 返回作为端点的贡献
            return node.val + max(left, right)
        
        dfs(root)
        return max_sum

时间复杂度:O(n),空间复杂度:O(h)(h为树高)

树形DP的通用模式:(1) 递归处理子问题;(2) 组合子问题的答案得到当前节点的答案;(3) 区分"返回给父节点的值"和"用于更新全局最优的值"。

🎯 第3题:最长有效括号 (LC 32) — 栈/DP双解法

难度:Hard  |  标签:栈、DP  |  通过率:37%

面试模拟对话: 你: "给一个只含(和)的字符串,找最长合法括号子串的长度。" 面试官: "合法括号子串有什么性质?" 你: "可以用栈来匹配。关键思路: 栈底始终保存'最后一个未匹配的右括号位置'。 遇到(入栈索引,遇到)弹出栈顶: - 弹出后栈空 → 当前长度 = i - 上一个未匹配右括号位置 - 弹出后栈非空 → 当前长度 = i - 栈顶索引" 示例: ")()())" i=0 ')': 栈初始[-1], 弹出-1后栈空→push(0) i=1 '(': push(1), 栈=[0,1] i=2 ')': 弹出1, 栈=[0], len=2-0=2 ✓ i=3 '(': push(3), 栈=[0,3] i=4 ')': 弹出3, 栈=[0], len=4-0=4 ✓ ← 最优 i=5 ')': 弹出0, 栈空→push(5) 最长 = 4

栈解法

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        stack = [-1]  # 栈底: 最后一个未匹配的右括号
        max_len = 0
        
        for i, ch in enumerate(s):
            if ch == '(':
                stack.append(i)
            else:
                stack.pop()
                if not stack:
                    stack.append(i)  # 更新未匹配右括号位置
                else:
                    max_len = max(max_len, i - stack[-1])
        
        return max_len

DP解法

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        dp = [0] * n  # dp[i] = 以s[i]结尾的最长合法括号长度
        max_len = 0
        
        for i in range(1, n):
            if s[i] == ')':
                if s[i-1] == '(':  # ...()
                    dp[i] = (dp[i-2] if i >= 2 else 0) + 2
                elif i - dp[i-1] - 1 >= 0 and s[i-dp[i-1]-1] == '(':  # ...))
                    dp[i] = dp[i-1] + 2 + (dp[i-dp[i-1]-2] if i-dp[i-1]-2 >= 0 else 0)
                max_len = max(max_len, dp[i])
        
        return max_len

时间复杂度:O(n),空间复杂度:O(n)

LC 32 的两种解法对比:栈解法更直觉(跟踪未匹配位置),DP解法更难想但空间可以优化到O(1)(用双指针双向扫描)。面试中推荐栈解法,思路清晰不易出错。

🎯 第4题:合并K个升序链表 (LC 23) — 堆/分治

难度:Hard  |  标签:堆、分治、链表  |  通过率:56%

面试模拟对话: 你: "合并K个有序链表,可以用最小堆。每次取堆顶 (当前K个链表头的最小值)加入结果,然后将该节点的 next推入堆中。" 面试官: "时间复杂度?" 你: "每个节点入堆出堆各一次,堆大小最多K。 O(N log K),N是总节点数。" 面试官: "还有其他方法?" 你: "分治合并——两两合并,类似归并排序。 同样O(N log K),但不需要额外空间。" 堆方法: ┌─────────────────────────────────────┐ │ L1: 1 → 4 → 5 │ │ L2: 1 → 3 → 4 │ │ L3: 2 → 6 │ │ │ │ 初始堆: [1(L1), 1(L2), 2(L3)] │ │ 弹1(L1) → push 4(L1) │ │ 弹1(L2) → push 3(L2) │ │ 弹2(L3) → push 6(L3) │ │ ... │ │ 结果: 1→1→2→3→4→4→5→6 │ └─────────────────────────────────────┘

堆解法

import heapq

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        # 用 (val, list_idx, node) 避免比较 ListNode
        heap = []
        for i, node in enumerate(lists):
            if node:
                heapq.heappush(heap, (node.val, i, node))
        
        dummy = ListNode(0)
        cur = dummy
        
        while heap:
            val, idx, node = heapq.heappop(heap)
            cur.next = node
            cur = cur.next
            if node.next:
                heapq.heappush(heap, (node.next.val, idx, node.next))
        
        return dummy.next

分治解法

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        if not lists:
            return None
        
        def merge2(l1, l2):
            dummy = ListNode(0)
            cur = dummy
            while l1 and l2:
                if l1.val <= l2.val:
                    cur.next = l1
                    l1 = l1.next
                else:
                    cur.next = l2
                    l2 = l2.next
                cur = cur.next
            cur.next = l1 or l2
            return dummy.next
        
        # 分治合并,类似归并排序
        interval = 1
        while interval < len(lists):
            for i in range(0, len(lists) - interval, interval * 2):
                lists[i] = merge2(lists[i], lists[i + interval])
            interval *= 2
        
        return lists[0]

堆解法:时间 O(N log K),空间 O(K)

分治解法:时间 O(N log K),空间 O(log K)(递归栈)

🎯 第5题:最小覆盖子串 (LC 76) — 滑动窗口

难度:Hard  |  标签:滑动窗口、哈希  |  通过率:44%

面试模拟对话: 你: "找s中包含t所有字符的最短子串。滑动窗口, 用counter记录需求,formed记录已满足的字符种数。" 面试官: "formed怎么判断?" 你: "当window[ch]达到need[ch]时formed++, 当window[ch]低于need[ch]时formed--。 formed == len(need)时窗口满足条件。" 面试官: "时间复杂度?" 你: "每个字符最多被左右指针各访问一次,O(|s|+|t|)。"

代码实现

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need = Counter(t)
        window = Counter()
        formed = 0
        need_cnt = len(need)
        
        left = 0
        min_len = float('inf')
        min_start = 0
        
        for right, ch in enumerate(s):
            window[ch] += 1
            if ch in need and window[ch] == need[ch]:
                formed += 1
            
            while formed == need_cnt:
                if right - left + 1 < min_len:
                    min_len = right - left + 1
                    min_start = left
                
                left_ch = s[left]
                if left_ch in need and window[left_ch] == need[left_ch]:
                    formed -= 1
                window[left_ch] -= 1
                left += 1
        
        return s[min_start:min_start + min_len] if min_len != float('inf') else ""

时间复杂度:O(|s| + |t|),空间复杂度:O(字符集)

🎯 第6题:戳气球 (LC 312) — 区间DP

难度:Hard  |  标签:区间DP  |  通过率:68%

面试模拟对话: 你: "戳气球的关键是逆向思维——不先戳哪个,而是 最后戳哪个。最后戳的k,其左右邻居就是区间边界i和j, 收益 = nums[i] * nums[k] * nums[j]。" 面试官: "状态转移方程?" 你: "dp[i][j] = 开区间(i,j)的最大收益 dp[i][j] = max(dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]) 其中 i < k < j 按区间长度从短到长填表。" 面试官: "为什么要加虚拟边界?" 你: "当k是第一个或最后一个被戳的,它的邻居是 边界外的1。所以前后各加一个1作为哨兵。"

代码实现

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        n = len(nums)
        arr = [1] + nums + [1]
        m = n + 2
        
        dp = [[0] * m for _ in range(m)]
        
        for length in range(2, m):
            for i in range(m - length):
                j = i + length
                for k in range(i + 1, j):
                    dp[i][j] = max(dp[i][j],
                        dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j])
        
        return dp[0][m - 1]

时间复杂度:O(n³),空间复杂度:O(n²)

🔬 6道Hard的算法思想映射

┌──────────────────────────────────────────────────────────┐ │ 题目 算法思想 课程来源 面试频率 │ ├──────────────────────────────────────────────────────────┤ │ LC 42 接雨水 双指针/单调栈 第34/38课 ★★★★★ │ │ LC 124 路径和 树形DP 第23课 ★★★★☆ │ │ LC 32 有效括号 栈/DP 第6/18课 ★★★★☆ │ │ LC 23 合并K链表 堆/分治 第15/16课 ★★★★★ │ │ LC 76 覆盖子串 滑动窗口 第37课 ★★★★★ │ │ LC 312 戳气球 区间DP 第35课 ★★★☆☆ │ ├──────────────────────────────────────────────────────────┤ │ │ │ 面试中Hard题的应对策略: │ │ 1. 不追求一次写对最优解——先暴力再优化 │ │ 2. 画图!画图!画图!——直觉来源于可视化 │ │ 3. 识别题型模式——90%的Hard是已知算法的组合 │ │ 4. 逆向思维——正向难以递推时,反过来想 │ │ 5. 边界条件——Hard的边界往往比Medium多 │ └──────────────────────────────────────────────────────────┘

🔬 面试模拟实战技巧

1. 时间管理

45分钟面试的时间分配:
- 0-5分钟:自我介绍 + 题目理解
- 5-10分钟:讨论思路,说暴力解法
- 10-25分钟:编码实现最优解
- 25-35分钟:测试验证 + 优化讨论
- 35-45分钟:追问 + 反问

2. 沟通技巧

边想边说:不要沉默太久。把你的思考过程说出来,即使还没有完整方案。面试官可以给你提示,但前提是你展示了思考过程。
主动给复杂度分析:不用说"时间复杂度是..."等面试官问。说完思路就主动分析,展示你的分析能力。
先写框架再填细节:先写出整体框架(循环结构、递归函数签名),再填充具体逻辑。这样即使时间不够,面试官也能看到你的思路。

3. 常见坑点

# 坑1: 整数溢出 (Python不存在, 但面试官可能问)
# Java: int → long, 注意 mid = left + (right - left) / 2

# 坑2: 空输入
if not nums: return 0  # 养成习惯!

# 坑3: 循环条件 left <= right vs left < right
# 找精确值: left <= right (返回mid)
# 找边界:   left < right  (返回left/right)

# 坑4: 深拷贝 vs 浅拷贝
# Python中 list[:] 是浅拷贝, 嵌套结构用 deepcopy

# 坑5: 递归深度
# Python默认1000, 需要 sys.setrecursionlimit(10**6)

🔬 40课知识体系全景图

算法面试通关 · 40课知识体系 📐 基础篇 (1-7) ├── 01 数组基础 ├── 02 双指针 ├── 03 滑动窗口 ├── 04 二分查找 ├── 05 链表 ├── 06 栈 └── 07 队列 🧩 进阶篇 (8-14) ├── 08 哈希表 ├── 09 前缀和 ├── 10 排序 ├── 11 二叉树 ├── 12 BFS ├── 13 DFS └── 14 回溯 ⚡ 高级篇 (15-24) ├── 15 堆 ├── 16 优先队列 ├── 17 贪心 ├── 18 DP入门 ├── 19 背包问题 ├── 20 子序列DP ├── 21 路径DP ├── 22 字符串DP ├── 23 树形DP └── 24 状态机DP 🏆 精通篇 (25-34) ├── 25 图基础 ├── 26 拓扑排序 ├── 27 最短路径 ├── 28 并查集 ├── 29 字典树 ├── 30 区间问题 ├── 31 数学技巧 ├── 32 位运算 ├── 33 数据结构设计 └── 34 综合训练 🎓 冲刺篇 (35-40) ├── 35 DP进阶(背包/区间DP) ├── 36 贪心进阶(跳跃/区间) ├── 37 滑动窗口进阶(子串) ├── 38 单调栈/队列 ├── 39 二分搜索进阶 └── 40 毕业项目(面试模拟)

📝 6道Hard + 扩展题单

  1. LC 42 接雨水(双指针/单调栈)✅
  2. LC 124 二叉树最大路径和(树形DP)✅
  3. LC 32 最长有效括号(栈/DP)✅
  4. LC 23 合并K个升序链表(堆/分治)✅
  5. LC 76 最小覆盖子串(滑动窗口)✅
  6. LC 312 戳气球(区间DP)✅
  7. LC 41 缺失的第一个正数(原地哈希)
  8. LC 51 N皇后(回溯)
  9. LC 85 最大矩形(单调栈2D)
  10. LC 297 二叉树序列化(DFS/BFS)

🔑 最终知识点清单

成就解锁:算法面试通关 — 完成全部40课!从数组基础到Hard综合实战,系统掌握算法面试核心知识!🎓🎉
LeetCode AC验证:LC 42 Trap Rain Water ✅ | LC 124 Max Path Sum ✅ | LC 32 Longest Valid Parentheses ✅ | LC 23 Merge K Lists ✅ | LC 76 Min Window Substring ✅ | LC 312 Burst Balloons ✅

🎓 恭喜毕业!

40课,200+道LeetCode,覆盖算法面试95%+的考点

从数组基础到Hard实战,你已经具备了算法面试通关的实力

记住:算法面试不只是背题,更是思维的训练 💪

持续练习 · 定期复盘 · 面试顺利 🚀

📚 面试备战路线图(完整版)

  1. 第1-2周:基础篇(1-7课),每天1课 + 3道练习题
  2. 第3-4周:进阶篇(8-14课),每天1课 + 5道练习题
  3. 第5-6周:高级篇(15-24课),每天1课 + 5道练习题
  4. 第7-8周:精通篇(25-34课),每天1课 + 5道练习题
  5. 第9-10周:冲刺篇(35-40课),每天1课 + 5道练习题
  6. 第11-12周:模拟面试,限时刷题,复盘错题

总计约 40课 × 8题 = 320道题,覆盖面试高频题的 90% 以上。坚持 12 周,算法面试无忧。

推荐刷题节奏:Easy 5min/题 → Medium 15-20min/题 → Hard 25-35min/题