题目四:接雨水(No. 43)
题目链接:https://leetcode.cn/problems/trapping-rain-water/description/?envType=study-plan-v2&envId=top-100-liked
难度:困难
给定 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
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
题解
首先来观察对于一个格子来说,它的容量是多少:
比如上图中的红色部分,它存储水的容量其实就是 左边的最大高度 和 右边的最大高度 的 最小值,减去这里的格子高度(案例中为 0),在上面的案例中,左边的最高高度为 2,右边的最高高度为 3。
所以本题的暴力解法就比较好想了,找到其左边最大高度,再找到其右边的最大高度,通过上面的规律进行运算。
class Solution {
public int trap(int[] height) {
int res = 0;
for (int i = 1; i < height.length; i++) {
int left = i - 1;
int right = i + 1;
int maxLeft = 0;
int maxRight = 0;
while (left >= 0) { // 去左边寻找最大的
maxLeft = Math.max(maxLeft, height[left--]);
}
while (right < height.length) { // 去右边寻找最大的
maxRight = Math.max(maxRight, height[right++]);
}
int temp = Math.min(maxLeft, maxRight) - height[i]; // 执行运算逻辑
res += temp > 0 ? temp : 0;
}
return res;
}
}
这个方法的时间复杂度无疑是非常高的,因为每遍历到一个节点的时候都需要遍历整张表,接下来考虑如何对上面的方法进行优化。
首先来看左边的最大值,考虑一下,我们真的有必要每次去遍历来求得最大值吗?
可以将之前遍历过的节点的最大值保存下来,比如说这么一个高度数组 4,2,0,3,2,5
,我们从第二个数字开始遍历,当执行完这个数字的逻辑之后,就可以拿它和之前的最大高度作比较,用作下一个节点的左边最大高度,这样不断更新就能保证每次求得的都是准确的。
class Solution {
public int trap(int[] height) {
int res = 0;
int maxLeft = height[0];
for (int i = 1; i < height.length; i++) {
int left = i - 1;
int right = i + 1;
int maxRight = 0;
while (right < height.length) { // 找到右边最大高度
maxRight = Math.max(maxRight, height[right++]);
}
int temp = Math.min(maxLeft, maxRight) - height[i];
res += temp > 0 ? temp : 0;
maxLeft = Math.max(height[i], maxLeft); // 维护一个更新的 maxLeft
}
return res;
}
}
既然对左边可以进行优化,那对右边是否可以优化呢?
因为是从前向后遍历的,所以右边肯定不可能像左边这样简单的更新;所以可以考虑提前处理,如果从后向前遍历的话,就可以类似左边那样求出 每个节点 的右最大值,所以可以在进入循环之前,先遍历一次高度数组,求出一个存储着每个节点右最大值的数组。
class Solution {
public int trap(int[] height) {
int res = 0;
int maxLeft = height[0];
int k = 2;
int[] maxRight = new int[height.length];
int m = height[maxRight.length - 1];
for (int j = maxRight.length - 2; j >= 0; j--) { // 从后向前遍历,维护一个存储每个节点最大值的数组
maxRight[j] = m;
m = Math.max(m, height[j]);
}
for (int i = 1; i < height.length; i++) {
int temp = Math.min(maxLeft, maxRight[i]) - height[i]; // 使用数组中的值
res += temp > 0 ? temp : 0;
maxLeft = Math.max(height[i], maxLeft);
}
return res;
}
}
题目一:无重复字符的最长字串(No. 3)
题目链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/?envType=study-plan-v2&envId=top-100-liked
题目难度:中等
给定一个字符串 s
,请你找出其中不含有重复字符的 最长
子串
的长度。
示例 1:
输入:s = "abcabcbb"
输出:3
解释: 因为无重复字符的最长子串是"abc",所以其长度为 3。
示例 2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是"b",所以其长度为 1。
示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是"wke",所以其长度为 3。
请注意,你的答案必须是子串的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
题解
首先来看第一种方法,提到最长子串,想到的第一个方法就是动态规划。
先来尝试找一下状态,题目中问的是没有重复字符的最长的子串,可以将某个节点的状态定义为:以这个节点为结尾的,不含有重复字符的字串的最大长度,这种方式在处理子串的问题中非常常见。
再来思考这个状态应该如何转移。对于一个节点其实有这些选择
- 从这个节点开始(重新开始)
- 接续前面的子串
本题接续前面子串的时候要考虑不能含有重复的元素,所以并不像之前的题目那样就简单的做一个加一,但总而言之状态是可以转移的,那就来尝试一下。
先来确定 dp 数组,根据上面的推理,dp 数组只需要一维就即可, dp[i]
的含义为以 s.charAt(i)
为结尾的,不含有重复子串的最大长度。
然后就是确定状态转移方程了,对于一个下标为 x 的节点,它所代表的子串就是从 x - dp[i]
到 x 这个范围内内容;比如我们现在遍历到了下标为 x + 1 的节点,如果想要接续上前面的内容,就需要上面的范围中不含有 s.charAt(x + 1)
这个字符,此时需要通过遍历来确定是否有这个字符。
如果没有发现,那 dp[i] = dp[i - 1] + 1
如果发现了重复的字符串,比如下面的情况
此时要求得的是绿色的 b 的值,前一个节点最大子串的长度为 3,但当遍历到图中粉色的节点的时候发现了重复,此时 b 的长度应该为多少呢?是 2
画个图来更直观的了解一下:
当发现了相同的节点之后,这个值应该就是从被发现的位置索引 + 1 一直到新节点的长度
此时就确定了两种情况的递推公式,下面来讨论一下初始化
因为需要前一个节点的情况,所以 dp[0]
是要被初始化的,按照上面的含义,该位置应该被初始化为 1。
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) return 0;
int[] dp = new int[s.length()];
dp[0] = 1;
int res = 1;
for (int i = 1; i < s.length(); i++) {
int field = dp[i - 1]; // 需要查询重复的范围
char c = s.charAt(i);
boolean findSame = false; // 是否找到了相同的节点
while (field > 0) {
int index = i - (field--);
if (s.charAt(index) == c) { // 找到了相同的节点
int m = i - index - 1 + 1;
dp[i] = m;
findSame = true;
break;
}
}
if (!findSame) dp[i] = dp[i - 1] + 1;
res = Math.max(dp[i], res); // 动态更新 res
}
return res;
}
}
接下来来看一下滑动窗口方案
所谓的滑动窗口其实就是用索引去限定一个范围,通过不断移动左范围和右范围的方式来调整窗口的范围,从而收集信息。
当滑动窗口的内容满足要求的时候就不断 移动右边界,来扩展滑动窗口的大小,如果发现不满足要求,也就是出现了重复的情况,就通过 收缩左边界 最终使得窗口内的元素始终满足要求,然后记录滑动窗口的长度。
滑动窗口方法的核心和上面的动态规划方法相同,都是通过遍历每个元素为 右节点 的情况,来不断更新最大值。
class Solution {
char[] charArray;
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) return 0;
charArray = s.toCharArray();
int left = 0, right; // 左范围,右范围
int res = 1;
for (int i = 1; i < charArray.length; i++) {
right = i;
while (examine(left, right, charArray[i])) {
left++;
}
res = Math.max(right - left + 1, res); // 更新结果
}
return res;
}
// 检测范围内是否有和此时右范围相同的元素
public boolean examine(int left, int right, char c) {
for (int i = left; i <= right - 1; i++) {
if (charArray[i] == c) return true;
}
return false;
}
}