LeetCode 42. 接雨水:双指针解法深度剖析与全方法汇总

在LeetCode的hard难度题目中,「42. 接雨水」是经典的数组应用题,核心考察对"边界约束"的理解和空间复杂度优化能力。本文将从题目本质出发,先剖析双指针解法的核心逻辑并完成优化,再补充前缀/后缀数组法、单调栈法两种主流方案,全面覆盖不同复杂度需求下的实现思路,兼顾原理讲解与代码实操。

一、题目回顾:接雨水的核心逻辑

题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算下雨之后能接多少雨水。

核心原理:每个柱子能接住的雨水量,取决于它左右两侧的"最高柱子高度的较小值"与自身高度的差值------即 water[i] = min(leftMax[i], rightMax[i]) - height[i](若结果为正则累加,否则为0)。

常见解法分为三类,各有优劣,适配不同场景:

  • 暴力法(O(n²)时间,O(1)空间):遍历每个柱子,分别找左右最大值,效率过低,仅作思路参考。

  • 前缀/后缀数组法(O(n)时间,O(n)空间):提前存储左右最大值数组,逻辑直观,空间开销较大。

  • 双指针法(O(n)时间,O(1)空间):最优性能解法,动态维护左右最大值,无需额外数组存储。

  • 单调栈法(O(n)时间,O(n)空间):基于栈维护递减序列,适合理解"层叠积水"逻辑,空间开销中等。

下文先聚焦双指针解法的优化,再逐一讲解其他两种实用方法。

二、双指针解法:从可用到优雅

2.1 原代码实现与剖析

typescript 复制代码
function trap(height: number[]): number {
  const hL = height.length;
  let left = 0;
  let leftMax = height[0];
  let right = hL - 1;
  let rightMax = height[hL - 1];
  let res = 0;
  while (left < right) {
    if (height[left] <= height[right]) {
      left++;
      if (height[left] > leftMax) {
        leftMax = height[left];
      } else {
        res += leftMax - height[left];
      }
    } else {
      right--;
      if (height[right] > rightMax) {
        rightMax = height[right];
      } else {
        res += rightMax - height[right];
      }
    }
  }
  return res;
};

原代码核心逻辑正确,已达 O(n) 时间、O(1) 空间的最优性能,但存在边界处理缺失、变量命名模糊、逻辑顺序冗余等问题,需针对性优化。

2.2 优化后代码

typescript 复制代码
function trap(height: number[]): number {
  // 边界处理:数组长度小于3无法接水,直接返回0
  if (height.length < 3) return 0;

  let left = 0;
  let right = height.length - 1;
  let leftMax = 0; // 左指针左侧的最大值
  let rightMax = 0; // 右指针右侧的最大值
  let water = 0; // 总接水量

  while (left < right) {
    // 先更新当前侧的最大值(保证最大值的有效性)
    leftMax = Math.max(leftMax, height[left]);
    rightMax = Math.max(rightMax, height[right]);

    // 矮侧决定接水量,处理矮侧并移动指针
    if (leftMax <= rightMax) {
      water += leftMax - height[left];
      left++;
    } else {
      water += rightMax - height[right];
      right--;
    }
  }

  return water;
}

2.3 优化要点说明

  1. 鲁棒性提升:补充数组长度小于3的边界判断,规避空数组、短数组导致的越界错误。

  2. 可读性优化 :变量 hL 改为通用的 height.lengthres 改为语义化的 water,逻辑更清晰。

  3. 简洁性提升 :调整"更新最大值→计算水量→移动指针"的顺序,贴合核心公式,用 Math.max 替代冗余 if-else

优化后代码性能不变,且适配所有合法输入场景,更符合工程实践需求。

三、其他解法:多思路拓展

3.1 前缀/后缀数组法(直观易理解)

该方法直接对应接雨水的核心原理,通过两个辅助数组提前存储每个位置的左右最大值,再遍历数组计算总水量,逻辑直观,适合新手理解。

实现思路
  1. 构建前缀最大值数组 leftMaxArrleftMaxArr[i] 表示第 i 个柱子左侧(含自身)的最高高度。

  2. 构建后缀最大值数组 rightMaxArrrightMaxArr[i] 表示第 i 个柱子右侧(含自身)的最高高度。

  3. 遍历每个柱子,累加 min(leftMaxArr[i], rightMaxArr[i]) - height[i](结果为正才累加)。

代码实现
typescript 复制代码
function trap(height: number[]): number {
  const n = height.length;
  if (n < 3) return 0;

  // 前缀最大值数组
  const leftMaxArr = new Array(n).fill(0);
  leftMaxArr[0] = height[0];
  for (let i = 1; i < n; i++) {
    leftMaxArr[i] = Math.max(leftMaxArr[i - 1], height[i]);
  }

  // 后缀最大值数组
  const rightMaxArr = new Array(n).fill(0);
  rightMaxArr[n - 1] = height[n - 1];
  for (let i = n - 2; i >= 0; i--) {
    rightMaxArr[i] = Math.max(rightMaxArr[i + 1], height[i]);
  }

  // 计算总接水量
  let water = 0;
  for (let i = 0; i < n; i++) {
    water += Math.min(leftMaxArr[i], rightMaxArr[i]) - height[i];
  }

  return water;
}
复杂度分析

