刷题记录10
力扣72. 编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
解题思路:
本题与583. 两个字符串的删除操作其实是一样的,只是583是可以在两个字符串中进行删除,而本题只在word1中进行增删改操作,这里我们需要对操作进行等价。
在word1中删除元素不用变
在word1中增加元素相当于在word2中删除对应元素
在word1中修改元素相当于同时删除word1和word2中对应的元素(元素不同时)
动规五部曲:
1.dp数组定义和含义
定义二维dp数组,大小为word1和word2的大小加1,dp[i][j]表示以i-1结尾的word1的子串,通过增删改变成word2的以j-1结尾的子串的最小操作数。
2.dp数组递推公式
当word1[i-1]==word2[j-1],即比较的字符相同时,不需要对word1做任何操作,因此dp[i][j]=dp[i-1][j-1]。
当word1[i-1]!=word2[j-1],即比较的字符不同时,我们可以有三种操作:
- 删除word1中对应元素,则dp[i][j] = dp[i-1][j]+1
- 在word1增加一个对应的word2元素,相当于删除word2中对应元素,则dp[i][j]=dp[i][j-1]+1
- 改变word1中对应元素为word2对应元素,相当于同时删除word1和word2对应元素,则dp[i][j]=dp[i-1][j-1]+1
综上,当比较的字符不同时,dp[i][j]=min{dp[i-1][j],dp[i][j-1],dp[i-1][j-1]} + 1.
if (word1.charAt(i-1) == word2.charAt(j-1)) {// 字符相等,不需要删除dp[i][j] = dp[i-1][j-1];}else {// 字符不等,考虑删除、修改、增加的情况dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i-1][j-1], dp[i][j-1])) + 1;}
3.dp数组初始化
同样的,我们需要对dp数组的第一行和第一列进行初始化
dp[i][0]表示word1中以i-1结尾的子串要变成word2中空串需要的最小操作数,只需要删除对应的字符即可,故dp[i][0]=i
dp[0][j]表示word1中空串要变成word2中以j-1结尾的子串需要的最小操作数,只需要增加对应的字符即可,故dp[0][j]=j
// 初始化dp数组for (int i = 1; i <= word1.length(); i++) {// word1以i-1个字符结尾的子序列,变成空字符串,需要删除的字符个数为idp[i][0] = i;}for (int j = 1; j <= word2.length(); j++) {// word2以j-1个字符结尾的子序列,变成空字符串,需要删除的字符个数为jdp[0][j] = j;}
4.确认遍历顺序
dp值只与左方,左上方和上方dp值有关,因此外层正序遍历word1,内层正序遍历word2即可
5.举例推导dp数组
输入:word1 = "horse", word2 = "ros"
为例,dp矩阵状态图如下:
整体代码:
class Solution {public int minDistance(String word1, String word2) {int[][] dp = new int[word1.length()+1][word2.length()+1];// 初始化dp数组for (int i = 1; i <= word1.length(); i++) {// word1以i-1个字符结尾的子序列,变成空字符串,需要删除的字符个数为idp[i][0] = i;}for (int j = 1; j <= word2.length(); j++) {// word2以j-1个字符结尾的子序列,变成空字符串,需要删除的字符个数为jdp[0][j] = j;}// 遍历for (int i = 1; i <= word1.length(); i++) {for (int j = 1; j <= word2.length(); j++) {if (word1.charAt(i-1) == word2.charAt(j-1)) {// 字符相等,不需要删除dp[i][j] = dp[i-1][j-1];}else {// 字符不等,考虑删除、修改、增加的情况dp[i][j] = Math.min(dp[i-1][j],Math.min(dp[i-1][j-1], dp[i][j-1])) + 1;}}}return dp[word1.length()][word2.length()];}
}
力扣647.回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
解法一:暴力破解
解题思路:
通过两层循环遍历所有的子串,然后内层增加一个回文串判断,如果子串是回文串则计数加1
class Solution {public int countSubstrings(String s) {int res = 0;for (int i = 0; i < s.length(); i++){for (int j = i; j < s.length(); j++) {if (isHuiWenString(s.substring(i, j+1))) {res++;}}}return res;}public boolean isHuiWenString(String s) {int left = 0, right = s.length()-1;while (left < right) {if (s.charAt(left) != s.charAt(right)) return false;left++;right--;}return true;}
}
解法二:动态规划
解题思路:
如果字符串开头和结尾的两个字符相同,那么该子串是否为回文串则看去掉这两个字符后的子串是否为回文串。
动规五部曲:
1.dp数组定义和含义
定义二维boolean类型dp数组,大小为字符串s的长度,dp[i][j]表示字符串s下标为[i,j](i<j)的子串是否为回文串。
2.dp数组递推公式
如果字符i等于字符j,则考虑子串[i+1,j-1],即dp[i+1][j-1]的布尔值,另外,由于i+1需要不大于j-1,因此我们需哟爱单独处理j和i的距离小于2的情况。当j-i小于2且i和j对应字符相等时,有两种情况,a和aa,这两种都是回文串,都需要统计。
因此当i和j对应的字符相等时,dp[i][j]为true的情况有:i-j<2和dp[i+1][j-1]==true。
if(s.charAt(i) == s.charAt(j)) {if (j - i <= 1) {// 子串长度小于等于1,即a或aa的情况res++;dp[i][j] = true;}else if (dp[i+1][j-1]) {// 子串长度大于1,且i+1到j-1的子串是回文串res++;dp[i][j] = true;}}
3.dp数组初始化
对于dp数组,初始化所有值都为false,保证在统计前未匹配上
4.确认遍历顺序
根据dp推导公式,dp[i][j]收到dp[i+1][j-1]的影响,即dp数组左下方的影响,因此行需要逆序遍历,列需要正序遍历,另外,由于我们统计的时[i,j]的子串,因此要保证i不大于j。因此外层逆序遍历i,内层正序遍历j,j初始化为i(i可以等于j)
在填充的时候,保证只填充数组的右上部分
for (int i = s.length()-1; i >= 0; i--) {for (int j = i; j < s.length(); j++) {}}
5.举例推导dp数组
输入:"aaa",dp[i][j]状态如下:
整体代码:
class Solution {public int countSubstrings(String s) {boolean[][] dp = new boolean[s.length()][s.length()];int res = 0;for (int i = s.length()-1; i >= 0; i--) {for (int j = i; j < s.length(); j++) {if(s.charAt(i) == s.charAt(j)) {if (j - i <= 1) {// 子串长度小于等于1,即a或aa的情况res++;dp[i][j] = true;}else if (dp[i+1][j-1]) {// 子串长度大于1,且i+1到j-1的子串是回文串res++;dp[i][j] = true;}}}}return res;}
}
力扣516.最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
解题思路:
动规五部曲:
1.dp数组定义和含义
定义二维dp数组,大小为字符串s长度,dp[i][j]表示子串[i,j](i<j)的最长回文序列长度
2.dp数组递推公式
当字符i等于字符j时,dp[i][j]受到去掉两个字符后的子串的影响,即dp[i][j] = dp[i+1][j-1]+2
当字符i不等于字符j时,考虑去掉字符i或字符j后的子串的最长回文子序列长度,即dp[i+1][j]和dp[i][j-1],取两者的最大值,故dp[i][j] = max{dp[i+1][j],dp[i][j-1]}
if (s.charAt(i) == s.charAt(j)) {// 字符相等,去掉两个字符子串的回子序列长度+2dp[i][j] = dp[i+1][j-1] + 2;}else {// 字符不相等,看去掉首尾字符得到的回文子序列哪个大dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);}
3.dp数组初始化
对于单个字符的情况,其初始值应该为1,也可以后续进行初始化,只要保证在使用到之前进行了初始化即可
// dp数组初始化,单字符的情况,也可以后续遍历时赋值for (int i = 0; i < s.length(); i++) {dp[i][i] = 1;}
4.确认遍历顺序
本题的dp值与左方、下方和左下方的dp值有关,因此采用外层逆序遍历i,内层正序遍历j的方式,注意,在内层如果之前初始化了dp[i][j],则j从i+1开始处理,否则从j=i开始处理,并对i=j的情况进行单独赋值。
for (int i = s.length()-1; i >= 0; i--) {for (int j = i+1; j < s.length(); j++) {}}
5.举例推导dp数组
s:"cbbd" 为例,dp数组状态如图:
整体代码:
class Solution {public int longestPalindromeSubseq(String s) {int[][] dp = new int[s.length()][s.length()];// dp数组初始化,单字符的情况,也可以后续遍历时赋值for (int i = 0; i < s.length(); i++) {dp[i][i] = 1;}for (int i = s.length()-1; i >= 0; i--) {for (int j = i+1; j < s.length(); j++) {if (s.charAt(i) == s.charAt(j)) {// 字符相等,去掉两个字符子串的回子序列长度+2dp[i][j] = dp[i+1][j-1] + 2;}else {// 字符不相等,看去掉首尾字符得到的回文子序列哪个大dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);}}}return dp[0][s.length()-1];}
}
单调栈部分
力扣793.每日温度
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
解法一:暴力解法(超时)
一个简单的方法就是遍历每个元素,然后遍历它右侧的所有温度,找到第一个比它高的温度则记录下来,反之则不记录。
class Solution {public int[] dailyTemperatures(int[] temperatures) {int[] result = new int[temperatures.length];for (int i = 0; i < temperatures.length; i++) {// 遍历每一天for (int j = i + 1; j < temperatures.length; j++) {// 遍历后面的每一天,找到第一个温度高的时候,记录结果并跳出循环if (temperatures[j] > temperatures[i]) {result[i] = j-i;break;}}}return result;}
}
解法二:单调栈
创建一个栈,存储元素的下标,保证栈中的元素从栈顶到栈顶是递增的,即栈顶元素比它下面的元素大。
对于一个访问元素,它和栈顶元素的关系有:
- 当前元素比栈顶元素大,则将栈顶元素出栈,然后记录出栈元素的结果,为result[st.peek()]=i-st.pop(),即栈顶元素的对应的结果为当前元素下标减去栈顶元素下标。注意这里要循环出栈,直到栈为空或者栈顶元素大于当前元素。
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {// 单调栈非空,且访问元素大于栈顶元素时,保持持续出栈int k = stack.pop();// 栈顶元素下标result[k] = i-k;// 记录结果}
- 当前元素小于等于栈顶元素或者栈为空时,直接将元素下标入栈即可
// 当前元素不大于栈顶元素或栈空时,当前元素下标入栈stack.push(i);
整体代码:
class Solution {public int[] dailyTemperatures(int[] temperatures) {Stack<Integer> stack = new Stack<>();// 单调栈,存储下标int[] result = new int[temperatures.length];// 结果集stack.push(0);// 存入第一个元素下标for (int i = 1; i < temperatures.length; i++){while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {// 单调栈非空,且访问元素大于栈顶元素时,保持持续出栈int k = stack.pop();// 栈顶元素下标result[k] = i-k;// 记录结果}// 当前元素不大于栈顶元素或栈空时,当前元素下标入栈stack.push(i);}return result;}
}
力扣496.下一个更大的元素1
nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
大的元素。
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。
对于每个 0 <= i < nums1.length
,找出满足 nums1[i] == nums2[j]
的下标 j
,并且在 nums2
确定 nums2[j]
的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1
。
返回一个长度为 nums1.length
的数组 ans
作为答案,满足 ans[i]
是如上所述的 下一个更大元素 。
解法一:暴力算法
解题思路:
注意本题要找的是nums1的元素在nums2中对应位置后面的元素是否有下一个更大的元素,不是在nums2中所有元素,因此暴力算法的思路为:
遍历nums1,内部遍历nums2,找到nums1元素对应在nums2中的下标位置,然后从这个位置起遍历nums2的剩余元素,找到下一个更大的元素。
整体代码:
class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {int[] result = new int[nums1.length];for (int i = 0; i < nums1.length; i++) {// 遍历nums1的元素int left = 0;for (int j = 0; j < nums2.length; j++) {// 找nums2中的nums1[i]对应的元素if (nums1[i] == nums2[j]){left = j;break;}}result[i] = -1;// 初始化// 找nums2中nums1[i]对应的元素后面的第一个比它大的元素left++;while (left < nums2.length) {if (nums2[left] > nums1[i]) {// 找到第一个比nums1[i]大的元素,记录结果并跳出循环result[i] = nums2[left];break;}left++;}}return result;}
}
解法二:单调栈
解题思路:
用一个Map存储nums1中元素和下标的映射,用于后续找到对应元素在nums1中的下标值。
单调栈只用来存储nums2的元素下标(也可以直接存储元素值),当nums2中的元素大于栈顶元素时,在map映射中找对应元素是否存在,不存在则直接将栈顶元素出栈,否则找到nums1对应元素的下标,然后对结果进行赋值,直到栈为空或者nums2当前元素不大于栈顶元素。
当nums2的当前元素小于等于栈顶元素时,将元素入栈。
整体代码:
class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {Map<Integer, Integer> map = new HashMap<>();// nums1元素和下标的映射int[] result = new int[nums1.length];// 结果集只记录nums1即可Arrays.fill(result, -1);// 初始化为-1,表示不存在,存在则后续会覆盖for (int i = 0; i < nums1.length; i++) {map.put(nums1[i], i);// 记录nums1元素和下标映射}Stack<Integer> stack = new Stack<>();// 单调栈,记录nums2的元素下标stack.push(0);// 遍历nums2for (int i = 1; i < nums2.length; i++) {if (nums2[i] <= nums2[stack.peek()]) {// 当前元素小于等于栈顶元素,当前元素入栈stack.push(i);}else {// 当前元素大于栈顶元素,栈顶元素出栈,直到栈为空或者当前元素大于栈顶元素while (!stack.isEmpty() && nums2[i] > nums2[stack.peek()]) {int k = stack.pop();// 栈顶元素出栈if (map.containsKey(nums2[k])) {// 判断nums2[k]对应的元素在nums1中是否存在int index = map.get(nums2[k]);// nums2[k]对应的元素在nums1中的下标result[index] = nums2[i];// 记录结果}}stack.push(i);}}return result;}
}
力扣503.下一个更大元素2
给定一个循环数组 nums
( nums[nums.length - 1]
的下一个元素是 nums[0]
),返回 nums
中每个元素的 下一个更大元素 。
数字 x
的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1
。
解题思路:
还是采用单调栈找右侧的第一个比它大的元素,但这里与上一题不同的是,需要对右侧未成功赋值的元素进行处理,对数组进行循环,他们的前方可能存在比它大的元素。
两次遍历数组:
在第一次遍历数组完成后,我们可以保证当元素右侧存在比它大的元素时,它的结果能够被赋值,此时,右边没有更大值的元素还在单调栈中堆积着,此时进行第二遍元素遍历。
// 第一遍遍历数组for (int i = 1; i < nums.length; i++) {while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {int k = stack.pop();result[k] = nums[i];}stack.push(i);}
第二遍元素遍历能够找到元素左边是否有比它大的值,当两次遍历完成后都没有对结果赋值的话,表示它是最大值,不需要赋值。
// 第二次遍历数组for (int i = 0; i < nums.length; i++) {while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {int k = stack.pop();result[k] = nums[i];}stack.push(i);}
整体代码:
class Solution {public int[] nextGreaterElements(int[] nums) {int[] result = new int[nums.length];Arrays.fill(result, -1);Stack<Integer> stack = new Stack<>();stack.push(0);for (int i = 1; i < nums.length; i++) {while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {int k = stack.pop();result[k] = nums[i];}stack.push(i);}for (int i = 0; i < nums.length; i++) {while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {int k = stack.pop();result[k] = nums[i];}stack.push(i);}return result;}
}
上面的代码可以进一步简化,直接for循环遍历2被数组大小,然后下标对数组大小取模极为当前访问的元素下表,能够保证访问元素两次完成元素赋值。
简化后的代码:
class Solution {public int[] nextGreaterElements(int[] nums) {int[] result = new int[nums.length];Arrays.fill(result, -1);Stack<Integer> stack = new Stack<>();stack.push(0);int n = nums.length;for (int i = 1; i < 2*n; i++) {while (!stack.isEmpty() && nums[i%n] > nums[stack.peek()]) {int k = stack.pop();result[k] = nums[i%n];}stack.push(i%n);}return result;}
}
力扣42.接雨水
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
解法一:暴力破解(超时)
我们计算每一列能够存储的雨水数,求和极为总雨水数。
对于某一列,它的雨水数等于左右两侧比它本身柱子高的柱子中,更矮的一个柱子与当前柱子之差
- 往左遍历,找到左侧比当前柱子高的柱子
- 往右遍历,找到右侧比当前柱子高的柱子
- 如果有一侧没有比当前柱子高的柱子,那么说明当前柱子所在位置不存储雨水
- 当左右两侧找到比当前柱子高的最大柱子时,取两者间更矮的柱子高度
- 当前柱子位置的雨水数=更矮的柱子高度-当前柱子高度
可以理解为把左右两侧最高的柱子移到当前柱子两侧,此时存储的雨水数极为当前柱子雨水数
整体代码:
class Solution {public int trap(int[] height) {int result = 0;// 总雨水量for (int i = 1; i < height.length-1; i++) {// 遍历每一列,从第二列开始,到倒数第二列结束// 第一列没有左边柱子,第二列没有右边柱子int left = i-1, right = i+1;// 左右柱子下标int leftMaxHeight =height[i], rightMaxHeight = height[i];// 左右最高的柱子高度while (left >= 0) {// 找左边柱子比当前柱子更高的最高的柱子leftMaxHeight = Math.max(leftMaxHeight, height[left]);left--;}while (right < height.length) {// 找右边柱子比当前柱子更高的最高的柱子rightMaxHeight = Math.max(rightMaxHeight, height[right]);right++;}if (leftMaxHeight != height[i] && rightMaxHeight != height[i]) {// 当前柱子两边都有比它高的柱子,可以接雨水,计算雨水量// 雨水量 = 当前柱子两边最高的柱子中更低的一个高度 - 当前柱子的高度result += Math.min(leftMaxHeight, rightMaxHeight) - height[i];}// 当前柱子两边都没有比它高的柱子,无法接雨水}return result;}
}
解法二:双指针+dp优化
在暴力解法中,每一个格子的雨水数量为它左侧和右侧最大的柱子高度中的更小的一个与它本身的柱子高度差(大于0),但每次都是通过左右遍历去找到最大值,每此都需要再次遍历数组,有许多的重复步骤。
对上面的方法进行改进,我们先将每个柱子左侧和右侧的最高柱子求出来,保存到数组中,后续只需要取对应的值即可。
这里,左侧最高柱子为:当前柱子高度和前一个格子的最高柱子高度两者的最大值(前面可能没有比当前柱子更高的,此时我们用本身高度代替,防止后面出现负值),此处就有dp的体现。
// 创建两个数组,分别记录每个柱子左边和右边最高的柱子高度int[] leftMaxHeight = new int[height.length];int[] rightMaxHeight = new int[height.length];leftMaxHeight[0] = height[0];for (int i = 1; i < height.length; i++) {// 当前柱子左边最高的柱子高度为当前柱子高度和左边柱子当前最高高度中较大的一个leftMaxHeight[i] = Math.max(leftMaxHeight[i-1], height[i]);}rightMaxHeight[height.length-1] = height[height.length-1];for (int j = height.length-2; j >= 0; j--) {// 当前柱子右边最高的柱子高度为当前柱子高度和右边柱子当前最高高度中较大的一个rightMaxHeight[j] = Math.max(rightMaxHeight[j+1], height[j]);}
后续遍历的时候,只需要在两个数组中取对应值来使用即可
int result = 0;for (int i = 1; i < height.length-1; i++) {// 当前柱子两边最高的柱子中较低的一个高度减去当前柱子的高度,就是当前柱子能接的雨水量result += Math.min(leftMaxHeight[i], rightMaxHeight[i]) - height[i];}
整体代码:
class Solution {public int trap(int[] height) {// 创建两个数组,分别记录每个柱子左边和右边最高的柱子高度int[] leftMaxHeight = new int[height.length];int[] rightMaxHeight = new int[height.length];leftMaxHeight[0] = height[0];for (int i = 1; i < height.length; i++) {// 当前柱子左边最高的柱子高度为当前柱子高度和左边柱子当前最高高度中较大的一个leftMaxHeight[i] = Math.max(leftMaxHeight[i-1], height[i]);}rightMaxHeight[height.length-1] = height[height.length-1];for (int j = height.length-2; j >= 0; j--) {// 当前柱子右边最高的柱子高度为当前柱子高度和右边柱子当前最高高度中较大的一个rightMaxHeight[j] = Math.max(rightMaxHeight[j+1], height[j]);}int result = 0;for (int i = 1; i < height.length-1; i++) {// 当前柱子两边最高的柱子中较低的一个高度减去当前柱子的高度,就是当前柱子能接的雨水量result += Math.min(leftMaxHeight[i], rightMaxHeight[i]) - height[i];}return result;}
}
解法三:单调栈
单调栈的方式是按照行计算雨水数,这很抽象,简单的理解就是,凹槽中的雨水量等于宽*高,
其中高为:左边柱子和右边柱子的最小值减去底的高度,宽为:右边柱子下标减去左边柱子下表-1
对于单调栈,我们采用栈底到栈顶元素大小为从大到小的方式,则对于元素与栈顶元素处理为:
- 当前元素高度小于栈顶元素:则直接将该元素下标入栈
if (!stack.isEmpty() && height[i] < height[stack.peek()]){// 当前元素高度小于栈顶元素,直接入栈stack.push(i);}
- 当前元素高度等于栈顶元素:此处处理和前面不一样,我们需要将栈顶元素入栈,然后将该元素下表入栈,(这里是因为元素高度相同时,我们只需要保留右侧元素用于后续作为左侧柱子即可)
else if (!stack.isEmpty() && height[i] == height[stack.peek()]){// 当前元素高度等于栈顶元素,将栈顶元素出栈,当前元素入栈stack.pop();stack.push(i);}
- 当前元素高度大于栈顶元素:此时就是收集雨水的时候,将栈顶元素出栈作为低的高度,然后在栈非空的情况下,再次出出栈元素作为左边的柱子,根据这三个值计算收集的雨水量。注意,这里同样需要进行循环处理,直到当前元素高度不大于栈顶元素,将当前元素下标入栈
else {while (!stack.isEmpty() && height[i] > height[stack.peek()]) {int bottom = stack.pop();// 底柱子下标if (!stack.isEmpty()) {// 栈非空,找左边的柱子int left = stack.peek();// 高为当前柱子和左边柱子的最小值减去底的高度int h = Math.min(height[left], height[i]) - height[bottom];// 宽为当前柱子下标减去左边柱子下标减一(只有中间能用)int w = i-left-1;result += h*w;// 雨水量为宽乘高}}// 栈为空或当前元素不大于栈顶元素了,当前元素入栈stack.push(i);}
整体代码:
class Solution {public int trap(int[] height) {Stack<Integer> stack = new Stack<>();// 单调栈,存储柱子下标,保证柱子从栈底到栈顶为从大到小int result = 0;stack.push(0);for (int i = 1; i < height.length; i++) {if (!stack.isEmpty() && height[i] < height[stack.peek()]){// 当前元素高度小于栈顶元素,直接入栈stack.push(i);}else if (!stack.isEmpty() && height[i] == height[stack.peek()]){// 当前元素高度等于栈顶元素,将栈顶元素出栈,当前元素入栈stack.pop();stack.push(i);}else {while (!stack.isEmpty() && height[i] > height[stack.peek()]) {int bottom = stack.pop();// 底柱子下标if (!stack.isEmpty()) {// 栈非空,找左边的柱子int left = stack.peek();// 高为当前柱子和左边柱子的最小值减去底的高度int h = Math.min(height[left], height[i]) - height[bottom];// 宽为当前柱子下标减去左边柱子下标减一(只有中间能用)int w = i-left-1;result += h*w;// 雨水量为宽乘高}}// 栈为空或当前元素不大于栈顶元素了,当前元素入栈stack.push(i);}}return result;}
}
力扣84.柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解法一:暴力解法(超时)
对于一个柱子,它能带来的最大矩形的宽是固定的,即为它的高度,矩形的高度为它两侧的不低于它的柱子个数,因此我们只需要找到它两侧第一个小于它高度的柱子,这之间即为矩形高度。
因此,对于第i个元素,我们往左右两边遍历找到第一个高度小于它的柱子,即可得到矩形的宽,当前柱子能提供的最大矩形即为宽*高。
整体代码:
class Solution {public int largestRectangleArea(int[] heights) {int result =0;for (int i = 0; i < heights.length; i++) {int left = i, right = i;// 找左右小于当前高度柱子的下表// 左for (; left >= 0; left--) {// 找到了左边更矮的柱子if (heights[left] < heights[i]) break;}// 右for (; right < heights.length; right++) {// 找到了右边更矮的柱子if (heights[right] < heights[i]) break;}// 高度为当前高度,宽为左右下标差值减一int w = right - left -1;int h = heights[i];result = Math.max(result, w*h);}return result;}
}
解法二:双指针改进暴力法
同样可以使用双指针对暴力法进行改进。
用两个数组分别存储第i个元素左边和后边第一个小于其高度的柱子标,找不到则左边下标为-1,右边下标为数组大小。
左边第一个矮的柱子数组:
遍历整个数组,然后用while循环往左找更小的柱子,这里当t的高度小于i时,则找到了结果,否则的话,由于i的高度不大于t,则t的更小柱子下标到t之间的柱子的高度要比i大,因此可以直接跳过
// 往左遍历leftMinIndex[0] = -1;// 左边没有更小的for (int i = 1; i < heights.length; i++) {int t = i-1;// 往左寻找while (t >= 0 && heights[t] >= heights[i]){t = leftMinIndex[t];// 当前i柱子比t柱子还小,则t的更小柱子到t之间的一定比i更高(或者相等),因此可以剪枝}leftMinIndex[i] = t;// t为-1或t为左边第一个小于当前高度柱子的下标}
右边第一个矮的柱子数组:和左侧数组是一样的
// 往右遍历rightMinIndex[heights.length-1] = heights.length;// 右边没有更小的for (int j = heights.length - 2; j >= 0; j--) {int t = j+1;// 往右边找while (t < heights.length && heights[t] >= heights[j]) {t = rightMinIndex[t];// 当前j柱子比t柱子还小,则t到t的更小柱子之间的一定比j更高(或者相等),因此可以剪枝}rightMinIndex[j] = t;// t为heights.length或t为右边第一个小于当前高度柱子的下标}
整体代码:
class Solution {public int largestRectangleArea(int[] heights) {int[] leftMinIndex = new int[heights.length];// 每个位置左边第一个小于当前高度柱子的下标int[] rightMinIndex = new int[heights.length];// 每个位置右边第一个小于当前高度柱子的下标// 往左遍历leftMinIndex[0] = -1;// 左边没有更小的for (int i = 1; i < heights.length; i++) {int t = i-1;// 往左寻找while (t >= 0 && heights[t] >= heights[i]){t = leftMinIndex[t];// 当前i柱子比t柱子还小,则t的更小柱子到t之间的一定比i更高(或者相等),因此可以剪枝}leftMinIndex[i] = t;// t为-1或t为左边第一个小于当前高度柱子的下标}// 往右遍历rightMinIndex[heights.length-1] = heights.length;// 右边没有更小的for (int j = heights.length - 2; j >= 0; j--) {int t = j+1;// 往右边找while (t < heights.length && heights[t] >= heights[j]) {t = rightMinIndex[t];// 当前j柱子比t柱子还小,则t到t的更小柱子之间的一定比j更高(或者相等),因此可以剪枝}rightMinIndex[j] = t;// t为heights.length或t为右边第一个小于当前高度柱子的下标}// 正式遍历寻找结果int result = 0;for (int i = 0; i < heights.length; i++) {int w = rightMinIndex[i] - leftMinIndex[i] - 1;// 宽度int h = heights[i];// 高度result = Math.max(result, w*h);}return result;}
}
解法三:单调栈
本题和42接雨水的思路很像,接雨水是找一个柱子左右两侧第一个更大的柱子,本题找的是左右两侧第一个更小的柱子,找到两个这种柱子left和right后,当前节点能得到的最大矩形为:高*宽,其中高为当前节点柱子的高,宽为right-left-1。
本题的单调栈找的是第一个更小的,因此单调栈内的元素保持栈底到栈顶元素递增的顺序。
第i个节点与栈顶元素的大小关系有三种:
- 第i个元素高度大于栈顶元素:此时直接将第i个元素下标入栈,保证单调栈递增
if (!stack.isEmpty() && heights[i] > heights[stack.peek()]){// 当前元素高度大于栈顶元素,直接入栈stack.push(i);}
- 第i个元素高度等于栈顶元素:此时将栈顶元素出栈,然后将第i个元素下标入栈
else if (!stack.isEmpty() && heights[i] == heights[stack.peek()]){ // 当前元素高度等于栈顶元素,将栈顶元素出栈,当前元素入栈stack.pop();stack.push(i);}
- 第i个元素高度小于栈顶元素:此时就是进行计算结果的时候。当栈中存在元素时,将栈顶元素cur出栈,对此元素进行结果计算。当cur出栈后,需要考虑栈是否为空,当栈为空时,表示左边没有更小的元素了,此时的左边界下标为-1。当栈不为空时,此时的栈顶元素即为cur的左边第一小的值,i为右边第一个小的值,从而计算第i个节点能够得到的最大矩形面积。
else {while (!stack.isEmpty() && heights[i] < heights[stack.peek()]){int cur = stack.pop();if (!stack.isEmpty()) {int w = i - stack.peek() - 1;int h = heights[cur];result = Math.max(result, w * h);}else {// 左边没有更小的柱子,宽度为当前柱子下标(左侧一个虚拟柱子,下标为-1)int w = i;int h = heights[cur];result = Math.max(result, w * h);}}stack.push(i);}
注意:本题要考虑最后一个元素的特殊情况,最后一个元素可能是一直递增来的,因此,最后的递增序列没有计算。因此在遍历完数组后,需要对栈中元素进行处理,这里右边的边界用数组大小(类似于虚拟一个高度为0的柱子,用于作为右边第一个小的元素)。即
// 处理最后一个元素的情况,右边没有更小的柱子了,设置右侧虚拟柱子下标为数组大小while (!stack.isEmpty()){int cur = stack.pop();if (!stack.isEmpty()) {int w = heights.length - stack.peek() - 1;int h = heights[cur];result = Math.max(result, w * h);}else {int w = heights.length;int h = heights[cur];result = Math.max(result, w * h);}}
整体代码:
class Solution {public int largestRectangleArea(int[] heights) {Stack<Integer> stack = new Stack<>();// 单调栈,存储柱子下标,保证柱子从栈底到栈顶为从小到大(找左边的第一个更小值)int result = 0;stack.push(0);for (int i = 1; i < heights.length; i++) {if (!stack.isEmpty() && heights[i] > heights[stack.peek()]){// 当前元素高度大于栈顶元素,直接入栈stack.push(i);}else if (!stack.isEmpty() && heights[i] == heights[stack.peek()]){// 当前元素高度等于栈顶元素,将栈顶元素出栈,当前元素入栈stack.pop();stack.push(i);}else {while (!stack.isEmpty() && heights[i] < heights[stack.peek()]){int cur = stack.pop();if (!stack.isEmpty()) {int w = i - stack.peek() - 1;int h = heights[cur];result = Math.max(result, w * h);}else {// 左边没有更小的柱子,宽度为当前柱子下标(左侧一个虚拟柱子,下标为-1)int w = i;int h = heights[cur];result = Math.max(result, w * h);}}stack.push(i);}}// 处理最后一个元素的情况,右边没有更小的柱子了,设置右侧虚拟柱子下标为数组大小while (!stack.isEmpty()){int cur = stack.pop();if (!stack.isEmpty()) {int w = heights.length - stack.peek() - 1;int h = heights[cur];result = Math.max(result, w * h);}else {int w = heights.length;int h = heights[cur];result = Math.max(result, w * h);}}return result;}
}
相关文章:

刷题记录10
力扣72. 编辑距离 给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作: 插入一个字符删除一个字符替换一个字符 解题思路: 本题与583. 两个字符串的删除操作其实是一样…...

数学软件Matlab下载|支持Win+Mac网盘资源分享
如大家所了解的,Matlab与Maple、Mathematica并称为三大数学软件。Matlab应用广泛,常被用于数据分析、无线通信、深度学习、图像处理与计算机视觉、信号处理、量化金融与风险管理、机器人,控制系统等领域。 Matlab将数值分析、矩阵计算、科学…...

5G学习笔记之BWP
我们只会经历一种人生,我们选择的人生。 参考:《5G NR标准》、《5G无线系统指南:如微见著,赋能数字化时代》 目录 1. 概述2. BWP频域位置3. 初始与专用BWP4. 默认BWP5. 切换BWP 1. 概述 在LTE的设计中,默认所有终端均能处理最大2…...
Spark 介绍
Spark 架构 Spark 是一个基于内存计算的大数据处理框架,相比 Hadoop 的 MapReduce,它能够提供 更高效的迭代计算 和 流式计算能力。Spark 采用 主从架构(Master-Slave),主要包括 Driver、Cluster Manager、Worker、Ex…...
mac Homebrew安装、更新失败
我这边使用brew安装git-lfs 一直报这个错: curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL更新brew update也是报这个错误。最后使用使用大佬提供的脚本进行操作: /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/mast…...

【实战 ES】实战 Elasticsearch:快速上手与深度实践-2.2.3案例:电商订单日志每秒10万条写入优化
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 Elasticsearch批量写入性能调优实战:2.2.3 案例:电商订单日志每秒10万条写入优化1. 原始架构与瓶颈分析1.1 初始集群配置1.2 性能瓶颈定位 2. 全链路…...
http的post请求不走http的整个缓存策略吗?
http的post请求不走http的整个缓存策略吗? DeepSeek R1 思考 33 秒 思考过程 嗯,用户问的是HTTP的POST请求是否不遵循整个缓存策略。我需要先回忆一下HTTP缓存的基本机制,然后考虑不同请求方法的缓存处理。 首先,HTTP缓存通常涉…...
c++ 预处理器和iostream 文件
在C中,预处理器和<iostream>头文件各自扮演关键角色,二者协同工作以实现程序的输入输出功能。以下是它们的详细关系和作用: 1. 预处理器的作用 处理预处理指令:预处理器在编译前执行,处理所有以#开头的指令&…...

【前端】前端设计中的响应式设计详解
文章目录 前言一、响应式设计的定义与作用二、响应式设计的原则三、响应式设计的实现四、响应式设计的最佳实践总结 前言 在当今数字化时代,网站和应用程序需要适应各种设备,从桌面电脑到平板电脑和手机。响应式设计应运而生,成为一种可以适…...

探秘基带算法:从原理到5G时代的通信变革【四】Polar 编解码(二)
文章目录 2.3.3 极化编码巴氏参数与信道可靠性比特混合生成矩阵编码举例 2.3.4 极化译码最小单元译码串行抵消译码(SC译码)算法SCL译码算法 2.3.5 总结**Polar 码的优势****Polar 码的主要问题****Polar 码的应用前景** 2.3.6 **参考文档** 本博客为系列…...

打开 Windows Docker Desktop 出现 Docker Engine Stopped 问题
一、关联文章: 1、Docker Desktop 安装使用教程 2、家庭版 Windows 安装 Docker 没有 Hyper-V 问题 3、安装 Windows Docker Desktop - WSL问题 二、问题解析 打开 Docker Desktop 出现问题,如下: Docker Engine Stopped : Docker引擎停止三、解决方法 1、检查服务是否…...
6.人工智能与机器学习
一、人工智能基本原理 1. 人工智能(AI)定义与范畴 核心目标:模拟人类智能行为(如推理、学习、决策)分类: 弱人工智能(Narrow AI):专精单一任务(如AlphaGo、…...
RabbitMQ怎么实现延时支付?
一、使用“死信队列”消息过期时间 1、原理: 设置消息”存活时间“,如果没有被及时消费,就会被丢弃到一个”死信队列“,然后消费者监听这个死信队列处理消息 2、步骤: 2.1、创建两个队列: 2.1.1、普通队…...
vite-vue3使用web-worker应用指南和报错解决
主线程:初始化worker和监听子线程的消息 let worker: any; const salesConfigData ref<any[]>([]); // 显示非上架 const showNotList ref(false);// /src/views/ceshi/salesConfig/worker.js worker new Worker(new URL("/src/views/ceshi/salesConf…...

校园快递助手小程序毕业系统设计
系统功能介绍 管理员端 1)登录:输入账号密码进行登录 2)用户管理:查看编辑添加删除 学生信息 3)寄件包裹管理:查看所有的包裹信息,及物流信息 4)待取件信息:查看已到达的…...

python量化交易——金融数据管理最佳实践——使用qteasy管理本地数据源
文章目录 统一定义的金融历史数据表最重要的数据表数据表的定义交易日历表的定义:交易日历表: trade_calendar qteasy是一个功能全面且易用的量化交易策略框架, Github地址在这里。使用它,能轻松地获取历史数据,创建交易策略并完…...
BIO、NIO、AIO、Netty从简单理解到使用
Java编程中BIO、NIO、AIO是三种不同的I/O(输入/输出)模型,它们代表了不同的I/O处理方式。 Netty就是基于Java的NIO(New Input/Output)类库编写的一个高性能、异步事件驱动的网络应用程序框架,用于快速开发可…...

计算机毕业设计SpringBoot+Vue.js工厂车间管理系统源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
一、图形图像的基本概念
文章目录 一、分辨率概念二、图形图像的区别三、位图和矢量图的区别 一、分辨率概念 图形显示计数中的分辨率概念有三种,即屏幕分辨率、显示分辨率和显卡分辨率。它们既有区别又有着密切的联系,对图形显示的处理有极大的影响。 1.屏幕分辨率 显示器分辨…...
前端跨域问题初探:理解跨域及其解决方案概览
在当今的Web开发中,跨域问题是一个常见且棘手的挑战 随着前端技术的不断进步,越来越多的应用需要从不同的域名、协议或端口获取资源 然而,浏览器的同源策略(Same-Origin Policy)限制了这种跨域请求,以确保…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
Caliper 配置文件解析:fisco-bcos.json
config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

淘宝扭蛋机小程序系统开发:打造互动性强的购物平台
淘宝扭蛋机小程序系统的开发,旨在打造一个互动性强的购物平台,让用户在购物的同时,能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机,实现旋转、抽拉等动作,增…...

Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合
作者:来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布,Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明,Elastic 作为 …...
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一:HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二:Floyd 快慢指针法(…...