LeetCode 热题 100——day7 接雨水

✨ 把代码写进星轨,用逻辑丈量宇宙。

导航 链接
个人主页 🏠 星轨初途
基础语言专栏 💻 C语言📚 数据结构
刷题实战专栏 🚀 算法及编程题分享、📝 力扣每日刷题分享

🌧️ 又到了每日刷题时间!今天这道《接雨水》算是非常经典的一道题了

(ง •_•)ง

一道题可以用双向扫描、前后缀、双指针、单调栈四种方式解决,正好一次把几个常见思想串起来!

文章目录

接雨水

题目思路

对于某一个位置 i 来说,它能够接多少水,取决于它左右两边最高的柱子。

假设:

text 复制代码
左边最高柱子 = leftMax
右边最高柱子 = rightMax

那么当前位置能够达到的最高水位就是:

text 复制代码
min(leftMax, rightMax)

因此当前位置能够接到的水为:

cpp 复制代码
min(leftMax, rightMax) - height[i]

所以这道题的核心其实就是:

找到每个位置左右两边能够形成的边界,再计算这个位置可以接多少水。

下面用四种方法来解决。


方法一:左右两次扫描

这是我最开始写的方法。

先从左向右扫描,使用 preh 记录当前左边界高度。

如果当前柱子比 preh 矮:

cpp 复制代码
curnum += preh - h;

先把这部分水暂时记录下来。

如果后面找到了:

cpp 复制代码
h >= preh

说明右边出现了一堵足够高的墙,这一段水确定可以接住:

cpp 复制代码
cnt += curnum;

然后更新新的左边界。

但是如果一直到数组末尾都没有找到比左边界更高的柱子,那么最后暂存的水不能直接计算,因为右边可能挡不住。

所以记录:

cpp 复制代码
prei

也就是左侧最后确定的边界位置,然后再从右往左扫描一次,把剩余区域计算出来。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

代码

cpp 复制代码
class Solution
{
public:
    int trap(vector<int>& height)
    {
        int n = height.size();
        int cnt = 0;
        int curnum = 0;
        int preh = height[0];
        int prei = 0;   
        // 从左向右计算
        for (int i = 1; i < n; i++)
        {
            int h = height[i];
            if (h >= preh)
            {
                // 找到足够高的右边界,这部分水可以确定
                cnt += curnum;
                curnum = 0;
                preh = h;
                prei = i;
            }
            else
            {
                // 暂存当前能够接的水
                curnum += preh - h;
            }
        }
        // 从右向左处理剩余部分
        preh = height[n - 1];
        curnum = 0;
        for (int i = n - 2; i >= prei; i--)
        {
            int h = height[i];
            if (h >= preh)
            {
                cnt += curnum;
                curnum = 0;
                preh = h;
            }
            else
            {
                curnum += preh - h;
            }
        }
        return cnt;
    }
};

这个方法的核心就是:

text 复制代码
左边能确定的 → 从左向右算
右边剩下的   → 从右向左算

不需要额外数组,空间复杂度只有 O(1)


方法二:前后缀最大值

这个方法是最好理解的一种。

对于每个位置 i,只要知道:

text 复制代码
左边最高柱子
右边最高柱子

就能直接算出当前位置接多少水。

先用 pre[i] 保存:

text 复制代码
0 ~ i 范围内的最高柱子

再用 end[i] 保存:

text 复制代码
i ~ n-1 范围内的最高柱子

于是:

cpp 复制代码
min(pre[i], end[i])

就是当前位置的水位。

当前位置的水量:

cpp 复制代码
min(pre[i], end[i]) - height[i]

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

代码

cpp 复制代码
class Solution 
{
public:
    int trap(vector<int>& height) 
    {
        int n = height.size();
        // pre[i]:i 左边(包括自己)的最高柱子
        vector<int> pre(n); 
        pre[0] = height[0];
        for (int i = 1; i < n; i++) 
        {
            pre[i] = max(pre[i - 1], height[i]);
        }
        // end[i]:i 右边(包括自己)的最高柱子
        vector<int> end(n); 
        end[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) 
        {
            end[i] = max(end[i + 1], height[i]);
        }
        int cnt = 0;
        // 每个位置能够接的水
        for (int i = 0; i < n; i++)
        {
            cnt += min(pre[i], end[i]) - height[i]; 
        }
        return cnt;
    }
};

这种方法非常直观,但是需要两个额外数组。


方法三:双指针优化

既然前后缀方法真正需要的只是:

text 复制代码
左侧最高值
右侧最高值

那么其实没有必要把所有位置的最大值全部保存下来。

可以使用两个变量:

cpp 复制代码
pre
end

分别记录当前左右两边的最高柱子。

再使用:

cpp 复制代码
left
right

从数组两端向中间移动。

如果:

cpp 复制代码
pre <= end

说明左边当前的短板是 pre

右侧已经存在一个高度至少为 pre 的柱子,所以 left 位置能接多少水已经可以直接确定:

cpp 复制代码
pre - height[left]

然后:

cpp 复制代码
left++;

反过来,如果:

cpp 复制代码
pre > end

那么就可以确定右边的水量:

cpp 复制代码
end - height[right]

然后:

cpp 复制代码
right--;

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

代码