时间复杂度 O(n):需三次遍历数组(前缀、后缀、计算水量),总次数为 3n,渐进复杂度为 O(n)。

空间复杂度 O(n):需两个长度为 n 的辅助数组,空间开销与数组长度线性相关。

3.2 单调栈法(层叠积水视角)

该方法从"层叠积水"的角度思考,用单调栈维护一个递减的柱子高度序列,当遇到比栈顶高的柱子时,说明形成了积水区域,通过栈顶元素计算该区域的积水量。

实现思路
  1. 栈中存储柱子的索引,保证栈内索引对应的高度严格递减。

  2. 遍历数组,若当前柱子高度小于等于栈顶索引对应的高度,直接入栈(维持递减序列)。

  3. 若当前柱子高度大于栈顶索引对应的高度,弹出栈顶元素(作为积水区域的底部),计算该区域的宽度和高度,累加积水量。

  4. 重复步骤2-3,直至遍历完所有柱子。

代码实现
typescript 复制代码
function trap(height: number[]): number {
  const n = height.length;
  if (n < 3) return 0;

  const stack: number[] = []; // 存储柱子索引,维持高度递减序列
  let water = 0;

  for (let i = 0; i < n; i++) {
    // 当栈不为空且当前高度大于栈顶高度,说明形成积水区域
    while (stack.length > 0 && height[i] > height[stack[stack.length - 1]]) {
      const bottomIdx = stack.pop()!; // 积水区域的底部索引
      // 栈空则无左边界,无法形成积水,退出循环
      if (stack.length === 0) break;
      const leftIdx = stack[stack.length - 1]; // 积水区域的左边界索引
      // 积水高度 = 左右边界的较小值 - 底部高度
      const waterHeight = Math.min(height[leftIdx], height[i]) - height[bottomIdx];
      // 积水宽度 = 当前索引 - 左边界索引 - 1
      const waterWidth = i - leftIdx - 1;
      // 累加积水量
      water += waterHeight * waterWidth;
    }
    stack.push(i);
  }

  return water;
}
复杂度分析

时间复杂度 O(n):每个柱子最多入栈、出栈各一次,总操作次数为 2n,渐进复杂度为 O(n)。

空间复杂度 O(n):最坏情况下(柱子高度严格递减),栈内会存储所有柱子索引,空间开销为 O(n)。

四、解法对比与场景选择

为方便大家根据需求选择合适解法,整理对比表格如下:

解法 时间复杂度 空间复杂度 核心优势 适用场景
双指针法 O(n) O(1) 性能最优,无额外空间开销 空间敏感场景、面试最优解
前缀/后缀数组法 O(n) O(n) 逻辑直观,易理解和实现 新手学习、代码快速迭代场景
单调栈法 O(n) O(n) 从层叠视角理解积水,拓展性强 复杂积水场景分析、算法思路拓展

五、总结:算法优化与思路拓展

「接雨水」问题的核心是抓住"边界最大值约束水量"的本质,三种解法虽思路不同,但都围绕这一核心展开:

  1. 双指针法通过动态维护左右最大值,实现了性能最优,是面试中的首选解法,需重点掌握其"矮侧优先处理"的逻辑。

  2. 前缀/后缀数组法是核心原理的直接落地,适合新手入门,帮助理解接雨水的本质逻辑。

  3. 单调栈法提供了全新的视角,通过栈维护递减序列,能精准定位每个积水区域,拓展了数组问题的解题思路。

在实际应用中,需根据空间限制、代码可读性需求选择合适解法;面试中,优先掌握双指针法,同时能讲解其他解法的思路,更能体现对问题的深度理解。

相关推荐
灰海5 小时前
vue实现即开即用的AI对话打字机效果
前端·javascript·vue.js·打字机
液态不合群5 小时前
如何提升 C# 应用中的性能
开发语言·算法·c#
诗远Yolanda5 小时前
EI国际会议-通信技术、电子学与信号处理(CTESP 2026)
图像处理·人工智能·算法·计算机视觉·机器人·信息与通信·信号处理
程序员-King.5 小时前
day165—递归—最长回文子序列(LeetCode-516)
算法·leetcode·深度优先·递归
智绘前端5 小时前
React 组件开发速查卡
前端·react.js·前端框架
BHXDML5 小时前
推导神经网络前向后向传播算法的优化迭代公式
神经网络·算法·机器学习
2401_841495645 小时前
【LeetCode刷题】删除链表的倒数第N个结点
数据结构·python·算法·leetcode·链表·遍历·双指针
箫笙默5 小时前
前端相关技术简介
前端
叫我:松哥5 小时前
基于YOLO深度学习算法的人群密集监测与统计分析预警系统,实现人群密集度的实时监测、智能分析和预警功能,支持图片和视频流两种输入方式
人工智能·深度学习·算法·yolo·机器学习·数据分析·flask