力扣——42.接雨水(困难难度)——条条大路通罗马

一、算法目录合集

1.地址

   算法目录合集

2.说明

  该地址指向所有由本人自己所经历的算法习题(也有可能仅仅是一个入门的案例或者是经典案例),仅仅为我做过而且比较有意思的,也许还会有一些我自己想出来的,出于兴趣写在这里,具体格式我会在下面列明,总目录也会在这里出现,方便查阅以及自己进行复习回顾。

二、题目说明

1.题干

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

  上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

2.原地址

  42.接雨水

三、实现步骤

1.思路分析

  对于这个题,其实解决方法挺多的,双指针,动态分析等;我这里用一个递归的方法来求,原题比较不太容易看出来这个思路,我画一个新的图,下面的图分别是空桶和最大限度装满水的情况:

空桶
empty

满水
full

  这个图的层数稍微高了一点儿,柱子与柱子之间的距离有些刻意空开,这样比较直观地展现思路。
  有些人会被题目误导,产生一个惯性思维:题目叫“接雨水”,那我就看看是什么样的填充方法可以满足要求?试试从下往上吧,于是在他心里,就有了这么一个过程:
错误思路
  其实这种思路也不能说是一种错误思路,因为完全是可以实现的,先把最左边的1和最右边的1找到,那么最中间的0肯定都是要变成1的,因为他们都被填上了水……以此类推,一直填到最高处,那么如果一个数组中只有一个数字呢,比如只有一个2,那么怎么操作呢,那就只能找比他高一位的了——3,这样最左侧的2和最右侧的3之间所有的比2小的填充为2,再这么进行下去,一直到最高的柱子,最终得到的就是结果了,把总值减去之前的总值,就是填充部分的雨水面积,只不过这种方法需要每次判定最右侧的柱子到底怎么选择,比较麻烦,不推荐。
  下面提供一种我想的解题思路:

1.1.分析问题

  首先要分析一下,怎么才能方便地去填水呢?不知道大家见过这个模型没:香槟塔!

香槟塔矢量图(图片来自千库网)在这里插入图片描述

香槟塔(图片来自百度)百度

  没错,就是这么个东西,这是怎么倒酒的?你没见过一个人拿个香槟一个杯子一个杯子挨个倒吧?😀😀😀😀这个就是从最高处的杯子倒水,它满了自然就自动去倒下一层了,这个思想也可以用在我们这一题来。

1.2.转化问题

  怎么把这个填充问题转化成香槟塔呢?请看下图:
pour
  我们就往这个最高层倒水,结果显而易见(😕😕😕这玩意儿又不是杯子,这是柱子,柱子!!),离中间最近的空格子很快就会满了然后就是继续往两边进行扩散,然后渐渐地两侧地也满了,然后就没有然后了,这水不就满了吗?

1.3.简化问题

  在上一个步骤里按照逻辑是没有任何问题的,不过可以再进行优化一下:当距离最近的格子不是最高格子(当然除了已知的最高的那个了😏😏,最高的记为A)的时候,可以忽略不记,一直到最高的(记为B)那个,然后把他到实际最高的格子A之间所有的格子填满水(即高度提升至B的高度),如图:
在这里插入图片描述
  然后继续往下找,记为C,填充至C
在这里插入图片描述
  如果还有,就继续DEFG,把26个字母给他用完,不够?用汉字计数!!😂😂😂😂这样就把问题给简化了。

1.4.具体步骤

① 特殊情况分析

  如果整个数组的元素只有一个或者没有,那说明无法倒水,返回0(说明一个格子的水也没填上)。

