借助Leetcode 热门题目学习C++系列——双指针

借助Leetcode 热门题目学习C++系列——双指针&&“接雨水问题”

问题描述

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

示例 1

输入

height = [0,1,0,2,1,0,1,3,2,1,2,1]

输出

6

解释
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
图示:
请添加图片描述

示例 2

输入

height = [4,2,0,3,2,5]

输出

9

常见思路

一般情况下,可有以下两种常见思路解决该题。

方法一:动态规划

思路
对于下标 i,下雨后水能到达的最大高度等于 下标 i 两边的最大高度的最小值
下标 i 处能接的雨水量等于:
min ( leftMax [ i ] , rightMax [ i ] ) − height [ i ] \text{min}(\text{leftMax}[i], \text{rightMax}[i]) - \text{height}[i] min(leftMax[i],rightMax[i])height[i]

朴素的做法

height 中的每个元素,分别向左和向右扫描,记录左边和右边的最大高度,然后计算每个位置能接的雨水量。

  • 时间复杂度:O(n²) (每个元素向左右扫描)
  • 优化方向:使用动态规划预处理左/右最大高度,避免重复扫描。

优化:动态规划

  1. 定义两个数组

    • leftMax[i]:表示 i 及其左边位置中,height 的最大高度。
    • rightMax[i]:表示 i 及其右边位置中,height 的最大高度。
  2. 预处理左/右最大高度

    • leftMax[0] = height[0]
    • rightMax[n-1] = height[n-1]
    • 状态转移
      • leftMax[i] = max(leftMax[i-1], height[i])
      • rightMax[i] = max(rightMax[i+1], height[i])
  3. 计算雨水量

    • 对于 0 ≤ i < n
      water [ i ] = min ( leftMax [ i ] , rightMax [ i ] ) − height [ i ] \text{water}[i] = \text{min}(\text{leftMax}[i], \text{rightMax}[i]) - \text{height}[i] water[i]=min(leftMax[i],rightMax[i])height[i]

