一、两个字符串的删除操作
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = "sea", word2 = "eat" 输出: 2 解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"
思路:
求出两个字符串的最大公共子序列的长度,此时最长公共子序列就是他们相同的地方,把不相同的地方删除掉就好了。最小操作次数=两个字符串的长度-最大公共子序列长度*2;
代码和求最长公共子序列类似;
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
//定义dp数组
int[][] dp=new int[len1+1][len2+1];
//遍历dp数组
int max=0;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
max=Math.max(max,dp[i][j]);
}
}
return len1-max+len2-max;
}
}
另一种思路:
1.dp[i][j]:以i-1为结尾的字符串word1,和以j-1为结尾的字符串word2,要想达到相等,所需要删除元素的最少次数。
2.递推公式:if(s.charAt(i)==t.chartAt(j))dp[i][j]=dp[i-1][j-1];
else{
dp[i][j]=Math.min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+2);
如果不相等就分三种情况,删s串/删t串/删s,t串
}
3.初始化:
dp[i][0]=i;dp[0][j]=j;
4.遍历:从1 1 开始
代码:
class Solution {
public int minDistance(String word1, String word2) {
int[][] dp = new int[word1.length() + 1][word2.length() + 1];
for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;
for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;
for (int i = 1; i < word1.length() + 1; i++) {
for (int j = 1; j < word2.length() + 1; 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 - 1] + 2,
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
}
}
}
return dp[word1.length()][word2.length()];
}
}
二、编辑距离
思路:
1.dp[i][j]:以i-1为结尾的字符串和以j-1为结尾的字符串 变成一样的所需要的最小操作数
2.递推公式:
当s.charAt(i-1)==s.charAt(j-1),dp[i][j]=dp[i-1][j-1];解释:当字符相等的时候,不需要做任何操作,将状态转移到下一位上就可以。
当s.charAt(i-1)!=s.charAt(j-1):就要进行删除/添加/替换操作。删除操作和添加操作是对称的操作。
1.通过删除字符的操作次数和通过添加字符的操作次数是相等的。因此删除和添加可以看成一种操作。dp[i][j]=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
2.替换操作:当两个元素不一样的时候,就要替换成相同的字符。相同之后就要比较各自下一位的字符,因此:dp[i][j]=dp[i-1][j-1]+1;
3.初始化:dp[0][j]=j;dp[i][0]=i;
4.遍历顺序:从11开始
代码:
class Solution {
public int minDistance(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
//定义dp数组
//dp数组的含义是:以i-1结尾的字符串和j-1结尾的字符串 要相等 所需要的最小操作数
int[][] dp=new int[len1+1][len2+1];
//初始化dp
for(int i=0;i<=len1;i++)dp[i][0]=i;
for(int j=0;j<=len2;j++)dp[0][j]=j;
//遍历dp数组
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
//如果不相等的话 就要进行删除 增加 替换操作
dp[i][j]=Math.min(Math.min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+1);
}
}
}
return dp[len1][len2];
}
}
三、回文子串(动态规划/双指针)
动态规划:
1.dp[i][j],从i到j这个范围里的字符串是否是回文子串。类型是boolean,如果是就true,不是就false
2.递推公式: 当s.chartAt(i)==s.charAt(j)相等时,i/j的情况有三种
2.1 i==j,就一个字符a,那么它一定是
2.2 i+1=j 中间隔一个字符,比如说aa,那么它也一定是
2.3 中间隔了n个字符;abba/abca,此时就要判断dp[i+1][j-1](向里面缩小范围)是否是回文子串, 如果是的话,那么i,j也是;如果不是的话,那么i,j也不是
3.初始化,因为刚开始不知道是否是回文子串,都初始化为false
4.遍历顺序(很重要):因为判断dp[i][j]的时候要根据dp[i+1][j-1]的boolean值。因此我们i必须从下往上遍历,j必须从左往右遍历
for(int i=size-1;i>=0;i--)
for(int j=i;j<size;j++
代码:
class Solution {
public int countSubstrings(String s) {
int size=s.length();
//定义dp数组
boolean[][] dp=new boolean[size][size];
//遍历dp数组
int result=0;
for(int i=size-1;i>=0;i--){
for(int j=i;j<size;j++){
if(s.charAt(i)==s.charAt(j)){
if(j-i<=1){
result++;
dp[i][j]=true;
}else{
if(dp[i+1][j-1]==true){
result++;
dp[i][j]=true;
}
}
}
}
}
return result;
}
}
双指针:
四、最长回文子串(回文子串可以是不连续的)
思路:
1.dp[i][j],下标从i->j这个范围中,回文子串的最大长度
2.dp[i][j]=dp[i+1][j-1]+2(相等的情况下.s.charAt(i)==s.charAt(j));因为相等,所以又多了两个字符。在谁的基础上多了两个字符?在dp[i+1][j-1]的基础上。
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]) (不相等的情况下,就要移动i,j)
3.初始化:dp[i][i]=1;因为一个字符也是一个回文子串,长度为1
4.遍历顺序,i依然是从下往上,j依然是从左往右
代码:
class Solution {
public int longestPalindromeSubseq(String s) {
int size=s.length();
int[][] dp=new int[size][size];
//初始化
for(int i=0;i<size;i++)dp[i][i]=1;
//遍历dp数组
for(int i=size-1;i>=0;i--){
for(int j=i+1;j<size;j++){
if(s.charAt(i)==s.charAt(j)){
dp[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][size-1];
}
}
五、单调栈应用之每日温度
单调栈:通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,这个时候就使用单调栈。时间复杂度为O(n)。
要求右边元素第一个比自己大的,那么就用单调自增栈(从栈顶到栈尾是递增的)。原因:如果遇到比自己大的,就记录跟该元素的距离;如果遇到比自己小的,直接push进去
要求右边元素第一个比自己小的,那么用单调递减栈(从栈顶到栈尾是递减的)。原因:如果遇到比自己大的,就直接push进去。如果遇到比自己小的,就记录一下。
题意:
题目要求找到离自己最近的且比自己温度还要高的。并且返回距离自己的距离。如果没有找到比自己还高的,就返回0。
思路:
挨个遍历temperature[]数组
1.如果比栈顶元素大,就记录该元素距离栈顶元素的距离。result[stack.peek()]=i-stack.peek();但是这个操作必须是可循环的。因为一个元素不一定只是一个元素的最近且温度高的。
2.如果比栈顶元素小或者相等。直接push进去
代码:
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int[] result = new int[temperatures.length];
Stack<Integer> stack = new Stack<>();
stack.push(0);// 放第一个元素的下标值
// 开始遍历
for (int i = 1; i < temperatures.length; i++) {
if (temperatures[i] <= temperatures[stack.peek()]) {
stack.push(i);
} else {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
result[stack.peek()] = i - stack.peek();
stack.pop();
}
stack.push(i);
}
}
return result;
}
}
六、下一个更大元素I
题意:
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
大的元素。即:在nums[2]中找出nums[1]中元素的响应下标位置,然后在右侧找到第一个比x大的元素。
思路:
因为nums1是nums2的子集。因此在nums2中使用单调栈进行操作,然后如果遇到nums1中的元素(使用HashMap将元素和下标一一映射起来)。然后就把他放到result中。
代码:
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int[] result = new int[nums1.length];
Arrays.fill(result, -1);
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums1.length; i++) {
map.put(nums1[i], i); // 值和下标
}
// 对nums2进行操作
Stack<Integer> stack = new Stack<>();
stack.push(0);
for (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()]) {
if (map.containsKey(nums2[stack.peek()])) {
int indexOfNums1 = map.get(nums2[stack.peek()]);
result[indexOfNums1] = nums2[i];
}
stack.pop();
}
stack.push(i);
}
}
return result;
}
}
七、下一个更大元素II
给定一个循环数组 nums
( nums[nums.length - 1]
的下一个元素是 nums[0]
),返回 nums
中每个元素的 下一个更大元素 。
思路:
循环的长度变为nums.length*2-1,然后遍历。这样就可以重新遍历数组一次,达到环的效果。
代码:
class Solution {
public int[] nextGreaterElements(int[] nums) {
int size=nums.length;
int[] result = new int[size];
Arrays.fill(result,-1);
Stack<Integer> stack = new Stack<>();
stack.push(0);
// 故意循环两遍
for(int i=0;i<size*2;i++){
if(nums[i%size]<=nums[stack.peek()]){
stack.push(i%size);
}else{
while(!stack.isEmpty()&&nums[i%size]>nums[stack.peek()]){
result[stack.peek()]=nums[i%size];
stack.pop();
}
stack.push(i%size);
}
}
return result;
}
}
八、接雨水(单调栈/双指针)
单调栈
思路:
仍然利用单调栈的思想。但是当nums[i]>nums[stack.peek()]时,处理逻辑发生了变化。
首先我们可以得知,当nums[i]大于时,中间的是nums[stack.peek()],右边的是nums[i],左边的是栈顶元素的下一个元素。然后计算雨水的面积。height:Math.min(nums[i],nums[j])-nums[mid];为什么要取最小的,因为木板效应。算出来之后加到总的面积里面。
注意:
在栈顶下面取第二个元素的时候,要先判断Stack.isEmpty()。如果为空的话,就不可以pop()了,会报异常的。
这个长的雨水的面积是:有一个元素它的下一个比它大的元素距离它有点远,因此weight比较大。
代码:
class Solution {
public int trap(int[] height) {
int[] result = new int[height.length];
if(height.length<=2)return 0;//接不住雨水 漏掉了
int sum = 0;
Stack<Integer> stack = new Stack<>();
stack.push(0);
for (int i = 1; i < height.length; i++) {
if (height[i] <= height[stack.peek()]) {
stack.push(i);
} else {
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int mid = stack.pop();// 中间的那个元素
if (!stack.isEmpty()) {
int left = stack.peek();// 左边它大的元素
int h = Math.min(height[left], height[i])-height[mid];
int weight = i - left - 1;
if(h*weight>0)sum += h * weight;
}
}
stack.push(i);
}
}
return sum;
}
}
双指针:
九、柱状图中的最大矩形
思路:
求左右两边第一个小的柱状图。此时矩形的面积=(right-left-1)*nums[i](高度)。
eg:当遍历到5的时候,左边第一个小的柱形图是1,右边第一个小的柱形图是2,下标差为3
但实际上只有2,因此还要-1;高度就是nums[i],也就是5的高度。所以面积=2*5
对比:
这道题和接雨水的题很类似。接雨水的题是找两边第一个大的,这样雨水才能不漏。
代码:
class Solution {
public int largestRectangleArea(int[] heights){
int[] newHeights=new int[heights.length+2];
//首尾加0
newHeights[0]=0;
newHeights[newHeights.length-1]=0;
for(int i=0;i<heights.length;i++){
newHeights[i+1]=heights[i];
}
//定义栈
Stack<Integer> stack=new Stack<>();
stack.push(0);
int max=0;
for(int i=0;i<newHeights.length;i++){
if(newHeights[i]>=newHeights[stack.peek()]){
stack.push(i);
}else{
while(!stack.isEmpty()&&newHeights[i]<newHeights[stack.peek()]){
int right=i;
int mid=stack.pop();
if(!stack.isEmpty()){
int left=stack.peek();
int weight=right-left-1;
int height=newHeights[mid];
max=Math.max(max,weight*height);
}
}
stack.push(i);
}
}
return max;
}
}
注意:
这里要在数组的前后加0,如果不加0的话,首尾元素遇到特殊情况,就无法按照下面的逻辑来计算。
eg:2,1。2入栈后,出现了右边第一个比它小的。但是因为2是第一个,左边没有比它还小的了。所以right-left-1=0;因此首尾要加一个0;
eg:当遍历到末尾之后,还是没有找到比栈顶元素小的数,那么右边就没有比它还小的。right的下标就找不到。因此末尾也要添一个0;