markdown复制## 1. 算法刷题的核心价值与学习路径
算法能力是程序员技术实力的试金石。无论是应对大厂技术面试,还是解决实际工程中的性能优化问题,扎实的算法功底都能让你在关键时刻脱颖而出。但很多初学者在刷题过程中常陷入两个典型困境:
- **盲目刷题陷阱**:每天刷3-5道题,但遇到新题仍然毫无思路
- **卡题恐惧症**:稍微复杂的题目思考超过20分钟就忍不住看答案
这些问题的本质在于缺乏「场景化思维」和「系统性训练」。我在过去两年辅导过200+名学员的算法提升,总结出最高效的进阶路径:
1. **建立算法知识体系**:将常见问题归类到7大核心场景(数组/字符串、链表、哈希表/栈/队列、二叉树、二分查找、贪心算法、动态规划)
2. **掌握解题模板**:每个场景都有标准解法框架,如双指针的「快慢指针」「左右指针」模式
3. **刻意练习高频题**:优先攻克各场景的经典题目,再扩展到变种题
> 以数组场景为例:当遇到「有序数组」相关问题时,首先应该考虑二分查找(时间复杂度从O(n)降到O(logn));当遇到「子数组求和」问题时,前缀和技巧能优化到O(1)查询
## 2. 数组/字符串场景精讲
### 2.1 双指针技术体系
双指针是处理数组问题的瑞士军刀,主要分为两种模式:
#### 2.1.1 快慢指针(同向移动)
典型应用场景:原地修改数组(删除元素、去重等)
```java
// LeetCode 27.移除元素标准解法
public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
return slow;
}
易错点警示:
- 边界条件处理:空数组直接返回0
- slow指针的移动时机:只有元素保留时才需要移动
- 返回值理解:slow最终指向的是「新数组」的下一个位置,其值正好等于新长度
2.1.2 左右指针(相向移动)
典型应用场景:有序数组的两数之和、反转数组等
java复制// LeetCode 167.两数之和II
public int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return new int[]{left + 1, right + 1}; // 题目要求下标从1开始
} else if (sum < target) {
left++; // 需要更大的数
} else {
right--; // 需要更小的数
}
}
return new int[]{-1, -1};
}
实战技巧:
- 有序数组的特性利用:通过比较sum与target的大小,可以智能调整指针
- 下标转换:注意题目要求的输出格式(本题下标从1开始)
2.2 前缀和与差分数组
2.2.1 前缀和模板
解决频繁查询区间和的场景,将查询时间复杂度从O(n)降到O(1)
java复制class NumArray {
private int[] preSum;
public NumArray(int[] nums) {
preSum = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
关键点:
- 前缀和数组长度比原数组多1(preSum[0]=0)
- 区间和公式:sumRange(l,r) = preSum[r+1] - preSum[l]
2.2.2 差分数组模板
高效处理区间更新操作(如航班预订问题)
java复制public int[] corpFlightBookings(int[][] bookings, int n) {
int[] diff = new int[n + 2]; // 防越界
for (int[] b : bookings) {
diff[b[0]] += b[2];
diff[b[1] + 1] -= b[2];
}
int[] res = new int[n];
res[0] = diff[1];
for (int i = 1; i < n; i++) {
res[i] = res[i - 1] + diff[i + 1];
}
return res;
}
算法理解:
- 差分数组diff[i] = nums[i] - nums[i-1]
- 对区间[l,r]增加val ⇨ diff[l] += val, diff[r+1] -= val
- 通过求前缀和还原最终结果
3. 链表操作精要
3.1 虚拟头节点技巧
处理链表删除问题时,虚拟头节点能统一头节点和非头节点的操作逻辑
java复制public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode prev = dummy;
while (prev.next != null) {
if (prev.next.val == val) {
prev.next = prev.next.next;
} else {
prev = prev.next;
}
}
return dummy.next;
}
注意事项:
- 循环条件用prev.next判断而非prev,方便执行删除操作
- 返回dummy.next而不是head,因为head可能已被删除
3.2 快慢指针高级应用
3.2.1 链表中点问题
java复制public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
变种训练:
- 如果要求返回第一个中点(偶数长度时):调整fast初始位置
- 链表的1/3、1/4节点:调整快指针步长
3.2.2 环形链表检测
java复制public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
}
数学原理:
设环前长度a,环内相遇点距环入口b,环剩余长度c
快慢指针相遇时:2(a+b) = a+b+k(b+c) ⇒ a = (k-1)(b+c)+c
即:从相遇点和头节点同时出发,必在环入口相遇
4. 二分查找的工程实践
4.1 标准二分模板
java复制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;
}
重要细节:
- mid计算使用left + (right - left)/2防止溢出
- 循环条件用<=而不是<,确保能处理区间只有一个元素的情况
- 边界更新要+1/-1,避免死循环
4.2 边界查找变种
java复制// 查找左边界
private int findLeftBound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
if (nums[mid] == target) res = mid;
} else {
left = mid + 1;
}
}
return res;
}
面试高频考点:
- 如何处理重复元素
- 未找到时的返回值处理
- 搜索区间开闭的选择
5. 动态规划的系统训练
5.1 经典背包问题
5.1.1 01背包模板
java复制public boolean canPartition(int[] nums) {
int sum = Arrays.stream(nums).sum();
if (sum % 2 != 0) return false;
int target = sum / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
关键点:
- 倒序遍历容量:防止重复选择
- 状态转移方程:dp[j] = dp[j] || dp[j - num]
- 空间优化:从二维降为一维
5.2 股票买卖问题
5.2.1 通用解法框架
java复制// 每天有三种选择:买入、卖出、休息
// dp[i][k][0/1]:第i天,最多k次交易,0未持有/1持有
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[n-1][0];
}
不同变种:
- 只能交易一次:k=1
- 无限次交易:k=+∞
- 含冷冻期:卖出后需要休息一天
- 含手续费:每次卖出扣除手续费
6. 刷题计划与面试策略
6.1 8周进阶计划
| 周数 | 重点场景 | 每日题量 | 核心目标 |
|---|---|---|---|
| 1-2 | 数组/字符串 | 3-5 | 掌握双指针、前缀和等基础技巧 |
| 3 | 链表 | 2-3 | 熟练虚拟头节点、快慢指针 |
| 4 | 哈希表/栈/队列 | 3-4 | 理解单调栈、滑动窗口 |
| 5 | 二叉树 | 3 | 递归思维训练 |
| 6 | 二分查找 | 2-3 | 边界条件处理 |
| 7 | 贪心算法 | 2 | 证明局部最优的正确性 |
| 8 | 动态规划 | 1-2 | 状态转移方程推导 |
6.2 面试解题框架
-
问题澄清(2分钟):
- 确认输入输出格式
- 询问边界条件和特殊场景
- 举例验证理解是否正确
-
暴力解法(5分钟):
- 先给出最直观的解法
- 分析时间/空间复杂度
- 寻找优化点
-
优化思路(8分钟):
- 识别问题场景(数组?链表?树?)
- 匹配已知算法模式(双指针?DP?)
- 画图辅助分析
-
代码实现(5分钟):
- 模块化编写(先写框架再补细节)
- 添加关键注释
- 实时验证样例
-
测试验证(3分钟):
- 常规用例
- 边界用例(空输入、极值等)
- 错误用例检查
个人经验:在真实面试中,面试官更关注你的思考过程而非完美答案。遇到难题时,可以坦率地说:"这道题我之前没见过,让我尝试这样分析..." 展示解题思路比直接给出正确答案更重要
code复制