LeetCode Hot 100 · 双指针 3 题 Java

覆盖 11 盛最多水的容器、15 三数之和、42 接雨水,主线是排序 / 双指针 / 前缀最值 与「短板效应」。

每题统一为:① 题目分析与解题思路 (含出错点)→ ② 常见解法及讲解 → ③ 知识点总结。


11. 盛最多水的容器

① 题目分析与解题思路

  • 核心公式 :容量 = 两线间距(宽度)× 两线中较短的高度(短板决定高度),即 area=(j−i)×min⁡(heighti,heightj)area = (j-i) \times \min(heighti, heightj)area=(j−i)×min(heighti,heightj)。
  • 目标 :选两个下标 (i,j)(i,j)(i,j) 使容量最大;限制 :n≤105n \le 10^5n≤105。
  • 暴力法 :枚举所有两两组合算面积,O(n2)O(n^2)O(n2)。
  • 双指针法(最优) :左指针最左、右指针最右(初始宽度最大),每轮移动更矮的那侧 ------移动高的一侧宽度必减、高度上限不变,面积只会更小。直至两指针相遇,O(n)O(n)O(n)。
常见出错点
  1. 误以为移动高的指针能得更大面积(双指针核心理解错误)。
  2. 面积写成 max⁡(heighti,heightj)\max(heighti, heightj)max(heighti,heightj),把长板当高度(错!是短板)。
  3. 宽度算成 jjj,忘记减去 iii。
  4. 暴力解法不看数据范围,直接超时(就是你现在遇到的)。

② 常见解法及讲解(Java)

解法 1:暴力枚举(仅适合小数据,不推荐)
java 复制代码
class Solution {
    public int maxArea(int[] height) {
        int max = 0;
        int n = height.length;
        for(int i = 0; i < n; i++){
            for(int j = i + 1; j < n; j++){
                int area = (j - i) * Math.min(height[i], height[j]);
                max = Math.max(max, area);
            }
        }
        return max;
    }
}

讲解:两层循环枚举所有柱子组合,算面积、记录最大值。时间 O(n2)O(n^2)O(n2)、空间 O(1)O(1)O(1),n 大时超时,只能用来理解题意。

  • ✅ 计算逻辑正确,小用例能出正确答案。
  • ❌ 致命问题:双重循环 O(n2)O(n^2)O(n2) ,n=105n=10^5n=105 时接近 50 亿次,直接超时;另外 maxHeight、maxlength、maxij 这类中间变量不必单独定义,一行算出即可。
解法 2:双指针(Hot 100 标准最优解,推荐背诵)
java 复制代码
class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int maxArea = 0;
        while (left < right) {
            int width = right - left;
            int h = Math.min(height[left], height[right]);
            int area = width * h;
            maxArea = Math.max(maxArea, area);

            // 移动短板
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return maxArea;
    }
}

讲解:

  1. left=0、right=len-1,此时宽度最大。
  2. 每轮算围成的面积并更新最大值;哪边更矮,就把矮的那侧指针往里移。
  3. 相遇即结束。时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)。

示例推演 :[1,8,6,2,5,4,8,3,7],初始 8×1=8;左边短 left++ → 7×7=49;右边短 right--;如此反复,一遍扫完得 49。

③ 知识点总结

  1. 容量 = 宽度 × 较短高度,「短板效应」是全部推导的起点。
  2. 每轮必须移动短板 ,一次移动等于排除一整批不可能更优的解 ,这是 O(n2)→O(n)O(n^2)\to O(n)O(n2)→O(n) 的关键;循环条件 left < right,width = right - left 别忘减 left。
  3. 面试先讲暴力,再讲双指针与正确性论证。

15. 三数之和

① 题目分析与解题思路

题目要求 :找出所有三个不同下标元素、和为 0 的组合,返回的三元组不能重复 ;三元组内顺序、答案顺序均不限,难点是去重。

核心思路 :三重循环 O(n3)O(n^3)O(n3) 会超时,优化为排序 + 固定第一个数 + 双指针找另两个数。

  1. 先排序:既方便双指针移动,也方便跳重;再固定 nums[i],目标变成在右侧找两数之和等于 -nums[i]。
  2. 令 left = i+1、right = n-1:和偏大 right--,偏小 left++,相等就记录三元组、两侧同时跳过重复值再收缩。
  3. 去重两处:nums[i] == nums[i-1] 跳过;命中后 left 右移、right 左移,直到与刚才的值不同。

复杂度 :排序 O(nlog⁡n)O(n\log n)O(nlogn) + 外层循环与双指针 O(n2)O(n^2)O(n2),整体 O(n2)O(n^2)O(n2),可通过 n=3000。

