文章目录
1. 剑指 Offer 10- I. 斐波那契数列
思路:斐波那契数列一般的常规解法是采用递归,但是递归的复杂度较高,是2^n,因此我们用迭代方法来取代递归方法,斐波那契数列的定义式其实就是它的状态转移方程可以创建一个大小为(n+1)的数组来记录各个dp[i]的数值,为了减少空间复杂度,可以用三个变量来迭代即可,空间复杂度为O(1),时间复杂度为O(n)
状态方程已知
class Solution {
public int fib(int n) {
if(n<=1)
return n;
/*
使用dp数组的做法
*/
// int dp[]=new int[n+1];
// dp[0]=0;
// dp[1]=1;
// for(int i=2;i<=n;i++)
// dp[i]=(dp[i-1]+dp[i-2])%1000000007;
// return dp[n];
int dp0,dp1,dp2=0;
dp0=0;
dp1=1;
for(int i=2;i<=n;i++)
{
dp2=(dp0+dp1)%1000000007;//防止数字过大
dp0=dp1;
dp1=dp2;
}
return dp2;
}
}
2. 剑指 Offer 10- II. 青蛙跳台阶问题
思路:青蛙跳台阶问题和斐波那契数列类似,斐波那契数列序列是0 1 1 2 3….青蛙跳台阶序列是1 1 2 3 5….采用和斐波那契数列相同的方法来解决,只不过初始值设置的不一样
class Solution {
public int numWays(int n) {
//数列:1 1 2 3 5
int dp0=1,dp1=1,dp2=0;
if(n<=1)
return 1;
for(int i=2;i<=n;i++)
{
dp2=(dp0+dp1)%1000000007;
dp0=dp1;
dp1=dp2;
}
return dp2;
}
}
//O(N)
//O(1)
3. 剑指 Offer 63. 股票的最大利润
思路:关键在于状态转移方程 前i天的最大利润=max(前i-1天的最大利润,price[i]-前i-1天内的最小价格) 因此用一个变量来维护前i天内的最小价格,一个变量来维护最大利润,然后遍历prices数组,不断地更新最大利润,最小价格即可
class Solution {
public int maxProfit(int[] prices) {
//前i天内的最小价格
int min_Price=Integer.MAX_VALUE;
//前i天内的最大利润
int max_Profit=0;
for(int price:prices)
{
min_Price=Math.min(min_Price,price);
max_Profit=Math.max(max_Profit,price-min_Price);
}
return max_Profit;
}
}
//O(N)
//O(1)
4. 剑指 Offer 42. 连续子数组的最大和
思路:遍历整个数组,当遍历到nums[i]时,可以知道nums[0]-nums[i]中连续子数组的最大值是多少,然后再用一个变量来更新不同nums[i]所对应的连续子数组的最大值,遍历结束后,该变量就是最终结果
状态方程: dp[i]=max(dp[i-1]+nums[i],nums[i])
dp[i]表示以nums[i]结尾的子数组,必须包含nums[i] 对于每一个nums[i]结尾的子数组都有一个最大值,这是局部的,设置的全局变量来获得全局的最大值
class Solution {
public int maxSubArray(int[] nums) {
int dp[]=new int[nums.length];
int maxVal=nums[0];
dp[0]=nums[0];
for(int i=1;i<nums.length;i++)
{
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
maxVal=Math.max(maxVal,dp[i]);
}
return maxVal;
}
}
//O(N)
//O(1)
上面的代码中使用了dp数组,因此产生了额外的空间消耗,实际代码中只有两个变量的切换,因此可以采用两个变量来执行(滚动数组的思想)
class Solution {
public int maxSubArray(int[] nums) {
int dp1,dp0;
int maxVal=nums[0];
dp0=nums[0];
for(int i=1;i<nums.length;i++)
{
dp1=Math.max(dp0+nums[i],nums[i]);
maxVal=Math.max(maxVal,dp1);
dp0=dp1;
}
return maxVal;
}
}
//O(n)
//O(1)
5. 剑指 Offer 47. 礼物的最大价值
思路:对于棋盘中的每一个位置(i,j)(i>1,j>1)都可以从两个位置达到,分别是上边和左边,因此当前位置(i,j)可以得到的最大价值取决于(i-1,j)位置可以得到的最大价值 (i,j-1)位置可以得到的最大价值中的较大值
状态转移方程为:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+grid[i][j]
边界值处理: 对于第1行,当前位置只能从左边位置到达,对于第1列,当前位置只能从上面到达
class Solution {
public int maxValue(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int dp[][]=new int[m][n];
dp[0][0]=grid[0][0];//初始化第一个值
//初始化第一行 第一行中的当前位置只能从左边到达
for(int j=1;j<n;j++)
dp[0][j]=dp[0][j-1]+grid[0][j];
//初始化第一列 第一列中的当前位置只能从上边到达
for(int i=1;i<m;i++)
dp[i][0]=dp[i-1][0]+grid[i][0];
//初始化其他行 列 (i,j)位置可以从左边到达 也可以从上面达到
//取左边位置和上面位置中的较大值
for(int i=1;i<m;i++)
for(int j=1;j<n;j++)
{
dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j])+grid[i][j];
}
return dp[m-1][n-1];
}
}
//O(mn)
//O(mn)
当不想额外地处理边界值时,可以将dp数组多开一行一列,即dp[m+1][n+1] 这种情况下dp[i][j]表示的是grid[0][0]到grid[i-1][j-1]范围内的最大值 对于dp[m][n] 这种情况下dp[i][j]表示的是grid[0][0]到grid[i][j范围内的最大值
//增加一行一列
class Solution {
public int maxValue(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int dp[][]=new int[m+1][n+1];
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1];
}
}
//dp[0][j]和dp[i][0]初始值都是0 对于(0,j)位置上的最大值也就是dp[1][j+1]
//从代码中可以看出dp[1][j+1]=max(dp[0][j],dp[1][j])+grid[i-1][j-1]
//dp[0][j]=0 所以dp[1][j+1]=dp[1][j]+grid[0][j]=grid[0][j-1]+grid[0][j]
//即对第1行就是逐个累加的过程
return dp[m][n];
}
}
//O(mn)
//O((m+1)(n+1))
上面的这种边界值处理使得空间开销变大了,我们可以使用滚动数组的方式,将二维数组降成一维数组
//采用滚动数组
class Solution {
public int maxValue(int[][] grid) {
int m=grid.length;
int n=grid[0].length;
int dp[]=new int[n+1];
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
dp[j]=Math.max(dp[j],dp[j-1])+grid[i-1][j-1];
//这里的dp[j]相当于二维数组时的dp[i-1][j](上面的)
//这里的dp[j-1]相当于二维数组时的dp[i][j-1](左面的)
//因为是1行1行地遍历的 所以在遍历到(i,j)位置时 dp[j]保存的是上一行的位置(i-1,j)的最大值 dp[j-1]在dp[j]前面执行 所以dp[j-1]是当前行的前一个位置所具有的最大值 对于一位数组dp 可以根据行数目确定也可以根据列的数目确定 代码中是以列数目确定的 如果以数目确定 即dp[m+1] 则在遍历时需要按列遍历 dp[i-1]表示上面的(当前列) dp[i]表示左边的(上一列)
}
}
return dp[n];
}
}
//O(mn)
//O(n)
6. 剑指 Offer 46. 把数字翻译成字符串
思路:类似于青蛙跳台阶问题。什么时候可以跳2个台阶?f(i-2): 当x_i和x_(i-1)可以组合起来翻译;什么时候可以跳1个台阶?f(i-1): 当x_i只能单独翻译 因此遍历到x_i(有i个台阶时)有的翻译种数:f(i)=f(i-1)+f(i-2)
class Solution {
public int translateNum(int num) {
String s=String.valueOf(num);
int n=s.length();
int dp[]=new int[n+1];//dp[i]表示s[0:i-1]区间内最多的翻译种数 即i个数字的翻译种数
dp[1]=1;//一个字符只有一种翻译
dp[0]=1;//因为"12"有两种翻译 dp[2]=dp[1]+dp[0]=1+dp[0] => dp[0]=1
for(int i=2;i<=n;i++)
{
String tmp=s.substring(i-2,i);//[i-2,i)
// 10=<tmp<=25 有两种翻译方法
if(tmp.compareTo("10")>=0&&tmp.compareTo("25")<=0)
dp[i]=dp[i-1]+dp[i-2];
else
dp[i]=dp[i-1];
}
return dp[n];//dp[n]对应着s[n-1]
}
}
//O(n)
//O(n)
//采用滚动数组减少空间消耗
class Solution {
public int translateNum(int num) {
String s=String.valueOf(num);
int a=1,b=1,c=0;
int n=s.length();
for(int i=2;i<=n;i++)
{
String tmp=s.substring(i-2,i);//[i-2,i)
// 10=<tmp<=25 有两种翻译方法
if(tmp.compareTo("10")>=0&&tmp.compareTo("25")<=0)
c=a+b;
else
c=b;
a=b;
b=c;
}
return b;//b保存了最新的结果
}
}
//O(n)
//O(1)
7. 剑指 Offer 48. 最长不含重复字符的子字符串
思路:从前往后遍历字符串 当遍历到位置j时 当前位置字符记为ch i表示ch在[0:j-1]区间内出现的位置,没有出现则用-1表示 然后更新当前字符ch在[0:j]区间中的位置 用dp[i]表示以s[i]结尾的字符串中最长的不重复子串 分为以下3种情况:
- i=-1 即ch在区间[0:j-1]中没有出现过 则dp[i]=dp[i-1]+1
- dp[j-1]<j-1-i+1 j-i表示上一次出现的前一个位置的区间长度 如果dp[j-1]<j-1-i+1 就说明之前出现的字符ch没有出现在dp[j-1]的计数范围内 因为dp[j-1]是以s[j-1]结尾的 当前字符是唯一出现的 则dp[i]=dp[i-1]+1
- dp[j-1]>=j-1-i+1 说明上一次出现的ch在dp[j-1]的计数范围内 所以不重复的区间长度是j-(i-1)+1=j-i
class Solution {
public int lengthOfLongestSubstring(String s) {
int n=s.length();
if(n==0)//""空串直接返回0
return 0;
int dp[]=new int[n];
//dp[i]表示以s[i]结尾的最长不重复子串长度
HashMap<Character,Integer> map=new HashMap<>();
int ans=1;//只有一个字符时答案是1
dp[0]=1;//最基本子问题 只有一个字符
map.put(s.charAt(0),0);//在map中存放第一个字符及其索引
for(int j=1;j<n;j++)
{
int i=map.getOrDefault(s.charAt(j),-1);
map.put(s.charAt(j),j);
if(dp[j-1]<j-i)
dp[j]=dp[j-1]+1;
else
dp[j]=j-i;
ans=Math.max(ans,dp[j]);//记录全局最优解
}
return ans;
}
}
//O(n)
//O(n)
8. 剑指 Offer 19. 正则表达式匹配
解释: dp[i][j]表示s的前i个字符和p的钱j个字符是否匹配(i=0,1,2,3…)
先考虑特殊情况:如果s串为空,即源串为空,则匹配串p只有像a*b*c*…这样的才能够和s匹配,且只有奇数位才有可能匹配成功,只要前面有一位没有匹配成功,则整个p和s匹配失败
考虑一般情况:
匹配串当前的位置j-1 对应dp[i][j]
- p.chatAt(j-1)==’*’ 以下三种情况可以匹配成功 即dp[i][j]=true
- dp[i][j-2]为true 即s[0:i-1]和p[0:j-3]已经匹配成功 这种情况下p[i-2]和p[i-1]可以作为整体出现0次 比如a*=“”
- dp[i-1][j]&&s[i-1]==p[j-2] 即s[0:i-2]和p[0:j-1]已经匹配成功 并且有s[i-1]==p[j-2] 此时可以使得 p[i-2]和p[i-1]可以作为整体再出现1次 即a*=aa
- dp[i-1][j]&&p[j-2]==’.’ 即s[0:i-2]和p[0:j-1]已经匹配成功 s[i-1]一定可以和p[j-2]匹配 .*=aa
- p.chatAt(j-1)==’*’ 以下2种情况可以匹配成功 即dp[i][j]=true
- dp[i-1][j-1]&&s[i-1]=p[i-1] 即s[0:i-2]和p[0:j-2]匹配 当前位置的s[i-1]和p[j-1]也匹配
- dp[i-1][j-1]&&p[i-1]==’.’ 即s[0:i-2]和p[0:j-2]匹配 当前位置的s[i-1]和p[j-1]也匹配
class Solution {
public boolean isMatch(String s, String p) {
int m=s.length();
int n=p.length();
// dp[i][j]表示s前i-1个字符,p前j-1个字符是否匹配
boolean dp[][]=new boolean[m+1][n+1];//默认值都是false
dp[0][0]=true;//空串可以与空串匹配
//处理第一行 当s为空串时 p只能是a*b*c*这种形式 即奇数下标元素是*
for(int j=2;j<n+1;j+=2)
{
dp[0][j]=dp[0][j-2]&&p.charAt(j-1)=='*';
}
//第一列其实不用处理 当p为空串时 dp[][0]都是false 而默认值就是false
for(int i=1;i<m+1;i++)
{
for(int j=1;j<n+1;j++)
{
if(p.charAt(j-1)=='*')
{
if(dp[i][j-2])//让当前字符*和前面的字符p[j-2]不要出现
dp[i][j]=true;
//让当前字符p[j-1]=*和p[j-2]再出现一次
else if(dp[i-1][j]&&s.charAt(i-1)==p.charAt(j-2))
dp[i][j]=true;
else if(dp[i-1][j]&&p.charAt(j-2)=='.')
dp[i][j]=true;//最特殊情况:p[j-2]=. p[j-1]=*时 是万能匹配
}
else// 当p[j-1]!=*时,有两种情况
{
if(dp[i-1][j-1]&&s.charAt(i-1)==p.charAt(j-1))
dp[i][j]=true;//纯字符匹配 前面元素之前都匹配 且 当前元素也相容
else if(dp[i-1][j-1]&&p.charAt(j-1)=='.')//.匹配任意一个字符
dp[i][j]=true;
}
}
}
return dp[m][n];
}
}
//O(mn)
//O(mn)
9. 剑指 Offer 49. 丑数
思路:丑数又比它小的丑数通过乘2/3/5得到一个新的丑数
可以这样理解: 有三匹马在赛跑,a马跑的最慢,b马中等,c马跑的最快 遵循一个原则,跑的慢可以先跑,但是跑的远了,就得后跑 一开始a马速度慢,所以a先跑2米, 然后b马也慢,b马跑3米, 然后a马跑的太慢了,让他先跑也只跑了两米,所以再让他先跑,所以此时 a马跑了4米,b马跑了3米,c马原地不动 此时c马以及落后太多了,所以c马得到了先跑权力。 直到我们交出了10次跑步优先权之后,结束竞争。谁小谁跑
class Solution {
public int nthUglyNumber(int n) {
int dp[]=new int[n];
int a=0,b=0,c=0;
dp[0]=1;//第一个丑数
for(int i=1;i<n;i++)
{
int n2=dp[a]*2,n3=dp[b]*3,n5=dp[c]*5;
dp[i]=Math.min(n2,Math.min(n3,n5));
if(dp[i]==n2)
a++;
if(dp[i]==n3)
b++;
if(dp[i]==n5)
c++;
}
return dp[n-1];
}
}
//O(n)
//O(n)
10. 剑指 Offer 60. n个骰子的点数
思路:单看第 n枚骰子,它的点数可能为 1,2,3,…,6 因此投掷完 n 枚骰子后点数之和 j 出现的次数,可以由投掷完 n−1枚骰子后时对应点数 j−1,j−2,j−3,…,j−6出现的次数之和转化过来,比如扔完n-1枚筛子后点数j-1出现了k次,第n枚筛子的点数是1,则扔完n枚筛子后点数之和为j的次数就是k。知道n枚骰子各个点数可能出现的次数 再除以总的可能出现情况就是概率
class Solution {
public double[] dicesProbability(int n) {
int dp[][]=new int[n+1][6*n+1];//dp[i][j]表示扔完第i枚刷子时出现点数和为j的可能次数
for(int i=1;i<=6;i++)
dp[1][i]=1;//1枚筛子时可能出现的点数范围是1-6 且都只会出现1次
for(int i=2;i<=n;i++)
{
for(int j=i;j<=6*i;j++)
{
for(int cur=1;cur<=6;cur++)
{
if(j<=cur)//n枚筛子得到的点数j只有比n-1枚筛子得到的点数大 才能由n-1枚筛子的点数转化
break;
//n枚骰子可以得到的点数可以由n-1枚骰子转化得到
//因为转化方式不唯一 所以需要加上dp[i][j]
//比如dp[3][4] 可以由dp[2][1](n-1枚筛子扔的点数是1 第n枚筛子扔的是5) dp[2][2] dp[3][3]转化
dp[i][j]=dp[i][j]+dp[i-1][j-cur];//第一次转化时dp[i][j]是0
}
}
}
double ans[]=new double[5*n+1];//n枚骰子可以得到5*n+1种点数
double all=Math.pow(6,n);//总的点数情况
for(int i=n;i<=6*n;i++)//n个筛子扔出的点数和最小是n 最大是6*n 一个5*n+1个
ans[i-n]=dp[n][i]/all;
return ans;
}
}
//O(n*n)
//O(n)