一、为什么需要科学的刷题策略
刷题这件事吧,就像健身一样,不是练得越多效果越好。我见过不少同学每天刷十几道题,坚持三个月后去面试,结果被问到的全是没练过的题型。也遇到过只刷了200道题就拿下FLAG offer的案例,区别就在于策略的科学性。
举个例子,假设你正在准备Java技术栈的面试:
// 经典高频题:两数之和(LeetCode第1题)
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No solution");
}
// 时间复杂度O(n),空间复杂度O(n)
// 使用哈希表实现快速查找,是面试中最常考的基础题
这道题在亚马逊出现频率高达62%,这就是典型的高频题。而像"正则表达式匹配"这种hard题,实际面试出现率不足5%。所以我们需要根据出现概率来分配精力。
二、黄金比例:高频/中等/难题的分配
经过分析上百份面经后,我总结出这个比例:高频题:中等题:难题 = 6:3:1。具体来说:
- 高频题(占60%):指出现率前20%的题目
- 中等题(占30%):能覆盖80%场景的典型题
- 难题(占10%):用于突破天花板
以Java技术栈为例的中等题示范:
// 中等难度:二叉树的锯齿形层序遍历(LeetCode 103)
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true;
while (!queue.isEmpty()) {
int levelSize = queue.size();
LinkedList<Integer> level = new LinkedList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
if (leftToRight) {
level.addLast(node.val);
} else {
level.addFirst(node.val);
}
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(level);
leftToRight = !leftToRight;
}
return res;
}
// 使用队列+BFS实现,注意方向切换的逻辑
三、时间规划的三阶段模型
第一阶段:筑基期(1-2周)
主攻高频题,每天5-8道。例如:
// 高频题示例:有效的括号(LeetCode 20)
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')');
else if (c == '{') stack.push('}');
else if (c == '[') stack.push(']');
else if (stack.isEmpty() || stack.pop() != c) {
return false;
}
}
return stack.isEmpty();
}
// 使用栈结构处理对称性问题,注意边界条件判断
第二阶段:强化期(3-4周)
主攻中等题+高频题复习,每天3-5道新题+2道旧题。例如:
// 中等题:前K个高频元素(LeetCode 347)
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
PriorityQueue<Integer> heap = new PriorityQueue<>(
(a, b) -> frequencyMap.get(a) - frequencyMap.get(b));
for (int num : frequencyMap.keySet()) {
heap.add(num);
if (heap.size() > k) {
heap.poll();
}
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = heap.poll();
}
return result;
}
// 使用最小堆维护TopK,时间复杂度O(nlogk)
第三阶段:冲刺期(1周)
难题突破+模拟面试,每天2道难题+3次模拟。例如:
// 难题示例:接雨水(LeetCode 42)
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int result = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
result += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
result += rightMax - height[right];
}
right--;
}
}
return result;
}
// 双指针解法,时间复杂度O(n),空间复杂度O(1)
四、常见误区与应对策略
- 盲目追求数量:有位同学刷了800道题,但前100道就覆盖了面试的70%考点
- 忽视重复练习:建议使用Anki进行间隔重复
- 过早接触难题:就像还没学会走就想跑
这里有个反面教材示例:
// 错误示范:过度优化导致代码可读性下降
public int maxProfit(int[] prices) {
int t1Cost = Integer.MAX_VALUE, t2Cost = Integer.MAX_VALUE;
int t1Profit = 0, t2Profit = 0;
for (int price : prices) {
t1Cost = Math.min(t1Cost, price);
t1Profit = Math.max(t1Profit, price - t1Cost);
t2Cost = Math.min(t2Cost, price - t1Profit);
t2Profit = Math.max(t2Profit, price - t2Cost);
}
return t2Profit;
}
// 虽然这是买卖股票的最佳时机IV的最优解,但面试时可能难以解释清楚
五、个性化调整建议
- 基础薄弱者:可以调整为7:2:1的比例
- 短期冲刺者:建议5:4:1的比例
- 目标公司差异:FLAG需要更多难题,中小厂高频题更重要
示例调整方案:
// 针对基础薄弱者的调整方案:增加基础数据结构练习
public class MyStack {
private Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<>();
}
public void push(int x) {
queue.offer(x);
for (int i = 1; i < queue.size(); i++) {
queue.offer(queue.poll());
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
// 用队列实现栈,帮助理解数据结构本质
六、工具与资源推荐
- LeetCode高频题库
- 《剑指Offer》经典题
- 公司定制题库(如亚马逊的Top 50)
这里有个资源使用示例:
// 来自《剑指Offer》的经典题:旋转数组的最小数字
public int minArray(int[] numbers) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (numbers[mid] > numbers[right]) {
left = mid + 1;
} else if (numbers[mid] < numbers[right]) {
right = mid;
} else {
right--;
}
}
return numbers[left];
}
// 二分查找变种,注意处理重复元素的情况
七、面试实战技巧
- 先说暴力解法再优化
- 主动写测试用例
- 时间复杂度分析要准确
实战示例:
// 面试时的完整演示:LRU缓存实现
class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
}
private void addNode(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addNode(node);
}
private DLinkedNode popTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
cache.put(key, newNode);
addNode(newNode);
if (cache.size() > capacity) {
DLinkedNode tail = popTail();
cache.remove(tail.key);
}
} else {
node.value = value;
moveToHead(node);
}
}
}
// 哈希表+双向链表实现,get和put操作都是O(1)时间复杂度
八、总结与行动建议
最后给个具体行动计划表:
- 第1天:制定计划,筛选高频题
- 第1周:完成50道高频题
- 第2周:高频题+20道中等题
- 第3周:开始加入难题
- 第4周:模拟面试训练
记住,刷题不是目的,而是手段。就像这个示例:
// 最终目标:培养算法思维
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;
if (nums[left] <= nums[mid]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
// 旋转数组搜索问题,需要灵活运用二分查找思想
评论