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,直到该模式失效。

  1. p[i]=='*',查看s当前字符和前一个字符是不是相等,用循环遍历所有相同字符。
  2. p[i]=='.',s跳过当前字符。
  3. 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 踩坑小记

  1. 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
    解释: 122585种不同的翻译,分别是"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]

不过,结合提议,对于123149这两个数字,分解翻译的结果就不尽相同,还需要考虑规模为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
    解释: 路径 13521 可以拿到最多价值的礼物
?
提示:
    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 踩坑小记

1java.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 题目

我们把只包含质因子 235 的数称作丑数(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];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值