复杂度分析

  • 时间复杂度:O(n)(遍历 height 三次)
  • 空间复杂度:O(n)(存储 leftMaxrightMax

方法二:单调栈

思路

  • 维护一个 单调递减栈,栈内存储的是 height 数组的下标
  • 栈底到栈顶对应的 height 元素是 单调递减 的。

步骤

  1. 遍历 height 数组,维护单调栈:

    • 如果当前 height[i] 大于 栈顶 height[top],则说明形成了一个接水区域。
    • 计算该区域的雨水:
      water = ( right − left − 1 ) × ( min ( height [ l e f t ] , height [ i ] ) − height [ t o p ] ) \text{water} = (\text{right} - \text{left} - 1) \times (\text{min}(\text{height}[left], \text{height}[i]) - \text{height}[top]) water=(rightleft1)×(min(height[left],height[i])height[top])
    • 不断弹出栈顶,直到栈为空或者 height[i] <= height[top]
    • 最后将 i 入栈。
  2. 遍历结束后,水量累加得到最终结果。

复杂度分析

  • 时间复杂度:O(n)(每个下标 i 只会入栈和出栈各一次)
  • 空间复杂度:O(n)(存储 height 下标的单调栈)

以上两种方法并不是本文重点,因此不展示代码实现。


本文重头戏——双指针

为什么说是重头戏?因为这种奇妙的思路使得解决这个问题仅仅需要常数量级的空间耗费!

思路

利用 leftMaxrightMax,但用两个变量代替数组,优化空间复杂度到 O(1):

  • 维护两个指针 leftright,分别从两端向中间移动
  • 维护两个变量 leftMaxrightMax,记录当前遍历过的最大高度。用每个位置的height值进行可能的更新。

步骤

  1. 初始化

    • left = 0, right = n-1
    • leftMax = 0, rightMax = 0
  2. 双指针遍历

    • 如果 height[left] < height[right]

      • 由于 leftMax < rightMax,左指针 left 处的水量为:
        water [ l e f t ] = leftMax − height [ l e f t ] \text{water}[left] = \text{leftMax} - \text{height}[left] water[left]=leftMaxheight[left]
      • 计算后 left++
    • 否则(height[left] >= height[right]

      • 由于 rightMax ≤ leftMax,右指针 right 处的水量为:
        water [ r i g h t ] = rightMax − height [ r i g h t ] \text{water}[right] = \text{rightMax} - \text{height}[right] water[right]=rightMaxheight[right]
      • 计算后 right--
  3. left == right,遍历结束,总水量累加得到最终结果。

会不会有点乱?

把抽象思路转换成题目具体信息,其实不难理解, leftMax < rightMax其实就意味着,右边的真正的“水桶隔板”要高于左边的隔板,意味着左指针的位置是有能力存一定的水的,那具体能存多少水呢?就是 water [ l e f t ] = leftMax − height [ l e f t ] \text{water}[left] = \text{leftMax} - \text{height}[left] water[left]=leftMaxheight[left]

总结一下就是:左端此处能存水由左端最大值和右端最大值的比较结果决定,此处能存多少水则由此处高度和左端最大高度的差决定。非常巧妙。

复杂度分析

  • 时间复杂度:O(n)(每个元素最多访问一次)
  • 空间复杂度:O(1)(仅使用常数额外空间)

具体的代码实现:

class Solution {
public:
    int trap(vector<int>& height) {
        int left = 0, right = height.size() - 1;
        int left_max = 0, right_max = 0;
        int ans = 0;
        
        while (left < right) {
            if (height[left] < height[right]) {
                if (height[left] >= left_max) left_max = height[left];
                else ans += left_max - height[left];
                ++left;
            } else {
                if (height[right] >= right_max) right_max = height[right];
                else ans += right_max - height[right];
                --right;
            }
        }
        return ans;
    }
};

另一种想法

筑波在写这题的时候,想到的一开始其实是另一种办法——

计算面积

什么意思,同样还是两个指针,从两端向中间移动,定义变量h = 1,面积s = 0;

  1. 当两个指针第一次遇到height[left] = h height[right] = h的时候,停下,记录这一层的面积: s += right - left + 1
  2. h自增,左右指针重新回到两端。
  3. 重复1、2,直到h等于height中的最大值时终止。

在执行以上算法之前,你需要先求出height所有元素的和以及最大值(复杂度为 O ( n ) O(n) O(n))。

这个思想是什么意思呢?就是想用计算出的面积,减去原数组的大小,就可以得到那些能存住水的方块面积(能存住水是由两个指针第一次遇到高度为h时停下所决定的,在这两个指针中间的区域内,所有height小于h的地方都有机会存水(具体需要在做减法的时候得出)。

只可惜,这个方案虽然对绝大多数案例都可行,但是它的时间复杂度,在数组非常长的情况下,每更新一个h,两个指针都会重新遍历一遍数组,实际上将会消耗掉非常多的时间。

(这才是最绝望的死法)

请添加图片描述
所以这个巧妙的思路死在了最后一关上,不过仍然是一个优秀的思路。

class Solution {
public:
    int trap(vector<int>& height) {
    int left = 0;
    int right = height.size() - 1;
    int h = 1;
    int s = 0;

    int high = 0;

    int l = height[left];
    int r = height[right];

    int max_height = height[left];

    for(auto i = height.begin(); i != height.end(); ++i){
        high += *i;
        if(*i > max_height){
            max_height = *i;
        }
    }
    //cout << "max_height: " << max_height << endl;
    while(h <= max_height){
        //记住两端的位置
        left = 0;
        right = height.size() - 1;
        l = height[left];
        r = height[right];
        // cout << left << " " << right << endl;
        // cout << "l: " << l << " r: " << r << endl;
        // cout << " h : " << h << endl;
        while(l < h || r < h){
            l = height[left];
            r = height[right];
            if(l < h){
                //cout << "l != h" << endl;
                left++;
            }
            if(r < h){
                //cout << "r != h" << endl;
                right--;
            }
            if(left >= right){
                break;
            }
        }
        // 计算当前的面积
        s += (right - left + 1);
        h++;
     }
        return s-high;
    }
};

总的来说,双指针是一个相当好的工具,可以在不同遍历方向上记录数据,并且节省遍历时间,特别是当问题解决需要两个主要变量共同决定时。


方法对比总结

方法时间复杂度空间复杂度适用情况
暴力法O(n²)O(1)适用于小规模数据
动态规划O(n)O(n)适用于中等规模数据
单调栈O(n)O(n)适用于需要维护局部信息的情况
双指针O(n)O(1)最优解,适用于大规模数据

最近筑波还在尝试其他双指针题目,各有魅力,后续继续更新~欢迎大家一起讨论!

(精彩继续~)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值