从零开始刷算法——双指针-三数之和&接雨水

双指针(Two Pointers)是算法中最常用、最高频的技巧之一,尤其适用于数组类问题。本篇文章将通过两道经典题目:

  • LeetCode 15:三数之和(3Sum)

  • LeetCode 42:接雨水(Trapping Rain Water)

来全面讲解双指针思想,并结合具体代码、详尽的时间复杂度与空间复杂度分析。


一、双指针思想是什么?

双指针就是:

使用两个索引在数组上移动,通过条件判断来减少不必要的遍历,从而优化复杂度。

常见的双指针形式:

  • 左右指针(L / R)

  • 快慢指针

  • 滑动窗口

本篇重点讲 左右指针 在算法中的应用。


二、三数之和(3Sum):排序 + 双指针的典型用法

题目要求

给定一个数组 nums,找出所有和为 0 的三元组。不能包含重复的三元组。


解题核心思想

  1. 先排序(为双指针法创造有序性)

  2. 固定一个数 nums[i]

  3. [i+1, n-1] 区间内使用双指针寻找:
    nums[i] + nums[left] + nums[right] == 0

  4. 根据 sum 大小移动左右指针:

    • sum > 0 → right--

    • sum < 0 → left++

  5. 用循环跳过重复元素,避免重复解


双指针代码实现

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 思路: 遍历数组i,针对每个i在他的后面取left 和 right
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        int n = nums.size();
        for (int i = 0; i < n - 1; ++i) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int left = i + 1;
            int right = n - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    ans.push_back({nums[i], nums[left], nums[right]});
                    left++;
                    while (left < right && nums[left] == nums[left - 1]){
                        left++;
                    }
                    right--;
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                }
                else if (sum > 0) right--;
                else left++;
            }
        }
        return ans;
    }

};

时间复杂度分析

1. 排序:

cpp 复制代码
O(n log n)

2. 遍历 + 双指针:

外层循环 n

内层双指针最多跑 (n-i)

整体为:

cpp 复制代码
O(n^2)

最终时间复杂度:

O(n²)


空间复杂度分析

  • 排序在 C++ 中是就地排序 → O(1)

  • 输出答案需要空间,但不算在算法额外空间里

空间复杂度:O(1)(不计结果集)


三、接雨水:前缀最大值 + 后缀最大值(双指针思想衍生)

虽然接雨水有 O(1) 双指针最优解,但你的代码是 前缀 + 后缀数组法,属于"预处理 + 双指针思想"的经典版本。


题目核心

每个位置的储水量 = 左侧最大值与右侧最大值的较小者 -- 当前高度

前缀最大值 + 后缀最大值解法

1. 预处理两个数组:

  • pre_max[i]:从左到 i 的最高柱子

  • suf_max[i]:从右到 i 的最高柱子

2. 再遍历一次计算水量:

cpp 复制代码
water += min(pre_max[i], suf_max[i]) - height[i]

原代码的问题(已修复)

你写的:

cpp 复制代码
suf_max[i] = max(height[i], suf_max[i - 1]);

应该看右边,所以应该是:

cpp 复制代码
suf_max[i] = max(height[i], suf_max[i + 1]);

正确代码

cpp 复制代码
class Solution {
public:
    int trap(vector<int>& height) {
        // 思路: 把前面的最大值和后面的最大值先求出来,再最后遍历一次得到ans
        int ans = 0;
        int n = height.size();
        vector<int> pre_max(n);
        vector<int> suf_max(n);
        pre_max[0] = height[0];
        suf_max[n - 1] = height[n - 1];

        for (int i = 1; i < n; ++i) {
            pre_max[i] = max(height[i], pre_max[i - 1]); 
        }
        for (int i = n - 2; i >= 0; --i) {
            suf_max[i] = max(height[i], suf_max[i + 1]);
        }
        for (int i = 0; i < n; ++i) {
            ans += min(suf_max[i], pre_max[i]) - height[i];
        }
        return ans;
    }
};

时间复杂度分析

预处理前缀最大值:O(n)

预处理后缀最大值:O(n)

计算水量:O(n)

总时间复杂度:O(n)

使用两次遍历和一次合并遍历,总共是线性时间。


空间复杂度分析

使用了两个长度为 n 的数组:

cpp 复制代码
pre_max[n]
suf_max[n]

因此:

空间复杂度:O(n)

(可用双指针优化到 O(1),但不是本文重点)


四、两题的双指针思想对比总结

题目 双指针方式 时间复杂度 空间复杂度 核心思想
三数之和 排序 + 左右夹逼 O(n²) O(1) 有序数组中根据 sum 调整指针
接雨水 前后缀数组(双指针思想) O(n) O(n) 左右区间最大值决定当前位置水量

五、双指针常用场景总结

场景 典型题目 指针移动策略
在排序数组里查找目标值 三数之和 left / right 根据 sum 调节
区间最大最小问题 接雨水、盛水最多的容器 哪侧更小,就移动哪侧
去重 删除排序数组重复项 快慢指针
滑动窗口 长度最短子数组 两指针都向右

结语

双指针不是一个具体的算法,而是一类极其重要的思维模式:

通过左右夹逼、滑动、区间缩小等方式降低复杂度,避免暴力枚举。

掌握好这类模式,你可以轻松解决大量中等难度甚至困难题。

相关推荐
无限进步_1 小时前
C语言数组元素删除算法详解:从基础实现到性能优化
c语言·开发语言·windows·git·算法·github·visual studio
松涛和鸣1 小时前
16、C 语言高级指针与结构体
linux·c语言·开发语言·数据结构·git·算法
Booksort2 小时前
【LeetCode】算法技巧专题(持续更新)
算法·leetcode·职场和发展
OJAC1112 小时前
2026高校毕业生1270万!但这些学生却被名企用高薪“提前预定”!
算法
Controller-Inversion2 小时前
岛屿问题(dfs典型问题求解)
java·算法·深度优先
小白程序员成长日记2 小时前
力扣每日一题 2025.11.28
算法·leetcode·职场和发展
Swift社区2 小时前
LeetCode 435 - 无重叠区间
算法·leetcode·职场和发展
sin_hielo2 小时前
leetcode 1018
算法·leetcode
大工mike2 小时前
代码随想录算法训练营第三十一天 | 1049. 最后一块石头的重量 II 494. 目标和 474.一和零
算法