if (height.length == 0 || height.length == 1) {
	return 0;
}
② 常规分析

  常规步骤就是上述的分析,先找出来最高的,找出他的值以及他的索引位置,方便以后的操作:

        ArrayList<Integer> heightList = new ArrayList<>();
        int[] heightNew = new int[height.length];

        for (int i : height) {
            heightList.add(i);
        }
        if (height.length == 0 || height.length == 1) {
            return 0;
        }
        int max = heightList.get(0);
        int maxIndex = 0;
        for (int i = 0; i < heightList.size(); i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        heightNew[maxIndex] = max;

  maxIndex就是最大值的索引位置,max就是目前的最大值。

③ 分类分析

  找到最高的之后就很简单了,先看左边
  这里就要用到递归的思想了,假设左边剩下的部分已经填满了,我们只需要填最后一步,就像图中,先考虑黄色,再考虑蓝色:
left

private void getLeft(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
	int max = heightList.get(startIndex);
	int maxIndex = startIndex;

	for (int i = startIndex; i < tailIndex; i++) {
		if (heightList.get(i) > max) {
			max = heightList.get(i);
			maxIndex = i;
		}
	}

	for (int i = maxIndex; i < tailIndex; i++) {
		heightNew[i] = max;
	}
	if (maxIndex != startIndex) {
		getLeft(heightList, heightNew, 0, maxIndex);
	}
}

  这里是把左边的格子填满,实现步骤是先把最高的找出来,赋值给max,并记录索引位置maxIndex,然后把maxIndex右边的,一直到A柱子中的所有数字,变成max的值,接着对剩下的左边部分,再次调用本身方法。

  同理,右边也是这样:
right

private void getRight(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
	int max = heightList.get(startIndex);
	int maxIndex = startIndex;

	for (int i = startIndex; i <= tailIndex; i++) {
		if (heightList.get(i) >= max) {
			max = heightList.get(i);
			maxIndex = i;
		}
	}
	for (int i = startIndex; i <= maxIndex; i++) {
		heightNew[i] = max;
	}
	if (maxIndex != tailIndex) {
		getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
	}
}

  左边和右边的方法已经完成了然后我们需要调用一下,调用方法之前有一个注意事项:确定A柱子右边是有东西的。如果A柱子已经在最右边了,那我们调用getRight方法的时候会报出“IndexOutOfBoundsException”异常,所以我们只有在A柱子不在最右边才会调用getRight方法。

getLeft(heightList, heightNew, 0, maxIndex);
if (maxIndex != height.length - 1) {
	getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
}

  最后,我们求出两数组中各个数值的和的差就是答案了。

for (int i = 0; i < heightNew.length; i++) {
	result += (heightNew[i] - height[i]);
}

2.代码实现

2.1 方法代码

class Solution42 {
    public int trap(int[] height) {
        ArrayList<Integer> heightList = new ArrayList<>();
        int[] heightNew = new int[height.length];

        for (int i : height) {
            heightList.add(i);
        }
        if (height.length == 0 || height.length == 1) {
            return 0;
        }
        int max = heightList.get(0);
        int maxIndex = 0;
        for (int i = 0; i < heightList.size(); i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        
        heightNew[maxIndex] = max;

        getLeft(heightList, heightNew, 0, maxIndex);
        if(maxIndex!=height.length-1) {
            getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
        }
        int result = 0;
        for (int i = 0; i < heightNew.length; i++) {
            result += (heightNew[i]-height[i]);
        }
        
        return result;
    }

    private void getLeft(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
        int max = heightList.get(startIndex);
        int maxIndex = startIndex;

        for (int i = startIndex; i < tailIndex; i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }

        for (int i = maxIndex; i < tailIndex; i++) {
            heightNew[i] = max;
        }
        if (maxIndex != startIndex) {
            getLeft(heightList, heightNew, 0, maxIndex);
        }
    }

    private void getRight(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
        int max = heightList.get(startIndex);
        int maxIndex = startIndex;

        for (int i = startIndex; i <= tailIndex; i++) {
            if (heightList.get(i) >= max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        for (int i = startIndex; i <= maxIndex; i++) {
            heightNew[i] = max;
        }
        if (maxIndex != tailIndex) {
            getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
        }
    }
}

2.2 测试部分代码

这里随便定义一个随便看看就好了

public class Test42Hard {
    public static void main(String[] args) {
        Solution42 s = new Solution42();
        System.out.println(s.trap(new int[]{4,2,0,3,2,5}));
    }
}

2.3 耗用资源情况

耗用资源情况

四、官方题解

1.原地址

力扣官方答疑戳这里

2.方法一——暴力

思路分析

直观想法
  直接按问题描述进行。对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
算法

  • 初始化 ans=0,
  • 从左向右扫描数组:
    • 初始化 max_left = 0 和 max_right = 0
    • 从当前元素向左扫描并更新:
      • max_left = max (max_left ,height[j])
    • 从当前元素向右扫描并更新:
      • max_right = max (max_right ,height[j])
    • 将 min (max_left ,max_right) - height[j] 累加到 ans\

代码实现(C++)

int trap(vector<int>& height)
{
    int ans = 0;
    int size = height.size();
    for (int i = 1; i < size - 1; i++) {
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) { //Search the left part for max bar size
            max_left = max(max_left, height[j]);
        }
        for (int j = i; j < size; j++) { //Search the right part for max bar size
            max_right = max(max_right, height[j]);
        }
        ans += min(max_left, max_right) - height[i];
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n2)。数组中的每个元素都需要向左向右扫描。
  • 空间复杂度:
      O(1) 的额外空间。

3.方法二——动态编程

思路分析

直观想法
   在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
   这个概念可以见下图解释:
leetcode1
算法

  • 找到数组中从下标 i 到最左端最高的条形块高度 left_max 。
  • 找到数组中从下标 i 到最右端最高的条形块高度 right_max 。
  • 扫描数组 height 并更新答案:
    • 累加 min (max_left[i],max_right[i]) − height[i] 到 ans 上

代码实现

int trap(vector<int>& height)
{
	if(height == null)
		return 0;
    int ans = 0;
    int size = height.size();
    vector<int> left_max(size), right_max(size);
    left_max[0] = height[0];
    for (int i = 1; i < size; i++) {
        left_max[i] = max(height[i], left_max[i - 1]);
    }
    right_max[size - 1] = height[size - 1];
    for (int i = size - 2; i >= 0; i--) {
        right_max[i] = max(height[i], right_max[i + 1]);
    }
    for (int i = 1; i < size - 1; i++) {
        ans += min(left_max[i], right_max[i]) - height[i];
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n)。存储最大高度数组,需要两次遍历,每次 O(n) 。最终使用存储的数据更新ans ,O(n)。
  • 空间复杂度:
       O(n) 额外空间。和方法 1 相比使用了额外的O(n) 空间用来放置left_max 和right_max 数组。

4.方法三——栈的应用

思路分析

直观想法
   我们可以不用像方法 2 那样存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。
   我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到ans 。
算法

  • 使用栈来存储条形块的索引下标。
  • 遍历数组:
    • 当栈非空且height[current]>height[st.top()]
      • 意味着栈中元素可以被弹出。弹出栈顶元素top。
      • 计算当前元素和栈顶元素的距离,准备进行填充操作
        distance=current−st.top()−1
      • 找出界定高度
        bounded_height=min(height[current],height[st.top()])−height[top]
      • 往答案中累加积水量ans+=distance×bounded_height
    • 将当前索引下标入栈
    • 将 current 移动到下个位置

代码实现

int trap(vector<int>& height)
{
    int ans = 0, current = 0;
    stack<int> st;
    while (current < height.size()) {
        while (!st.empty() && height[current] > height[st.top()]) {
            int top = st.top();
            st.pop();
            if (st.empty())
                break;
            int distance = current - st.top() - 1;
            int bounded_height = min(height[current], height[st.top()]) - height[top];
            ans += distance * bounded_height;
        }
        st.push(current++);
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n)。单次遍历O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。
  • 空间复杂度:
       O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n)的空间。

5.方法四——使用双指针

思路分析

直观想法
   和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
   从动态编程方法的示意图中我们注意到,只要 right_max[i] > left_max[i] (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 left_max[i] > right_max[i](元素 8 到元素 11)。
   所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
   我们必须在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
算法

  • 初始化 left 指针为 0 并且 right 指针为 size-1
  • While left<right, do:
    • If height[left] < height[right]
      • If height[left] ≥ left_max, 更新 left_max
      • Else 累加 left_max − height[left] 到 ans
      • left = left + 1.
    • Else
      • If height[right] ≥ right_max, 更新 right_max
      • Else 累加 right_max−height[right] 到 ans
      • right = right - 1.

step
友情提示:此gif循环播放,不算开始图一共11张,每张1秒,如果想暂停看截图或者去官网

代码实现

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

复杂度

  • 时间复杂度:
       O(n)。单次遍历的时间O(n)。
  • 空间复杂度:
       O(1) 的额外空间。left,right,left_max 和right_max 只需要常数的空间。
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值