常见出错点
  1. ❌ 忘记排序,直接双指针:双指针的前提是有序数组。
  2. ❌ 去重写错:只对 i 去重、命中后 left/right 不跳重,会出重复三元组;i 去重写成 nums[i] == nums[i+1],逻辑颠倒。
  3. ❌ 指针范围错:left 从 0 开始而非 i+1,i 与 left 下标会相同。
  4. ❌ 命中后只移动一个指针,陷入死循环。
  5. ❌ 边界:[0,0,0] 全 0 样例被错误跳过,得不到正确答案。
  6. ❌ 暴力三重循环:O(n3)O(n^3)O(n3),n=3000 直接超时。

② 常见解法及讲解(Java)

解法 1:排序 + 双指针(推荐,Hot 100 标准解法)
java 复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        // 列表里元素是列表
        List<List<Integer>> res = new ArrayList<>();
        // Arrays.sort 默认按升序排序
        Arrays.sort(nums);
        int n = nums.length;

        for (int i = 0; i < n; i++) {
            // 第一个数已经大于0,后面全部更大,不可能凑出和为0,直接break 
            if (nums[i] > 0) {
                break;
            }
            // i去重:和上一个一样,跳过
            // 若用 nums[i] 和 nums[i - 1] 则可能会越界
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            int left = i + 1;
            int right = n - 1;
            int target = -nums[i];
            int sum = 0;

            // 双指针每轮只做 O(1) 操作,整个 while 是 O(n)
            while (left < right) {
                sum = nums[left] + nums[right];
                if (sum < target) { // sum和小于目标数,和要变大向右移
                    left++;
                } else if (sum > target) { // sum和大于目标数,数要变小向左移
                    right--;
                } else { // 找到一组答案,添加进列表
                    res.add(Arrays.asList(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--;
                    }
                    // nums[i], nums[left], nums[right]三者有两个确定,另一个也确定了
                    // 收缩
                    left++;
                    right--;
                }
            }

        }
        return res;
    }
}

讲解:

  1. Arrays.sort(nums) 是整个算法的基础。
  2. nums[i] > 0 时数组升序、后面全 ≥ 它,三正数不可能和为 0,直接 break 剪枝。
  3. i > 0 && nums[i] == nums[i-1] 跳过重复首数(i > 0 同时防越界);命中后先跳过相邻重复的 left、right 再收缩,两个去重 while 都要带 left < right,否则越界。
解法 2:暴力三重循环(不推荐,仅理解)
java 复制代码
// 超时写法,仅用来理解题意
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Set<List<Integer>> set = new HashSet<>();
        int n = nums.length;
        for(int i=0;i<n;i++){
            for(int j=i+1;j<n;j++){
                for(int k=j+1;k<n;k++){
                    if(nums[i]+nums[j]+nums[k]==0){
                        List<Integer> temp = Arrays.asList(nums[i],nums[j],nums[k]);
                        Collections.sort(temp);
                        set.add(temp);
                    }
                }
            }
        }
        return new ArrayList<>(set);
    }
}

讲解:三层循环枚举所有三元组,用 Set 自动去重;O(n3)O(n^3)O(n3),n=3000 时极慢,无法通过。

解法 3:哈希表(了解即可,去重麻烦)

思路:固定 i,剩下两数之和用哈希表做两数之和;去重非常麻烦,面试优先写双指针版本。

附:Arrays.asList 与「为什么双指针不用 Set」

核心 :Arrays.asList(...) 把几个值包装成固定大小的 List------不能 add/remove(底层是数组包装),但可入 Set、可直接返回。

第一组:暴力写法里这三行

java 复制代码
List<Integer> temp = Arrays.asList(nums[i],nums[j],nums[k]);
Collections.sort(temp);
set.add(temp);
  1. 生成时按 i、j、k 的遍历顺序(如 [-1,2,-1]),顺序不固定,同一三元组会以 [-1,2,-1]、[-1,-1,2] 两种形态出现,在 Set 眼里是两个 List。
  2. Collections.sort(temp) 排完变 [-1,-1,2],✅ 统一内部顺序 ;set.add(temp) 靠元素不重复去重。即「先生成、再过滤」。

第二组:双指针写法里

java 复制代码
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
  1. nums 已全局排序且 i < left < right,天然满足 nums[i] ≤ nums[left] ≤ nums[right](如 [-4,-1,-1,0,1,2] 取 i=1、left=2、right=5 得 [-1,-1,2])。
  2. 故既不用 Collections.sort 也不用 Set:三处跳重让重复解根本不会生成,直接 add 进 res,属于「先过滤、再生成」。