cpp 复制代码
class Solution
{
public:
    int trap(vector<int>& height)
    {
        int cnt = 0;

        // 左右两边目前遇到的最高柱子
        int pre = 0, end = 0;

        int left = 0, right = height.size() - 1;

        while (left < right) 
        {
            pre = max(pre, height[left]); 
            end = max(end, height[right]); 

            // 左边比较低,左边水量已经可以确定
            if (pre <= end) 
            { 
                cnt += pre - height[left];
                left++;
            } 
            else 
            { 
                // 右边比较低,右边水量已经可以确定
                cnt += end - height[right];
                right--; 
            }
        }

        return cnt;
    }
};

这个方法可以看成:

把前后缀数组压缩成了两个变量。

所以既保持了 O(n) 的时间复杂度,又把空间复杂度优化到了 O(1)


方法四:单调栈

前面三种方法基本都是从竖着计算每根柱子上方有多少水的角度考虑。

单调栈则换了一个方向:

横着一层一层计算水。

使用一个栈保存柱子的下标,并保持栈中柱子高度整体单调递减。

当遇到一个更高的柱子 height[i] 时:

cpp 复制代码
height[st.top()] <= height[i]

说明当前柱子可以和栈中的左边柱子形成一个凹槽。

先取出凹槽底部:

cpp 复制代码
int pre_h = height[st.top()];

弹出之后,新的栈顶就是左边界:

cpp 复制代码
int left = st.top();

当前 i 就是右边界。

这一层水的高度:

cpp 复制代码
min(height[left], height[i]) - pre_h

宽度:

cpp 复制代码
i - left - 1

所以这一层水量:

cpp 复制代码
dh * (i - left - 1)

然后继续弹栈,相当于一层一层向上计算。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

虽然存在 while,但是每个元素最多:

text 复制代码
入栈一次
出栈一次

所以整体依然是 O(n)

代码

cpp 复制代码
class Solution
{
public:
    int trap(vector<int>& height) 
    {
        int ans = 0;
        // 存储柱子的下标
        stack<int> st;
        for (int i = 0; i < height.size(); i++) 
        {
            int h = height[i];
            // 当前柱子比栈顶高,可以形成凹槽
            while (!st.empty() && height[st.top()] <= h)
            {
                // 凹槽底部高度
                int pre_h = height[st.top()];
                st.pop();
                // 没有左边界,无法接水
                if (st.empty()) 
                {
                    break;
                }
                // 左边界
                int left = st.top();
                // 当前这一层水的高度
                int dh = min(height[left], height[i]) - pre_h; 
                // 高 × 宽
                ans += dh * (i - left - 1);
            }
            st.push(i);
        }
        return ans;
    }
};

例如出现:

text 复制代码
3   _   2
|       |
|   1   |

单调栈会把中间较矮的柱子作为"坑底",找到左右边界以后计算这一层能够接多少水。

如果凹槽有多层,就不断弹栈:

text 复制代码
第一层 → 算一次
第二层 → 再算一次
第三层 → 再算一次

最终把所有层的面积累加起来。


四种方法总结

方法 核心思路 时间复杂度 空间复杂度
左右两次扫描 左边确定一部分,再从右边补剩余部分 O(n) O(1)
前后缀最大值 保存每个位置左右最高柱子 O(n) O(n)
双指针 用两个变量代替前后缀数组 O(n) O(1)
单调栈 横向计算每个凹槽中的水 O(n) O(n)

这四种方法其实可以分成两类:

cpp 复制代码
按列计算:
左右扫描
前后缀
双指针

按层计算:
单调栈

其中前后缀方法最容易理解,而双指针则是在它的基础上进一步进行了空间优化。

单调栈的思维方式和前面几种完全不同,它不是考虑:

text 复制代码
"这一根柱子上面有多少水?"

而是考虑:

text 复制代码
"这个凹槽这一层能装多少水?"

所以第一次接触的时候会稍微绕一点,但理解以后还是非常有意思的

( •̀ ω •́ )✧

🎉 今天的《接雨水》就拿下啦!一道题连着练到了前后缀、双指针和单调栈,含金量还是相当高的。

有时候一道困难题不一定要只记住一种写法,多换几个角度去看,反而更容易把算法之间的联系串起来 ٩(ˊᗜˋ*)و

每日一题继续打卡,我们下一题再见!🌟

相关推荐
Ztt6666666661 小时前
荷口花瓶从瓶口开出一朵花
笔记·其他·百度·微信公众平台·微信开放平台
wangjialelele1 小时前
Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)
java·spring boot·笔记·学习·spring·spring cloud·gateway
qeen871 小时前
【数据结构】搜索二叉树的介绍与算法原理解析及实现
数据结构·c++·学习·算法·模板
风曦Kisaki1 小时前
# Kubernetes(K8s)笔记Day13 :工作负载资源概述与 Job (CronJob)控制器
linux·笔记·云原生·容器·kubernetes
阳明山水1 小时前
销量预测的“隐形杀手”:概念漂移与自适应学习实战
人工智能·深度学习·算法·机器学习·架构
Zenova EdgeOS2 小时前
C++ 工业边缘 Qt 桌面应用实战:从 QWidget 到模型视图、网络与多线程
网络·c++·qt
szarron2 小时前
HT666 0.6-6GHz 定向高增益手持天线:EMC 测试与频谱定位实战指南
网络·算法·5g·信号处理·射频工程·频谱仪
城管不管2 小时前
2026.8.6成都分部一面当场OC
面试·职场和发展
liulilittle2 小时前
反量化:反量化Q8零(q8_0)
人工智能·算法·机器学习·ai·llm