力扣——42.接雨水
- 一、算法目录合集
- 1.地址
- 2.说明
- 二、题目说明
- 1.题干
- 2.原地址
- 三、实现步骤
- 1.思路分析
- 1.1.分析问题
- 1.2.转化问题
- 1.3.简化问题
- 1.4.具体步骤
- ① 特殊情况分析
- ② 常规分析
- ③ 分类分析
- 2.代码实现
- 2.1 方法代码
- 2.2 测试部分代码
- 2.3 耗用资源情况
- 四、官方题解
- 1.原地址
- 2.方法一——暴力
- 思路分析
- 代码实现(C++)
- 复杂度
- 3.方法二——动态编程
- 思路分析
- 代码实现
- 复杂度
- 4.方法三——栈的应用
- 思路分析
- 代码实现
- 复杂度
- 5.方法四——使用双指针
- 思路分析
- 代码实现
- 复杂度
一、算法目录合集
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.思路分析
对于这个题,其实解决方法挺多的,双指针,动态分析等;我这里用一个递归的方法来求,原题比较不太容易看出来这个思路,我画一个新的图,下面的图分别是空桶和最大限度装满水的情况:
空桶
满水
这个图的层数稍微高了一点儿,柱子与柱子之间的距离有些刻意空开,这样比较直观地展现思路。
有些人会被题目误导,产生一个惯性思维:题目叫“接雨水”,那我就看看是什么样的填充方法可以满足要求?试试从下往上吧,于是在他心里,就有了这么一个过程:
其实这种思路也不能说是一种错误思路,因为完全是可以实现的,先把最左边的1和最右边的1找到,那么最中间的0肯定都是要变成1的,因为他们都被填上了水……以此类推,一直填到最高处,那么如果一个数组中只有一个数字呢,比如只有一个2,那么怎么操作呢,那就只能找比他高一位的了——3,这样最左侧的2和最右侧的3之间所有的比2小的填充为2,再这么进行下去,一直到最高的柱子,最终得到的就是结果了,把总值减去之前的总值,就是填充部分的雨水面积,只不过这种方法需要每次判定最右侧的柱子到底怎么选择,比较麻烦,不推荐。
下面提供一种我想的解题思路:
1.1.分析问题
首先要分析一下,怎么才能方便地去填水呢?不知道大家见过这个模型没:香槟塔!
香槟塔矢量图(图片来自千库网)
香槟塔(图片来自百度)
没错,就是这么个东西,这是怎么倒酒的?你没见过一个人拿个香槟一个杯子一个杯子挨个倒吧?这个就是从最高处的杯子倒水,它满了自然就自动去倒下一层了,这个思想也可以用在我们这一题来。
1.2.转化问题
怎么把这个填充问题转化成香槟塔呢?请看下图:
我们就往这个最高层倒水,结果显而易见(这玩意儿又不是杯子,这是柱子,柱子!!),离中间最近的空格子很快就会满了然后就是继续往两边进行扩散,然后渐渐地两侧地也满了,然后就没有然后了,这水不就满了吗?
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就是目前的最大值。
③ 分类分析
找到最高的之后就很简单了,先看左边:
这里就要用到递归的思想了,假设左边剩下的部分已经填满了,我们只需要填最后一步,就像图中,先考虑黄色,再考虑蓝色:
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的值,接着对剩下的左边部分,再次调用本身方法。
同理,右边也是这样:
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.方法二——动态编程
思路分析
直观想法
在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
这个概念可以见下图解释:
算法
- 找到数组中从下标 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.
友情提示:此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 只需要常数的空间。