1. 算法刷题实战:从二分查找到动态规划
最近在刷算法题的过程中,我系统性地整理了8道经典题目,涵盖了二分查找、双指针、滑动窗口、动态规划等多个重要算法。这些题目都来自"代码随想录"的刷题计划,对于提升算法思维和编程能力很有帮助。下面我将详细解析每道题的解题思路和实现细节,并分享一些在实际编码中遇到的坑和解决技巧。
2. 二分查找基础:力扣704题
2.1 题目理解与思路分析
二分查找是算法学习中最基础也是最重要的算法之一。题目给定一个升序排列的整数数组和一个目标值,要求我们实现一个时间复杂度为O(log n)的搜索算法。
核心要点:
- 数组必须是有序的(题目已保证)
- 每次比较都能将搜索范围减半
- 边界条件的处理是关键
2.2 实现细节与注意事项
cpp复制class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
};
注意事项:
- 循环条件是
left <= right而不是left < right,因为当left等于right时,仍需要检查该位置 - 计算mid时使用
left + (right - left)/2而不是(left + right)/2,可以避免整数溢出 - 每次调整边界时,mid位置已经被检查过,所以是
mid ± 1
3. 双指针技巧:力扣27题(移除元素)
3.1 双指针解法
这道题要求原地移除数组中所有等于给定值的元素,并返回新数组的长度。双指针法是最高效的解决方案。
cpp复制class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
return slow;
}
};
关键点:
- 快指针
fast遍历整个数组 - 慢指针
slow指向下一个有效元素应该存放的位置 - 时间复杂度O(n),空间复杂度O(1)
3.2 二分查找变种解法
虽然双指针是最优解,但也可以使用排序+二分查找的方法:
cpp复制class Solution {
public:
int removeElement(vector<int>& nums, int val) {
sort(nums.begin(), nums.end());
auto first = lower_bound(nums.begin(), nums.end(), val);
auto last = upper_bound(nums.begin(), nums.end(), val);
int cnt = nums.size() - (last - first);
copy(last, nums.end(), first);
return cnt;
}
};
这种方法虽然代码简洁,但时间复杂度为O(n log n),不如双指针高效。在实际面试中,应该优先考虑双指针解法。
4. 滑动窗口应用:力扣209题(长度最小的子数组)
4.1 滑动窗口原理
滑动窗口是处理子数组/子字符串问题的强大技巧。这道题要求找到和≥target的长度最小的连续子数组。
cpp复制class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, sum = 0;
int minLen = INT_MAX;
for (int right = 0; right < nums.size(); right++) {
sum += nums[right];
while (sum >= target) {
minLen = min(minLen, right - left + 1);
sum -= nums[left++];
}
}
return minLen == INT_MAX ? 0 : minLen;
}
};
4.2 关键点解析
- 窗口扩张:右指针移动,扩大窗口
- 窗口收缩:当sum≥target时,左指针移动,缩小窗口
- 更新结果:每次满足条件时记录当前窗口大小
- 边界情况:整个数组和仍小于target时返回0
5. 动态规划专题:打家劫舍系列
5.1 基础版(力扣198题)
经典的动态规划问题,不能偷相邻的房屋,求最大收益。
cpp复制class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) return nums[0];
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i-2] + nums[i], dp[i-1]);
}
return dp.back();
}
};
状态转移方程:
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
5.2 环形版(力扣213题)
房屋排成环形,首尾相连,不能同时偷首尾。
cpp复制class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) return nums[0];
return max(robRange(nums, 0, nums.size()-2),
robRange(nums, 1, nums.size()-1));
}
int robRange(vector<int>& nums, int start, int end) {
int prev = 0, curr = 0;
for (int i = start; i <= end; i++) {
int temp = max(prev + nums[i], curr);
prev = curr;
curr = temp;
}
return curr;
}
};
关键点:
- 分解为两个子问题:不偷第一个房屋或不偷最后一个房屋
- 取两个子问题的最大值
- 空间优化:使用两个变量代替dp数组
6. 矩阵操作:力扣59题(螺旋矩阵)
6.1 方向控制法
生成n×n的螺旋矩阵,数字从1到n²按顺时针螺旋排列。
cpp复制class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> matrix(n, vector<int>(n));
int dirs[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
int row = 0, col = 0, dir = 0;
for (int num = 1; num <= n*n; num++) {
matrix[row][col] = num;
int nextRow = row + dirs[dir][0];
int nextCol = col + dirs[dir][1];
if (nextRow < 0 || nextRow >= n || nextCol < 0 || nextCol >= n || matrix[nextRow][nextCol] != 0) {
dir = (dir + 1) % 4;
nextRow = row + dirs[dir][0];
nextCol = col + dirs[dir][1];
}
row = nextRow;
col = nextCol;
}
return matrix;
}
};
6.2 分层处理法
另一种思路是按层处理,从外向内逐层填充:
cpp复制class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> matrix(n, vector<int>(n));
int num = 1;
int left = 0, right = n-1, top = 0, bottom = n-1;
while (left <= right && top <= bottom) {
for (int col = left; col <= right; col++) {
matrix[top][col] = num++;
}
for (int row = top+1; row <= bottom; row++) {
matrix[row][right] = num++;
}
if (left < right && top < bottom) {
for (int col = right-1; col > left; col--) {
matrix[bottom][col] = num++;
}
for (int row = bottom; row > top; row--) {
matrix[row][left] = num++;
}
}
left++;
right--;
top++;
bottom--;
}
return matrix;
}
};
7. 前缀和应用:卡码网58题(区间和)
7.1 前缀和原理
前缀和是处理区间求和问题的高效方法,可以O(1)时间计算任意区间和。
cpp复制int main() {
int n;
cin >> n;
vector<int> prefix(n+1);
for (int i = 1; i <= n; i++) {
int num;
cin >> num;
prefix[i] = prefix[i-1] + num;
}
int a, b;
while (cin >> a >> b) {
cout << prefix[b+1] - prefix[a] << endl;
}
return 0;
}
7.2 实现细节
- 前缀和数组通常比原数组多一个元素(prefix[0]=0)
- 区间[a,b]的和为prefix[b+1]-prefix[a]
- 预处理时间复杂度O(n),查询时间复杂度O(1)
8. 刷题经验与技巧总结
8.1 常见错误与调试技巧
- 边界条件处理不足:如数组长度为1或2时的特殊情况
- 索引越界:特别是在处理环形数组或矩阵时
- 整数溢出:计算mid时使用(left+right)/2可能导致溢出
- 死循环:循环条件或边界更新不当导致
调试建议:
- 先处理简单测试用例
- 打印中间变量值
- 使用assert验证不变量
8.2 算法选择策略
- 查找问题:考虑二分查找(有序数组)
- 子数组/子字符串问题:考虑滑动窗口
- 最优化问题:考虑动态规划
- 原地修改数组:考虑双指针
- 区间查询:考虑前缀和或线段树
8.3 性能优化技巧
- 空间优化:用变量代替dp数组(如打家劫舍问题)
- 预处理:如前缀和数组
- 剪枝:提前终止不必要的计算
- 利用语言特性:如C++的lower_bound/upper_bound
在实际刷题过程中,我发现理解算法原理比单纯记忆代码更重要。每道题都应该先思考暴力解法,再考虑优化方向,最后选择合适的算法。同时,边界条件的处理和代码的鲁棒性也是面试中的考察重点。