1. 为什么选择双指针、滑动窗口和前缀和?
在LeetCode的算法题中,双指针、滑动窗口和前缀和是解决数组/字符串类问题的三大神器。我刷了300+道Hot100题目后发现,约40%的中等难度数组题都可以用这三种思路解决。它们之所以高效,是因为都利用了数据结构的连续性特征,通过特定的指针移动或区间计算来避免暴力解法中的重复计算。
双指针特别适合处理有序数组或需要同时遍历两个相关序列的场景。比如经典的Two Sum问题,当数组有序时,双指针可以将时间复杂度从O(n²)优化到O(n)。滑动窗口则擅长解决子数组/子串的最优解问题,通过动态调整窗口边界来维护当前最优解。而前缀和通过预处理数组,使得区间和查询可以在O(1)时间内完成,这在处理频繁的区间统计时非常高效。
提示:这三种方法经常被组合使用。比如在解决"和为K的子数组"问题时,可以先用前缀和预处理,再用哈希表优化查找,这本质上也是一种双指针思想的变体。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双指针的实战应用与边界处理
2.1 同向双指针的经典场景
同向双指针(快慢指针)是处理数组去重、移动零等问题的利器。以LeetCode 26题"删除有序数组中的重复项"为例:
cpp复制int removeDuplicates(vector<int>& nums) {
if(nums.empty()) return 0;
int slow = 0;
for(int fast = 1; fast < nums.size(); ++fast){
if(nums[fast] != nums[slow]){
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
这里slow指针指向当前唯一序列的末尾,fast指针探索新元素。这种写法保证了:
- 时间复杂度O(n)
- 空间复杂度O(1)
- 保持原数组元素的相对顺序
2.2 相向双指针的巧妙用法
相向双指针常用于有序数组的两数之和、三数之和等问题。以LeetCode 15题"三数之和"为例,核心代码结构:
cpp复制vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
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){
res.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 res;
}
这个解法有几个关键点:
- 先排序(O(nlogn))使双指针策略可行
- 外层循环固定第一个数,内层双指针找另外两个数
- 通过比较相邻元素来跳过重复解
注意:处理重复解时,一定要在找到有效解后再跳过重复元素,否则可能漏解。这是很多初学者容易出错的地方。
3. 滑动窗口的模板与变种
3.1 固定大小窗口的通用解法
固定窗口大小的问题相对简单,如LeetCode 643题"子数组最大平均数I"。这类问题的通用模板:
cpp复制double findMaxAverage(vector<int>& nums, int k) {
double sum = 0;
for(int i = 0; i < k; i++) sum += nums[i]; // 初始窗口
double maxSum = sum;
for(int i = k; i < nums.size(); i++){
sum += nums[i] - nums[i-k]; // 滑动窗口
maxSum = max(maxSum, sum);
}
return maxSum / k;
}
这个模板的关键在于:
- 先计算初始窗口的和
- 每次滑动时,加上新元素,减去离开窗口的元素
- 维护一个最大值变量
3.2 可变窗口的复杂情况处理
可变窗口大小的问题更具挑战性,如LeetCode 3题"无重复字符的最长子串"。这类问题的解法通常需要哈希表辅助:
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;
}
这个解法有几个精妙之处:
- 使用哈希表记录字符最后出现的位置
- 只有当重复字符在当前窗口内时才移动左边界
- 每次更新最大长度时,窗口一定是不含重复字符的
我在实际刷题中发现,这类问题最容易出错的是边界条件的处理。比如当字符串为空时,或者所有字符都相同时,要确保算法仍然正确。
4. 前缀和的原理与高阶应用
4.1 一维前缀和的基础实现
前缀和的核心思想是通过预处理,将区间和查询从O(n)优化到O(1)。以LeetCode 303题"区域和检索-数组不可变"为例:
cpp复制class NumArray {
private:
vector<int> prefix;
public:
NumArray(vector<int>& nums) {
prefix.resize(nums.size() + 1);
prefix[0] = 0;
for(int i = 0; i < nums.size(); i++){
prefix[i+1] = prefix[i] + nums[i];
}
}
int sumRange(int left, int right) {
return prefix[right+1] - prefix[left];
}
};
这个实现有几个要点:
- prefix数组比原数组长一位,prefix[0]=0
- prefix[i]表示前i个元素的和(不包括nums[i])
- 区间和计算用右边界+1的值减去左边界值
4.2 前缀和与哈希表的组合应用
更复杂的问题如LeetCode 560题"和为K的子数组",需要结合哈希表优化:
cpp复制int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> prefix_map;
prefix_map[0] = 1; // 前缀和为0出现1次
int sum = 0, count = 0;
for(int num : nums){
sum += num;
if(prefix_map.count(sum - k)){
count += prefix_map[sum - k];
}
prefix_map[sum]++;
}
return count;
}
这个解法之所以高效,是因为:
- 用哈希表记录各前缀和出现的次数
- 当前前缀和与历史前缀和的差等于k时,说明找到了符合条件的子数组
- 初始化prefix_map[0]=1处理了从数组开头开始的子数组情况
我在实际编码时发现,这类问题最容易忘记初始化prefix_map[0]=1,导致漏算从数组第一个元素开始的子数组。
5. 综合应用与性能优化
5.1 多技巧组合解决复杂问题
有些问题需要组合使用多种技巧。以LeetCode 76题"最小覆盖子串"为例:
cpp复制string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for(char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
int start = 0, len = INT_MAX;
while(right < s.size()){
char c = s[right++];
if(need.count(c)){
window[c]++;
if(window[c] == need[c]) valid++;
}
while(valid == need.size()){
if(right - left < len){
start = left;
len = right - left;
}
char d = s[left++];
if(need.count(d)){
if(window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
这个解法融合了:
- 滑动窗口控制子串范围
- 哈希表统计字符需求
- valid计数器判断窗口是否满足条件
5.2 性能优化的小技巧
在实际编码中,我发现几个可以提升性能的小技巧:
- 对于固定大小的滑动窗口,可以用数组代替哈希表,如ASCII字符集可以用int[128]来统计
- 在双指针问题中,如果数组已经有序,可以优先考虑相向双指针
- 前缀和问题中,如果数据范围有限,可以用固定大小数组代替vector
- 对于边界条件,可以先处理特殊情况(如空输入、单个元素等)提前返回
例如,在处理字符串问题时,如果知道字符集是字母,可以这样优化:
cpp复制vector<int> count(26, 0); // 代替unordered_map
for(char c : s){
count[c-'a']++; // 比哈希表查找更快
}
这些优化虽然看似微小,但在处理大规模数据或高频调用时,能显著提升性能。
