leetcode《图解数据结构》刷题日志【第四周】(2022/11/07-2022/11/20)
1. 剑指 Offer 19. 正则表达式匹配
1.1 题目
请实现一个函数用来匹配包含'. '和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。
示例 1:
输入:
s = "aa"
p = "a"
输出:
false
解释:
"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出:
true
解释:?
因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例?3:
输入:
s = "ab"
p = ".*"
输出:
true
解释:?
".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出:
true
解释:
因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出:
false
注意:
s?可能为空,且只包含从?a-z?的小写字母。
p?可能为空,且只包含从?a-z?的小写字母以及字符?.?和?*,无连续的 '*'。
1.2 解题思路
自己的思路是遍历模式字符串p,在遍历到某种符号的时候,顺便遍历待判断的字符串s,直到该模式失效。
p[i]=='*'
,查看s当前字符和前一个字符是不是相等,用循环遍历所有相同字符。p[i]=='.'
,s跳过当前字符。p[i]==其他字符
,和s中当前字符进行比较。
然鹅,这样无法得到组合模式,比如.*
之类的,虽然可以在原有的结构上修修补补,但是字符串的变种过多,个人根据这种简单的思路写出来的代码最多能够满足366/448
种情况。
而在官方题解中,考虑动态规划的最优子结构,从子问题的结果结合新添加的字符分情况讨论。对应状态d[i][j]
,可添加字符s[i-1]
或者p[j-1]
。
添加p[j-1]=='*'
,则考虑p[j-2]
出现0次、1次的情况,以及p[j-2]=='.'
的情况,出现任何一种情况都算正确。
p[j-2]
出现0次:则比较s[:i-1]以及p[:j-3]的匹配关系,对应的状态是d[i][j-2]
。
xxxxx a
xxxxx a b *
//比较的部分是"xxxxxa"和"xxxxxa",添加p[i-1]
p[j-2]
出现1次:则需要考虑s[:i-2]以及p[:j+1]的匹配关系(s中不包含s[i-1]
,而p中包含p[j-2]
,也就是添加字符s[i-1]
的情况,而p[j-2]
出现0次是添加p[j-1]
的情况),对应的状态是d[i-1][j]
,比较s[i-1]
和p[j-2]
(*前一个字符)的关系是不是相等或者p[j-2]
是不是等于'.'
。
xxxxx a a
xxxxx a *
//比较的部分是"xxxxxa"和"xxxxxa*",添加s[i-1]
添加p[j-1]!='*'
,考虑s[:i-2]以及p[:j-2]的匹配关系,也就是s[i-1]
和p[j-1]
之前的字符串比较,且待添加的两个字符需要相等或者p[j-1]=='.'
。
1.3 数据类型功能函数总结
//字符串相关操作
string.length();//获得字符串长度
string.charAt(index);//获得对应下标的字符
1.4 java代码
//官方解法
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length() + 1, n = p.length() + 1;
boolean[][] dp = new boolean[m][n];
dp[0][0] = true;
for(int j = 2; j < n; j += 2)
dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
dp[i][j] = p.charAt(j - 1) == '*' ?
dp[i][j - 2] || dp[i - 1][j] && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.') :
dp[i - 1][j - 1] && (p.charAt(j - 1) == '.' || s.charAt(i - 1) == p.charAt(j - 1));
}
}
return dp[m - 1][n - 1];
}
}
1.5 踩坑小记
- java.lang.StringIndexOutOfBoundsException: String index out of range: 2
数组访问越界,原因在于:
//错误代码
while(s.charAt(si-1)==s.charAt(si) && si<s.length()){
si++;
}
//错误点在于先进行数组访问再进行下标越界的判断,会导致数组访问出现越界。
//正确代码
while(si<s.length()&&s.charAt(si-1)==s.charAt(si)){
si++;
}
2. 剑指 Offer 42. 连续子数组的最大和
2.1 题目
2.2 解题思路
个人思路:
动态规划,分解成子问题,a[:n]的子数组和最大值和a[:n-1]有关。
关系分析:a[:n-1]的子数组最大值为x,下标起始为index,添加a[n]之后,如果a[index:n]的和大于x,a[n]的子数组最大值为a[index:n],否则子数组最大值为x。
由于存在负数,如果子数组最大值为负数,则下次直接选择a[n]作为子数组最大值。
但是这种想法还是存在漏洞,通过样例171/202。
官方题解里面将状态设置为dp[i]为以nums[i]为结尾的前数组段的子数组最大值。
和我的想法不同的是,这个dp[i]必须包含nums[i],从而保证连续性。因此需要一个dp[0:n-1]的数组存储每一小段的结果,从而求出最大值。dp[i]状态转移根据dp[i-1]和新加入的nums[i]来确认,如果dp[i-1]为负数,说明dp[i]中如果加入dp[i-1]只会帮倒忙,因此dp[i]=nums[i];如果dp[i-1]为正数,则可以加到结果中,即dp[i]=dp[i-1]+nums[i];
降低空间复杂度之后,改写nums[]数组的每一项来表示dp[i]的结果。这样不用开辟dp[n]的数组空间,提高效率。
2.3 数据类型功能函数总结
//数组
int[] array_Name=new int[size];//构建空数组
int n=array.length;//获得数组长度
2.4 java代码
class Solution {
public int maxSubArray(int[] nums) {
int n=nums.length;
//构建dp[i]最优子结构,可以直接修改nums[]数组
for(int i=1;i<n;i++){
if(nums[i-1]<=0){
nums[i]=nums[i];
}
else{
nums[i]+=nums[i-1];
}
}
//查找所有结果中的最大值。
int max=nums[0];
for(int i=1;i<n;i++){
if(max<nums[i]){
max=nums[i];
}
}
return max;
}
}
更加精简的代码:
class Solution {
public int maxSubArray(int[] nums) {
int n=nums.length;
int max=nums[0];
//一次遍历,一边修改nums[]数组保存结果,一边记录目前的最大值
for(int i=1;i<n;i++){
if(nums[i-1]>0){
nums[i]+=nums[i-1];
}
if(nums[i]>max){
max=nums[i];
}
}
return max;
}
}
第二种写法内存消耗小了0.2MB,看似不起眼,但是把超越人数从19%拉到了47%。
3. 剑指 Offer 46. 把数字翻译成字符串
3.1 题目
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
提示:
0 <= num < 231
3.2 解题思路
这题的分解情况和青蛙跳台阶的问题比较类似,青蛙跳台阶需要跳1层或者2层。
这里对于规模为n的数字,可以分解为规模为n-1的子问题和规模为n-2的子问题的和。初步的状态设计如下如下:
dp[i] 长度为i的数字的翻译方法
dp[0]=1,dp[1]=1,dp[2]=2,……
dp[i]=dp[i-1]+dp[i-2]
不过,结合提议,对于123
和149
这两个数字,分解翻译的结果就不尽相同,还需要考虑规模为n的数字能不能拆分为规模为n-2的子问题。
先来考虑状态设计:
dp[i] 长度为i的数字的翻译方法
dp[0]=1,dp[1]=1,dp[2]=2,……
//状态转移公式
dp[i]=dp[i-1]+dp[i-2];————如果能拆分,则跟上述分析类似
dp[i]=dp[i-1];————如果不能拆,则dp[i]的结果完全都dp[i-1]决定。仅有一种翻译方法。
下面考虑如何确定能不能拆分为n-2的子问题:
需要考虑n-2和n之间的数字和‘25’的大小关系。每次需要根据当前位数下标n,判断n-1和n组成的数字是不是在0~25的范围内。
因此,考虑将int型数组转为字符串s,并对两个字符串可能出现的情况进行讨论:
如果s[n-1]为1,或者s[n-1]为2但是s[n]为0~5,则表示n可以分解为n-2子问题;
否则,表示n不能被分解为n-2子问题。
特别说明:对于s[n-1]为‘0’的情况,0x虽然是一个小于25的数,但是不能按照0x表示的数字来翻译,不然则会丢掉‘0’的翻译结果,因此这一种情况需要0和x分别翻译,算在不能分解为子问题的情况里
3.3 数据类型功能函数总结
//字符串类型
String s=String.valueOf(int);//int型转字符串s
String.length();//获取字符串长度
string.charAt(index);//获取对应下标的字符
3.4 java代码
class Solution {
public int translateNum(int num) {
int dp_2=1,dp_1=1;
int dp=0;
if(num==0){//0
dp=1;
}
else if(num/10==0){//个位
dp=1;
}
else if(num/100==0){//两位数
dp=2;
}
String num_s=String.valueOf(num);//将数字转为字符串。
for(int i=1;i<num_s.length();i++){//122 12 1
char a1=num_s.charAt(i-1);
char a2=num_s.charAt(i);
//进行状态转移:
if(a1=='1'){
dp=dp_2+dp_1;
dp_2=dp_1;
dp_1=dp;
}
else if(a1=='2'&& a2>='0' && a2<='5'){
dp=dp_2+dp_1;
dp_2=dp_1;
dp_1=dp;
}
else{//不能分解为n-2
dp=dp_1;//dp=1+1=2 3 5
dp_2=dp_1;//dp2=1 2
dp_1=dp;//dp1=2 3
}
}
return dp;
}
}
4. 剑指 Offer 47. 礼物的最大价值
4.1 题目
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
? [1,3,1],
? [1,5,1],
? [4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
?
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
4.2 解题思路
动态规划问题,设dp[i][j]
表示移动到grid[i][j]
时得到的最大价值,最终取dp[m-1][n-1]
的值能够得到最大值。
- 最优子结构导出的状态转移式为:
dp[i][j]=max{dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j]}
- 初始状态
dp[0][0]=grid[0][0]
和连续子数组的最大值
那题类似,这题的状态变化只跟下一次要移动的格子里面的礼物价值相关,因此可以选择覆盖原有棋盘从而降低空间复杂度。
对于首行或者首列,从0 0位置移动仅有一种方式,因此首行和首列需要和上述的状态转移值分开考虑。在遍历过程中判断i-1,j-1有没有越界就可以实现
4.3 数据类型功能函数总结
//二维数组相关操作
int line=array2.length;//获得二维数组行数
int row=array2[0].length;//获得二维数组列数
4.4 java代码
class Solution {
public int maxValue(int[][] grid) {
int m=grid.length;//行数
int n=grid[0].length;//列数
//计算dp
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){//遍历第i行
if(i-1>=0){//非首行
if(j-1>=0){//非首列元素
int value=grid[i][j];
grid[i][j]=Math.max(grid[i-1][j]+value,grid[i][j-1]+value);
}
else{//首列元素
grid[i][j]+=grid[i-1][j];
}
}
else{//首行
if(j-1>=0){//不是第一个元素
grid[i][j]+=grid[i][j-1];
}
//首行第一个元素不需考虑,是grid[0][0]
}
}
}
return grid[m-1][n-1];
}
}
4.5 踩坑小记
1.java. lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
数组越界,仔细检查可知:自己的return是返回grid[m][n];其中m、n分别为行数和列数,实际上应该写出grid[m-1][n-1];
5. 剑指 Offer 48. 最长不含重复字符的子字符串
5.1 题目
5.2 解题思路
按照动态规划的思路,需要将问题分解为子问题。
最开始的思路:假设dp[i]指的是长度为i的字符串的无重复字符串长度,则dp[i+1]是从dp[i]增加一个s[i+1].
如果s[i+1]在字串里面有重复字符串,dp[i+1]=dp[i],,如果s[i+1]不重复,dp[i+1]=dp[i]+1
但是,这个最长连续子数组的最大和的题目类似,由于需要考虑连续字串,所以这里可以考虑修改dp[i]的定义是包含s[i]的前段字符的最长字串长度。
这样如果添加s[i+1],如果s[i+1]重复,dp[i+1]=1;如果不重复,dp[i+1]=dp[i]+1;
之后考虑重复字符串如何查询,最简单的思路是使用遍历,或者使用特殊的数据结构,比如集合set
再经过考虑,重复的时候dp[i+1]应该不是直接等于1,而是根据s[i+1]和之前的字符串中重复的位置来确定。所以用字符串遍历比用set更好,或者用list列表
5.3 数据类型功能函数总结
//字符串相关操作
string.length();//获取字符串长度
string.charAt(index);//获取对应下标的字符
//arraylist相关操作
ArrayList<> al_name=new ArrayList<>();//定义一个列表
ArrayList.add(elem);//在当前list末尾添加元素
ArrayList.remove(index);//删除下标为index的元素,后面的元素左移
ArrayList.indexOf(elem);//查找元素下标,没有的元素则返回-1
5.4 java代码
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length()==0){//特殊情况处理
return 0;
}
int dp=1;//初始值
int max=dp;//最大长度
ArrayList<Character> list= new ArrayList<Character>();
list.add(s.charAt(0));
for(int i=1;i<s.length();i++){
int index=list.indexOf(s.charAt(i));
if(index!=-1){//找到该元素,说明重复
for(int j=0;j<=index;j++){
list.remove(0);
}
list.add(s.charAt(i));
dp=dp-index;
}
else{//不重复
dp+=1;
list.add(s.charAt(i));
}
if(dp>max){
max=dp;
}
}
return max;
}
}
5.5 踩坑小记
1、java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
错误是下标访问越界的错误,考虑代码,我错误的地方在于删除list中数据的时候,以为remove()之后后面的元素不会自动左移,所以按照一般静态数组的方式来删除元素。
//错误代码如下:
for(int j=0;j<=index;j++){
list.remove(j);
}
//随着数据左移而j不断增加,最后会出现数组越界的情况
//正确代码如下:
for(int j=0;j<=index;j++){
list.remove(0);
}
6. 剑指 Offer 49. 丑数
6.1 题目
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:
1是丑数。
n不超过1690。
6.2 解题思路
最简单的思路:从1遍历,直到找到第n个丑数,结束遍历。
如果要用动态规划来求解的话,需要找到第n-1个丑数和第n个丑数之间的关系
假设dp[n]表示第n个丑数,则dp[n-1]和dp[n]的关系为:
- 如果dp[n-1]+1不满足丑数定义,一直加1直到满足为止。dp[n-1]+x=dp[n];
- 如果dp[n-1]+1满足丑数定义,dp[n-1]+1=dp[n]
再考虑初始情况,第一个丑数dp[1]=1;
丑数的判定则需要考虑是不是只含有2、3、5作为因子。如果num/2或3或5的结果也是丑数,则num是丑数。
但是后面用这种方法做的时候比较困难。最后还是根据官方题解的思路做的。
官方题解里面也是有一个思想:如果num/2或3或5的结果也是丑数,则num是丑数。所有dp[a]、dp[b]、dp[c]里面一定有一个是dp[i]%2或3或5得到的。这三个子问题求解得到dp[i];
且随着i的增加,子问题也需要不断前进更新,如果dp[x]*y==dp[i],则x这个下标需要往前进一个,这样才能保证没有重复值。
6.3 数据类型功能函数总结
//Math
Math.min(a,b);//比较两个数的最小值
//数组相关
int dp[] = new int[Size];//定义一维数组
6.4 java代码
class Solution {
public int nthUglyNumber(int n) {
int dp[]=new int[1690];
int a=0,b=0,c=0;
dp[0]=1;
for(int i=1;i<n;i++){
dp[i]=Math.min(dp[a]*2,dp[b]*3);
dp[i]=Math.min(dp[i],dp[c]*5);
if(dp[a]*2==dp[i]){
a++;
}
if(dp[b]*3==dp[i]){
b++;
}
if(dp[c]*5==dp[i]){
c++;
}
}
return dp[n-1];
}
}