这是字节跳动面试题库里面的题,也是上手做的第一道困难的题
五种解法,层层递进的思路
题目
五种解法从此开始
一、一行一行算(也是我的方法)超时了可以不看
说实话,这道题看完一瞬间我就冒出了这个想法,并且五分钟写完代码(代码习惯不好,debug花了很久)当时我的心情是
就这,困难题目不过如此,字节跳动不过如此
但是!一运行,超时!!!
看题目下面的评论说这个方法本来是AC的,后来改成超时了,我吐了,因为思路清晰并且运行通过(纪念自己的努力),所以还是贴上来
思路
这种一行一行算思路很简单:相当于切片,一层一层算水坑有多少
先算第一层,
意思就是,高度大于等于1的墙围起来的范围,[0,1,0,2,1,0,1,3,2,1,2,1]的原数组如下,判断大于等于1的范围。
被提取成[1,0,2,1,0,1,3,2,1,2,1]
然后找出有几块高度小于1,显然第一层有两个小于1的,第一层能装两块水。
然后算第二层
高度大于等于2的墙围起来的范围找出来[2,1,0,1,3,2,1,2]如下
然后找红色框范围内高度小于2的,发现有四块,黄色圆圈标出来的。
就是这样的思想,一直使操作的层数递增到最高的墙。
第三层就他一堵墙,没有小于3的,所以没有能装水的块
最后就是第一层的2块加第二层的4块=6
逐句注释的代码实现
class Solution {
public int trap(int[] height) {
//首先考虑特殊情况 数组长为0或者1
if (height.length == 0 && height.length == 1) return 0;
int totalWater = 0;//总水量
int level = 1;//起始层数
int maxNum = 0;//最大值
//取最大值
for (int i : height) {
maxNum = i >= maxNum ? i : maxNum;
}
while (level <= maxNum) {
ArrayList<Integer> temArr = new ArrayList<>();
//下面找到第一层 首先找到首尾 不小于1的第一个和最后一个数
//新建一个arrlist存坐标
ArrayList<Integer> index = new ArrayList<>();
for (int i = 0; i < height.length; i++) {
if (height[i] >= level) {
index.add(i);
}
}
//用首尾坐标切割原数组成新的数组
// 原数组 [0,1,0,2,1,0,1,3,2,1,2,1]
// 要切成 [ 1,0,2,1,0,1,3,2,1,2,1]
// 坐标数组 index[1,3,4,6,7,8,9,10,11]
// 坐标 最小的1,最大的11, 最小的排在 index里面的第一位 index.get(0) 最大的排在index里的最后一位 index.get(index.size()-1)
int minINdex = Collections.min(index);
int maxIndex = Collections.max(index);
for (int i = index.get(0); i < index.get(index.size() - 1) + 1; i++) {
temArr.add(height[i]);
}
minINdex = 0;
maxIndex = 0;
//然后从刚才找出的层里找比一小的 就是第一层能装的雨水坑
for (int j = 0; j < temArr.size(); j++) {
if (temArr.get(j) < level) {
totalWater++;
}
}
temArr.clear();
level++;
}
return totalWater;
}
}
IDEA里运行是没问题的,但是提交就超时了
原因是leetcode后台用来测试的最后一个数组。。。四五页都没显示全,大概上千个元素?
时间复杂度:如果最大的数是 m,个数是 n,那么就是 O(m*n) 显然数组很长而且最大元素很大时候会超时
空间复杂度:O(1)
二、按照列求
思路
对每一列来说,都判断自己这一列能不能装水,如何判断呢?
看自己左边最高的墙(不包括自己)和右边最高的墙(不包括自己),根据短板效应取这俩中更小的那个值lowWall
lowWall比自己高那才可能出现凹坑才能存水对吧,旁边的比自己矮那不流出去了
然后计算这一列上存了多少水
首先两端的墙不用考虑,存不了,直接从第二个看
比如下面红色这一列能存几个水呢?
1.红色自己高度是1,左边最高是绿色,高度为0,右边最高是紫色,值为3,根据短板效应取lowWall = 0,
2.lowWall没有自己高,装不了
来看个能装的例子,下图我们来看红色框里这一列,它能装多少水呢?
首先,左边最高的墙是绿色,高度为2,右边最高的墙为紫色,高度为3,lowWall取值为2
lowWall大于红色自身高度1所以能装啊
那每列能装多少水呢?根据上图看出来,是(lowWall-自身高度)
最后按照这个方法计算每一列就完事了
逐句注释的代码实现
public int trap(int[] height) {
int sum = 0;
//最两端的列不用考虑,因为一定不会有水。所以下标从 1 到 length - 2
for (int i = 1; i < height.length - 1; i++) {
int max_left = 0;
//找出左边最高
for (int j = i - 1; j >= 0; j--) {
if (height[j] > max_left) {
max_left = height[j];
}
}
int max_right = 0;
//找出右边最高
for (int j = i + 1; j < height.length; j++) {
if (height[j] > max_right) {
max_right = height[j];
}
}
//找出两端较小的
int min = Math.min(max_left, max_right);
//只有较小的一段大于当前列的高度才会有水,其他情况不会有水
if (min > height[i]) {
sum = sum + (min - height[i]);
}
}
return sum;
}
时间复杂度:O(n²),遍历每一列需要 nn,找出左边最高和右边最高的墙加起来刚好又是一个 n,所以是 n²。
空间复杂度:O(1)
三、动态规划(优化刚才的按列)
思路
刚才代码中出现了一些重复出现的循环遍历,每次计算新的一列时候,我们都要重新找两边最高的墙,完全没必要啊,利用动态规划的思想,把这些信息放到一个数组里就ok,动态规划的思想很复杂,在这里说不清,以后再详细讲解,反正大概就是把重复的方法归纳起来复用
逐句注释的代码实现
public int trap(int[] height) {
//sum计算总和
// 新建两个长度和height一样的数组用来存 每一列的左右最高墙在height中的索引
int sum = 0;
int[] max_left = new int[height.length];
int[] max_right = new int[height.length];
//求所有列的左边最高墙 索引都放在max_left里
for (int i = 1; i < height.length - 1; i++) {
max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
}
//求所有列的右边最高墙 索引都放在max_right里
for (int i = height.length - 2; i >= 0; i--) {
max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
}
//计算每一列的能存多少水累加起来 这一步和上面方法没区别
for (int i = 1; i < height.length - 1; i++) {
int min = Math.min(max_left[i], max_right[i]);
if (min > height[i]) {
sum = sum + (min - height[i]);
}
}
return sum;
}
动态规划优化了求一侧最大值的操作,其余代码没变化
时间复杂度:O(n)
空间复杂度:O(n),用来保存每一列左边最高的墙和右边最高的墙。
相比刚才的方法,时间复杂度降了下来,空间复杂度略有上升
四、双指针(把刚才上升的空间复杂度给降下来成为指标最优的解法)
思路
这道题中,可以看到,max_left[ i ] 和 max_right[ i ] 数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。
逐句注释的代码实现
我们先改造下 max_left
public int trap(int[] height) {
int sum = 0;
int max_left = 0;
//右侧的没改还用数组存
int[] max_right = new int[height.length];
for (int i = height.length - 2; i >= 0; i--) {
max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
}
//左侧的改了
for (int i = 1; i < height.length - 1; i++) {
max_left = Math.max(max_left, height[i - 1]);
int min = Math.min(max_left, max_right[i]);
if (min > height[i]) {
sum = sum + (min - height[i]);
}
}
return sum;
}
时间复杂度: O(n)
空间复杂度:O(1)
这种方法运行时间和内存消耗都击败了全国100%!
可以把右面也改一下,但是这样内存消耗居然只能击败全国97%,具体原因没思考,毕竟有个双百的了~
五、栈
思路
这道题好像就是想考栈= =
就是把墙的高度压入栈中,栈顶元素就是上一个墙的高度,如果指针指的当前墙比上一个墙高,那就说明,能存水,没有上一个墙高,那就存不了水,把这个墙放进去
具体思路都写代码注释里面了,注释已经不能再细了
逐句注释的代码实现
public int trap(int[] height) {
/**
* 初始栈
* 初始总和
* 初始指针指向第一个元素
*/
Stack<Integer> stack = new Stack<>();
int sum = 0;
int currIndex = 0;
/**
* 最后用大循环包围
*/
while (currIndex < height.length) {
/**
* 如果下一个比栈顶的大,那就进行一系列操作
*/
while (!stack.empty() && height[currIndex] > height[stack.peek()]) {
/**
* 1.首先取出栈顶元素 是计算面积的底
* 2.然后刚才的底没用了,出栈,我们要他之前的那个墙(新的栈顶)的高度,和currIndex指针指向的墙比,哪个矮,哪个-底就等于计算面积的高
* (矮墙-底) X 底边宽 = 面积
* 3.底边宽怎么求? 底边宽 = 后面的墙-前面的墙-1
* 4.所以最终 sum = 面积 = (矮墙-底) X 底边宽
*/
//1.
int bottomHeight = height[stack.peek()];
//2.
stack.pop();
//这里注意 要是出栈以后 栈空了那就跳出循环进行下一个height里的元素操作
if (stack.empty()) break;
//go on
int lowerWall = Math.min(height[currIndex], height[stack.peek()]);
//3.
int bottomWide = currIndex - stack.peek() - 1;
//4.
sum = sum + (lowerWall - bottomHeight) * bottomWide;
}
/**
* 如果下一个比栈顶的小,那就直接入栈
*/
stack.push(currIndex);
/**
* 遍历记得指针往后走
*/
currIndex++;
}
return sum;
}
}
时间复杂度:虽然 while 循环里套了一个 while 循环,但是考虑到每个元素最多访问两次,入栈一次和出栈一次,所以时间复杂度是 O(n)。
空间复杂度O(n)。栈的空间。
运行时间不算nice,这样看,双指针的动态规划牛🍺,各种优秀算法都有动态规划和双指针
一壶水,一包烟,一道力扣做一天
明天继续