对比总结

方式 前置条件 排序作用 去重方式 需要 Set
暴力三重循环 数组无序,i/j/k 随机遍历 统一三元组内部顺序 生成后靠 Set 剔除 ✅
排序 + 双指针 nums 已全局排序 + 指针跳重 无需再排 循环里提前跳过重复 ❌

高频坑 :Arrays.asList() 返回 java.util.Arrays.ArrayList,可 get、可入 Set、可直接返回,但 ❌ 不能 temp.add() / temp.remove()。要可变 List 用:

plain 复制代码
new ArrayList<>(Arrays.asList(a,b,c))

延伸(面试常问):能否用 Set 写双指针版?可以但不推荐------多一份空间开销,手动去重就是标准答案。

③ 知识点总结

  1. 三数之和 = 排序 + 固定一个数 + 双指针求两数之和,O(n2)O(n^2)O(n2)。
  2. 排序干三件事:双指针可移动、nums[i] > 0 可剪枝、去重只需比相邻值。
  3. 去重三处:i 与 i-1(i > 0 防越界)、命中后 left / right 跳重,且先跳重再收缩;源头去重就不需要 Set,暴力法只能事后补救。
  4. Arrays.asList 返回定长 List,可读不可增删;可变要用 new ArrayList<>(...)。

42. 接雨水

① 题目分析与解题思路

核心结论:下标 i 处能接的雨水 = min(左侧最高, 右侧最高) − 当前高度;结果 ≤ 0 表示接不到,取 0;总雨水为各位置累加。

wateri=max⁡(0, min⁡(leftMaxi,rightMaxi)−heighti)wateri = \max(0,\ \min(leftMaxi, rightMaxi) - heighti)wateri=max(0, min(leftMaxi,rightMaxi)−heighti)

  • leftMax[i]:i 左侧所有柱子的最大值;rightMax[i]:i 右侧所有柱子的最大值。

思路路线 :① 暴力------每个 i 向左右扫描;② 预处理数组------正向存 leftMax、反向存 rightMax 再累加(面试优先能写出来);③ 双指针------维护左右最大值,一趟完成,空间 O(1)O(1)O(1)。

常见出错点
  1. ❌ 理解错公式:写成 max⁡(leftMax,rightMax)−heighti\max(leftMax, rightMax) - heightimax(leftMax,rightMax)−heighti,应取左右最大值里更小的那个(短板)。
  2. ❌ 忘记和 0 比较:min(left,right) - height[i] 可能是负数,要取 0。
  3. ❌ 边界:最左、最右的柱子接不到雨水。
  4. ❌ 预处理数组初始化错:leftMax[0] = height[0]、rightMax[n-1] = height[n-1]。
  5. ❌ 双指针逻辑搞反:何时移动左 / 右判断错误。
  6. ❌ 累加时忽略每根柱子宽度是 1。

② 常见解法及讲解(Java)

解法 1:暴力法(理解原理,会超时,不推荐)
java 复制代码
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int res = 0;
        for (int i = 1; i < n - 1; i++) {
            int leftMax = 0;
            int rightMax = 0;

            // 找左侧最高
            for (int l = 0; l < i; l++) {
                leftMax = Math.max(leftMax, height[l]);
            }
            // 找右侧最高
            for (int r = i + 1; r < n; r++) {
                rightMax = Math.max(rightMax, height[r]);
            }
            // 累加
            int cur = Math.min(leftMax, rightMax) - height[i];
            if (cur > 0) {
                res += cur;
            }
        }
        return res;
    }
}

讲解:每个位置单独左右扫描。时间 O(n2)O(n^2)O(n2)、空间 O(1)O(1)O(1);n 到 2e4 时超时,仅用于理解公式。

解法 2:预处理左右最大值数组(DP,推荐先掌握)
java 复制代码
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        // 数量 < 3 结果为0
        if (n <= 2) {
            return 0;
        }
        int res = 0;
        // 预处理左右最大值数组
        int[] leftMax = new int[n];
        int[] rightMax = new int[n];

        leftMax[0] = height[0];
        // 从前往后,记录各自左侧最大值
        for (int i = 1; i < n; i++) {
            // 取上一个左侧最大值 和 当前值 中的较大值
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        rightMax[n - 1] = height[n - 1];
        // 从后往前,记录各自右侧最大值
        for (int j = n - 2; j >= 0; j--) {
            // 取 上一个右侧最大值 和 当前值 中的较大值
            rightMax[j] = Math.max(rightMax[j + 1], height[j]);
        }

        for (int k = 0; k < n; k++) {
            // 对于边界:左边界:leftMax==height,右边界:rightMax==height,water都为<=0
            int water = Math.min(leftMax[k], rightMax[k]) - height[k];
            if (water > 0) {
                res += water;
            }
        }
        return res;
    }
}

