解题思路与方法论
拿到题目后的第一件事
不要急着写代码!
很多人一看到题目就开始敲代码,结果写到一半发现思路不对,又要重来。正确的做法是:
读题 → 分析 → 设计 → 编码 → 测试
第一步:读懂题目
1. 提取关键信息
| 关键点 | 要明确的问题 |
|---|---|
| 输入 | 数据类型?范围?有序还是无序? |
| 输出 | 返回什么?数组、数字、布尔值? |
| 约束 | 时间复杂度要求?空间复杂度要求? |
| 特殊情况 | 空输入?重复元素?负数? |
2. 用自己的话复述
读完题目后,尝试用一句话概括:
"这道题是要我 ____,给我 ____,让我返回 ____"
例如:两数之和
"这道题是要我找两个数,给我一个数组和目标值,让我返回这两个数的下标"
3. 手动模拟例子
拿题目给的例子,用手在纸上走一遍,不要用脑子"想",要动手"做"。
例:两数之和
nums = [2, 7, 11, 15], target = 9
手动模拟:
- 看 2,需要找 9-2=7,数组里有 7 吗?有!在下标 1
- 所以答案是 [0, 1]
第二步:分析思路
1. 暴力解法先行
先想最笨的方法,哪怕是 或 也行。
暴力解法的好处:
- 帮你理解问题
- 作为优化的基准
- 面试时至少有东西可写
// 两数之和 - 暴力解法 O(n²)
function twoSum(nums: number[], target: number): number[] {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i, j];
}
}
}
return [];
}
2. 寻找优化点
问自己:哪里做了重复的工作?
暴力解法的问题:每次都要遍历整个数组找配对的数
优化思路:用哈希表记录已经看过的数,查找只需 O(1)
3. 联想常见套路
根据题目特征,联想学过的算法:
| 题目特征 | 可能的算法 |
|---|---|
| 有序数组 | 二分查找、双指针 |
| 找最值/方案数 | 动态规划 |
| 连续子串/子数组 | 滑动窗口 |
| 链表操作 | 双指针、递归 |
| 树的遍历 | DFS、BFS、递归 |
| 所有可能/排列组合 | 回溯 |
| 快速查找 | 哈希表 |
第三步:设计方案
1. 确定数据结构
选择合适的数据结构是成功的一半:
// 两数之和 - 优化解法
// 选择哈希表存储 {值: 下标}
const map = new Map<number, number>();
2. 写出伪代码
用中文/伪代码描述步骤,不要急着写真正的代码:
1. 创建一个哈希表
2. 遍历数组,对于每个数 num:
- 计算 complement = target - num
- 如果 complement 在哈希表中,返回结果
- 否则,把 num 和它的下标存入哈希表
3. 返回空数组(找不到)
3. 分析复杂度
在写代码前,先估算时间和空间复杂度:
时间复杂度:O(n) - 只需遍历一次
空间复杂度:O(n) - 哈希表最多存 n 个元素
第四步:编码实现
1. 先搭框架
function twoSum(nums: number[], target: number): number[] {
// TODO: 实现
return [];
}
2. 填充逻辑
按照伪代码,一步步实现:
function twoSum(nums: number[], target: number): number[] {
const map = new Map<number, number>();
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 [];
}
3. 代码风格
- 变量命名清晰:
complement比c好 - 适当加注释:解释"为什么"而不是"是什么"
- 保持简洁:能用一行就不用三行
第五步:测试验证
1. 用例子验证
手动跑一遍题目给的例子:
nums = [2, 7, 11, 15], target = 9
i=0: complement=7, map为空, map={2:0}
i=1: complement=2, map中有2! 返回 [0, 1] ✓
2. 考虑边界情况
| 边界情况 | 测试用例 |
|---|---|
| 空数组 | [], 1 |
| 只有一个元素 | [1], 2 |
| 目标在最后 | [1,2,3], 5 |
| 有重复元素 | [3,3], 6 |
| 负数 | [-1,2], 1 |
3. 检查常见错误
- 数组越界
- 空指针/undefined
- 整数溢出
- 死循环
- Off-by-one 错误(差一错误)
面试时的沟通技巧
1. 边想边说
不要闷头做题,把思考过程说出来:
"我先想一个暴力解法...两层循环可以解决,但时间复杂度是 ..."
"有没有更优的方法呢?我想用哈希表来优化..."
2. 主动提问
不确定的地方要问清楚:
- "数组是有序的吗?"
- "有重复元素怎么处理?"
- "返回任意一个答案还是所有答案?"
3. 承认不会
如果真的不会,不要假装会:
"这道题我没有思路,但我可以先写一个暴力解法,再想想怎么优化..."
解题 Checklist
□ 读懂题目了吗?能用一句话概括吗?
□ 手动模拟过例子吗?
□ 想到暴力解法了吗?
□ 能优化吗?用什么数据结构/算法?
□ 写出伪代码了吗?
□ 分析过复杂度吗?
□ 代码写完了吗?
□ 用例子验证过了吗?
□ 考虑过边界情况吗?
常见错误与避坑
1. 没看清题目
❌ 题目要返回下标,结果返回了值
❌ 题目要求原地修改,结果新建了数组
❌ 题目说有序,却用了 O(n²) 解法
2. 边界处理
// ❌ 错误:没考虑空数组
function findMax(nums: number[]): number {
let max = nums[0]; // 如果 nums 为空会报错
// ...
}
// ✅ 正确:先判断
function findMax(nums: number[]): number {
if (nums.length === 0) return -1; // 或抛出错误
let max = nums[0];
// ...
}
3. Off-by-one 错误
// ❌ 错误:多遍历了一个
for (let i = 0; i <= nums.length; i++) // length 应该取不到
// ✅ 正确
for (let i = 0; i < nums.length; i++)
刷题建议
1. 按专题刷
不要随机刷题,按专题来:
Week 1: 数组 + 双指针
Week 2: 链表
Week 3: 哈希表
Week 4: 二叉树
Week 5: 动态规划入门
...
2. 重复练习
一道题做完不是结束:
- 第1遍:看题解理解思路
- 第2遍:不看题解自己写
- 第3遍:隔几天再写一次
3. 总结模板
把常见题型的模板记下来:
// 二分查找模板
function binarySearch(nums: number[], target: number): number {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return mid;
if (nums[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}