1. 项目概述
作为一名C++开发者,刷LeetCode是提升算法能力的必经之路。Hot100作为LeetCode经典题库,涵盖了面试中最常出现的算法题型。这次我们聚焦数组类题目,这类问题在算法面试中出现频率高达60%以上,是每个程序员必须攻克的核心领域。
数组题看似简单,实则暗藏玄机。在实际面试中,面试官往往会从最基础的两数之和开始,逐步深入考察候选人对双指针、滑动窗口等核心技巧的掌握程度。我花了三周时间系统刷完了Hot100中的数组题目,总结出一套高效的解题方法论。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心题目解析
2.1 两数之和(哈希表解法)
两数之和作为LeetCode第一题,看似简单却蕴含着重要的算法思想。暴力解法O(n²)的时间复杂度显然不够理想,我们需要更优的解决方案。
cpp复制vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for (int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
if (hash.count(complement)) {
return {hash[complement], i};
}
hash[nums[i]] = i;
}
return {};
}
这个解法巧妙利用了哈希表的O(1)查找特性,将时间复杂度降到了O(n)。在实际编码时要注意:
- 使用unordered_map而非map,因为前者平均时间复杂度更低
- 先检查补数再插入当前元素,避免重复使用同一个元素
- 返回值的顺序要与题目要求一致
提示:在面试中,面试官可能会要求解释哈希冲突的处理方式。C++的unordered_map使用链地址法解决冲突,最坏情况下查找会退化到O(n),但实际应用中很少出现。
2.2 移动零(双指针技巧)
移动零问题展示了双指针的经典应用场景。我们需要在不使用额外空间的情况下,将所有的0移动到数组末尾。
cpp复制void moveZeroes(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); ++fast) {
if (nums[fast] != 0) {
swap(nums[slow++], nums[fast]);
}
}
}
这个解法中,slow指针指向下一个非零元素应该存放的位置,fast指针用于遍历数组。这种双指针模式在数组操作中非常常见,时间复杂度O(n),空间复杂度O(1)。
常见错误:
- 直接删除0元素再补0,这样会改变数组长度
- 使用额外数组存储结果,不符合空间复杂度要求
- 忘记处理全0数组的特殊情况
2.3 盛最多水的容器(贪心+双指针)
这道题考察对问题本质的理解。我们需要找到两条线,使得它们与x轴构成的容器能容纳最多的水。
cpp复制int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int max_area = 0;
while (left < right) {
int current = min(height[left], height[right]) * (right - left);
max_area = max(max_area, current);
if (height[left] < height[right]) {
++left;
} else {
--right;
}
}
return max_area;
}
解题关键在于理解:每次移动较短的边才有可能获得更大的面积。这个贪心策略的正确性需要数学证明,在面试中可能会被问到。
时间复杂度分析:
- 最坏情况O(n)
- 平均情况O(n)
- 最佳情况O(1)
2.4 三数之和(排序+双指针)
三数之和是两数之和的进阶版,需要找到所有不重复的三元组,使其和为0。
cpp复制vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); ++i) {
if (i > 0 && nums[i] == nums[i-1]) continue;
int left = i + 1, right = nums.size() - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left+1]) ++left;
while (left < right && nums[right] == nums[right-1]) --right;
++left; --right;
} else if (sum < 0) {
++left;
} else {
--right;
}
}
}
return result;
}
关键点:
- 先排序是必要条件,时间复杂度O(nlogn)
- 使用双指针将两数之和的时间复杂度从O(n²)降到O(n)
- 注意去重处理,避免重复解
注意:在面试中,面试官可能会要求处理大数情况下的溢出问题,需要考虑使用long long类型存储中间结果。
2.5 无重复字符的最长子串(滑动窗口)
这道题展示了滑动窗口的典型应用,需要找到不含重复字符的最长子串。
cpp复制int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left = 0, max_len = 0;
for (int right = 0; right < s.size(); ++right) {
char c = s[right];
if (window.count(c) && window[c] >= left) {
left = window[c] + 1;
}
window[c] = right;
max_len = max(max_len, right - left + 1);
}
return max_len;
}
滑动窗口的核心在于维护一个不满足条件时需要移动的左边界。这个解法的时间复杂度是O(n),空间复杂度O(min(m,n)),其中m是字符集大小。
优化点:
- 如果已知字符集较小(如ASCII),可以用数组替代哈希表
- 可以记录最长子串的起始位置而不仅仅是长度
3. 解题方法论总结
3.1 双指针技巧的四种变体
- 同向双指针(快慢指针):用于原地修改数组,如移动零
- 相向双指针:用于有序数组的查找,如两数之和
- 滑动窗口:用于子串/子数组问题,如无重复字符的最长子串
- 中心扩展:用于回文串等问题
3.2 数组问题的常见优化策略
- 排序预处理:将时间复杂度从O(n²)降到O(nlogn)
- 哈希表加速查找:将查找时间从O(n)降到O(1)
- 空间换时间:使用额外空间存储中间结果
- 数学优化:利用数学性质减少计算量
3.3 代码模板与解题步骤
对于大多数数组问题,可以遵循以下步骤:
- 分析问题,确定暴力解法的时间复杂度
- 寻找优化点,思考能否使用双指针、哈希表等技巧
- 处理边界条件(空数组、全相同元素等)
- 编写代码并测试各种特殊情况
4. 常见错误与调试技巧
4.1 数组越界问题
在双指针问题中,指针移动的条件判断不当容易导致越界。建议:
- 先写终止条件再写指针移动
- 使用size_t类型时要特别小心无符号整型的回绕问题
- 在循环体内先检查指针有效性再访问元素
4.2 重复结果处理
在组合类问题中(如三数之和),容易产生重复解。解决方法:
- 先排序再跳过相同元素
- 使用集合存储结果再转为vector
- 在递归解法中使用start参数避免重复选择
4.3 性能优化技巧
- 预先分配vector容量避免多次扩容
- 使用reserve方法为哈希表预留足够空间
- 在循环外定义变量减少构造/析构开销
- 使用emplace_back替代push_back减少拷贝
5. 进阶练习建议
完成Hot100数组题后,可以挑战以下进阶题目:
- 接雨水(多种解法对比)
- 寻找两个正序数组的中位数(二分查找应用)
- 最大子数组和(动态规划入门)
- 合并区间(实际业务场景常见)
- 旋转图像(二维数组操作)
对于每道题,建议:
- 先自己思考并实现暴力解法
- 查看题解理解最优思路
- 独立实现最优解法
- 记录解题思路和易错点
- 一周后重新做一遍检验掌握程度
在实际面试中,面试官往往会在你写出解法后继续追问:
- 时间/空间复杂度分析
- 边界条件处理
- 是否有其他解法
- 如何测试你的代码
因此平时练习时就要养成全面思考的习惯,不仅追求AC,更要追求最优解和代码质量。