讲解:leftMax 从左往右递推 max(上一个左最大值, 当前高度),rightMax 反向同理,最后套公式累加。时间 O(n)O(n)O(n)、空间 O(n)O(n)O(n)。

解法 3:双指针(最优,Hot 100 标准答案,空间 O(1)O(1)O(1))
java 复制代码
class Solution {
    public int trap(int[] height) {
        // 左指针,初始在数组最左侧
        int left = 0;
        // 右指针,初始在数组最右侧
        int right = height.length - 1;
        // 记录left指针走过区域的最大高度(左边最高柱子)
        int leftMax = 0;
        // 记录right指针走过区域的最大高度(右边最高柱子)
        int rightMax = 0;
        // 累加雨水总量
        int res = 0;

        // 左右指针相遇时停止循环
        while (left < right) {
            // 当前左柱子高度 < 右柱子高度:左边是短板
            // 含义:对于left位置,能装多少水只由leftMax决定,右侧一定存在一个更高的柱子托住水
            if (height[left] < height[right]) {
                // 如果当前柱子高度 >= 左侧历史最高,更新左侧最高,这个位置不能存水
                if (height[left] >= leftMax) {
                    leftMax = height[left];
                } else {
                    // 当前柱子比左侧最高矮,可以存水:水量 = 左侧最高 - 当前柱子高度
                    res += leftMax - height[left];
                }
                // 左指针向右移动
                left++;
            } else {
                // 当前右柱子高度 <= 左柱子高度:右边是短板
                // 含义:对于right位置,能装多少水只由rightMax决定,左侧一定存在更高的柱子托住水
                if (height[right] >= rightMax) {
                    // 更新右侧最高,该位置不能存水
                    rightMax = height[right];
                } else {
                    // 可以存水,水量 = 右侧最高 - 当前柱子高度
                    res += rightMax - height[right];
                }
                // 右指针向左移动
                right--;
            }
        }
        // 返回总共接住的雨水
        return res;
    }
}

讲解:

  • height[left] < height[right] 时左边是短板,left 处积水只由 leftMax 决定 (右侧必有更高的柱子托住水);反之处理 right、右指针左移,同步更新 leftMax / rightMax。
  • 核心原理一句话:谁矮谁就是短板,短板一侧的积水上限由自己这侧的历史最大值决定。
  • 时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)。
补充:单调栈解法(了解)

思路:栈存下标,栈内高度单调递减;遇到更高柱子弹栈顶算凹槽雨水。复杂度同为 O(n)O(n)O(n),稍难写,一般前两种优先掌握。

③ 知识点总结

  1. 单点公式 min(左最高, 右最高) - 当前高度(负数取 0)------短板效应,与 11 题同源。
  2. 三种解法是同一公式的不同落地:暴力(现算)→ 预处理(空间换时间)→ 双指针(变量替代数组);单调栈则是横向视角,同为 O(n)O(n)O(n)。
  3. 双指针正确性:height[left] < height[right] 时,left 右侧最高 ≥ height[right],故只受 leftMax 约束。
  4. 边界:首尾接不到水,n <= 2 返回 0;柱子宽按 1 计。
相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 圈子互动
java·开发语言·交友
Rain的Java大神之路1 小时前
北漂 13 年,我把 Java 家底钉成了这张知识图谱(全站免费)
java·后端·程序员
深圳老胡1 小时前
FDC2212 + STM32G030 液面探测:IIC 读数跳动导致下探深度不一致的排查与解决
笔记·stm32·单片机·代码规范·1024程序员节
程序边界1 小时前
一台数据集成软件的拆解笔记(上):把 KFS 装起来、跑起来,再挑一堆毛病
笔记·原型模式
叼烟扛炮1 小时前
Linux 学习笔记(二十九):进程信号(上)—— 信号捕捉与异步魔法
linux·运维·笔记·学习·1024程序员节
wuminyu1 小时前
JVM规范学习总结-Class文件结构
java·linux·c语言·jvm·c++
七夜zippoe2 小时前
Spring AI + Ollama 本地模型:数据不出内网的合规方案(落地全流程)
java·人工智能·spring·ai·ollama
曾庆睿2 小时前
计算机图形学入门笔记_GAMES101_Lecture01-02
笔记·计算机图形学
coolwest2 小时前
某游戏辅助完整破解复盘报告(仅用于本人学习记录)
笔记