剑指 Offer II 75-100(7.9日更新)
剑指 Offer II 100:三角形中最小路径之和
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。示例 2:
示例2:
输入:triangle = [[-10]]
输出:-10
提示:
- 1 <= triangle.length <= 200
- triangle[0].length == 1
- triangle[i].length == triangle[i - 1].length + 1
- -104 <= triangle[i][j] <= 104
解法一:暴力解
就直接递归所有的可能 去获得最小的那个 很明显过不了最后一个用例。
class Solution {
int min = Integer.MAX_VALUE;
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle.size()==1){
return triangle.get(0).get(0);
}
getRES(triangle,1,0,triangle.get(0).get(0));//第0层 第0个 前面路线的和为0
return min;
}
public void getRES(List<List<Integer>> triangle,int level,int index,int res){
if(level==triangle.size()-1){
min=Math.min(min,Math.min(triangle.get(level).get(index)+res,triangle.get(level).get(index+1)+res));
return;
}
getRES(triangle,level+1,index,res+triangle.get(level).get(index));
getRES(triangle,level+1,index+1,res+triangle.get(level).get(index+1));
}
}
这里我模糊的记忆告诉我优化需要根据输入的维数,而我的输入有4个,数组、遍历的层数(到第几层了)、当前索引到多少了、和是多少。
我就以为需要搞个四维的东西,其实不然,我们可以直接通过建立一个相同大小的二维数组,根据这个可以保存之前走过的值,而不是通过系统栈的递归重复去进行计算,有点像斐波那契数列的空间保存优化。
这里就有了我们的解法二:通过dp数组优化。
解法二:dp数组 空间O(N2)
我们每次通过数组记录我们之前的选择。首先自下而上(这个思路很重要,斐波那契数列问题也是这样优化的)的去思考,会发现每个位置(假设为i)都只能从上一层的该位置或者i-1位置过来,而且这条路径一定会加上当前层i位置值,我们只需要从上一层的这两个数中挑选较小的即可。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle.size()==1){
return triangle.get(0).get(0);
}
//经典dp做法 因为当前行的大小可以根据上一行中对应位置的元素得到
//用i j表示横纵坐标,每一行j<=i 把数字向左对齐是一个等腰直角三角形
//但是当处于每行中的0位置和最右位置时,数据可以直接得出 即上一行最左侧的路径和和最右侧的路径和
//其余的时候得判断一下两个中的较小值
int n = triangle.size();
int[][] res = new int[n][n];
int i=0,j=0;
res[0][0] = triangle.get(0).get(0);
for(i=1;i<n;i++){//从1开始遍历 因为(0,0)的路径和就是自己
//每一行0处的公式:res(i,0)=res(i-1,0)+triangle(i,0)
res[i][0]=res[i-1][0]+triangle.get(i).get(0);
for(j=1;j<i;j++){//这里是小于i 因为等于i的要特殊判定
//在第二行的时候 直接跳过这里了,因此从第三行才开始执行这个循环
res[i][j]=Math.min(res[i-1][j-1],res[i-1][j])+triangle.get(i).get(j);
}
//每一行i处的公式:res(i,i)=res(i-1,i-1)+triangle(i,i)
res[i][i]=res[i-1][i-1]+triangle.get(i).get(i);
}
int min = res[n-1][0];
for(i=1;i<n;i++){
min=Math.min(min,res[n-1][i]);
}
return min;
}
}
解法三:优化dp数组至空间O(N)
这里我们观察数组可以意识到,我们数据与数据之间只有上下一行有关系,因此我们有了优化做法,用两个长度为n的数组,左脚踩右脚完成所有路径和的叠加。(这里我们可以意识到,每个路径都需要走到最下面的一层,因此每个路径是可以自下而上的去探索规律的)。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle.size()==1){
return triangle.get(0).get(0);
}
//优化做法 用两个长度为n的数组 左脚踩右脚完成
int n = triangle.size();
int[] a = new int[n];
int[] b = new int[n];
int i=0,j=0;
a[0] = triangle.get(0).get(0);//a永远是旧的那个
for(i=1;i<n;i++){//b永远是新的那一个行
//每一行0处的公式:b(0)=a(0)+triangle(i,0)
b[0]=a[0]+triangle.get(i).get(0);
for(j=1;j<i;j++){//这里是小于i 因为等于i的要特殊判定
//在第二行的时候 直接跳过这里了,因此从第三行才开始执行这个循环
b[j]=Math.min(a[j-1],a[j])+triangle.get(i).get(j);
}
//每一行i处的公式:res(i,i)=res(i-1,i-1)+triangle(i,i)
b[i]=a[i-1]+triangle.get(i).get(i);
//交换a和b的指向
int[] c = a;
a=b;
b=c;
}
//因为最后的交换 现在a是结果行
int min = a[0];
for(i=1;i<n;i++){
min=Math.min(min,a[i]);
}
return min;
}
}
这里的数组是用到了两个长度为N的数组,a和b。那么我们能不能继续进行优化呢?答案当然是可以的,我们可以只用一个数组就完成遍历,这就是解法四。
解法四:一个数组完成
我们前几种方法的遍历,是通过每一行从左到右去生成下一行的内容,这样我们每个数据会依赖上一个行的上侧和左侧。那么我们换一种思路去遍历,从右至左的遍历,又可以剩下一个数组的空间:
每个数据只依赖上一行的上和左,我们将所有数据在一行上更新的话,从右往左走,每次最右侧的数据只依赖上一行的最右侧,每新的一行都要比上一行多一个,那么最右侧的i位置,只依赖上一行的i-1位置,即左侧的值。而右侧第二个数i-1位置,依赖上和左,而上就是当前i-1位置的值,左是i-2位置的值,从而我们可以更新i-1位置的值。
因此,我们可以通过一个数组去完成这个任务。
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle.size()==1){
return triangle.get(0).get(0);
}
//优化做法 用一个长度为n的数组 从右到左遍历完成
int n = triangle.size();
int[] res = new int[n];
int i=0,j=0;
res[0] = triangle.get(0).get(0);
for(i=1;i<n;i++){
//i不变 还是从上往下遍历 j需要从最大往最小遍历即从右到左
//因此需要先把i位置的值搞上 最后搞0位置的值
res[i]=res[i-1]+triangle.get(i).get(i);
for(j=i-1;j>0;j--){
res[j]=Math.min(res[j-1],res[j])+triangle.get(i).get(j);
}
res[0]=res[0]+triangle.get(i).get(0);
}
int min = res[0];
for(i=1;i<n;i++){
min=Math.min(min,res[i]);
}
return min;
}
}
总结
- 要学会自下而上的思考方式,根据这个再从上往下走。
- 学到了dp的优化,去找数据之间的依赖关系。
- 根据依赖关系再去进行空间的优化。
- 多去尝试遍历方式能否继续优化,对空间的利用可以做到极致。
剑指 Offer II 099. 最小路径之和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:一个机器人每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 200
- 0 <= grid[i][j] <= 100
注意:本题与主站 64 题相同: https://leetcode-cn.com/problems/minimum-path-sum/
解法一:暴力解
老规矩,先试试暴力解法。
class Solution {
int min = Integer.MAX_VALUE;
public int minPathSum(int[][] grid) {
//暴力解
help(grid,0,0,0);
return min;
}
public void help(int[][] grid,int i,int j,int res) {
if((i==grid.length-1)&&(j==grid[0].length-1)){
res+=grid[grid.length-1][grid[0].length-1];
min=Math.min(min,res);
return;
}
if(i>grid.length-1||j>grid[0].length-1){
return;
}
help(grid,i+1,j,res+grid[i][j]);
help(grid,i,j+1,res+grid[i][j]);
}
}
经典不给过嗷,现在去想咋用dp去做优化。
解法二:dp数组
这里我们直接一步到位,先分析这个问题,主要还是搞路径和对吧,那么我们就可以给中间状态的每一步去记录下来,就想到了用一个一样大小的数组去记录。首先是第一行和第一列的路径和是固定的,只能这么走(因为规定了是最短路径,且方格中每个数都大于0,那么每多走一步都会浪费,在边上的相邻的两个数肯定是直接走是最近的)。
因此我们可以先把第一行和第一列的结果求出来。然后挨个去求每一行的路径和,每个地方只需要去判断左和上哪个更小,哪个更小就加到自己就形成了最小的当前路径和,和第100题非常像,那么我们就可以直接修改grid数组来完成,这样一点额外空间都用不到,当然面试的时候可能会不让修改,那就建个数组。因为这里需要用到左侧和上侧的,且需要完成第一行和第一列的布置,因此没法优化空间了。
所以空间要么0要么O(M*N),看面试官咋选了。
class Solution {
public int minPathSum(int[][] grid) {
//dp
int n = grid.length;
int m = grid[0].length;
//第一行全都加成路径和
for(int i=1;i<m;i++){
grid[0][i]=grid[0][i-1]+grid[0][i];
}
//第一列全都加成路径和
for(int j=1;j<n;j++){
grid[j][0]=grid[j-1][0]+grid[j][0];
}
//开始搞第二行 然后每行接着往下搞
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
grid[i][j]=Math.min(grid[i][j-1],grid[i-1][j])+grid[i][j];
}
}
return grid[n-1][m-1];
}
}
剑指 Offer II 098. 路径的数目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例:
经典暴力解不过
class Solution {
int res=0;
int y;
public int uniquePaths(int m, int n) {
//经典暴力
y=n;
help(m-1,0);
return res;
}
public void help(int m, int n) {
if(m==0&&n==y-1){
res++;
return;
}
if(m<0||n>y-1){
return;
}
help(m-1,n);
help(m,n+1);
}
}
dp数组
这类题型已经开始轻车熟路起来了,我的建议是下次直接dp好吧~
分析:这里可以弄一个一样大小的数组,然后每个数都是(0,0)处到(i,j)处的方法数,第一列和第一行是固定都为1,其余的左和上相加即为当前处的可到达方法数。
思路:让第一列和第一行都是1 然后每个其他元素左和上加起来 一行一行的加,一直到右下角,右下角即为结果。
class Solution {
public int uniquePaths(int m, int n) {
//经典dp
int[][] res=new int[m][n];
//第一列全都为1
for(int i=0;i<m;i++){
res[i][0]=1;
}
//第一行全都为1
for(int i=1;i<n;i++){
res[0][i]=1;
}
//开始搞第二行 然后每行接着往下搞
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
res[i][j]=res[i-1][j]+res[i][j-1];
}
}
return res[m-1][n-1];
}
}
好像也没法优化(错! 可以优化 只要是这一行数据仅与上一行和这一行的内容有关,即可优化成左脚踩右脚)
但是优化的代码很简单,就不写了捏。
97是个困难,害怕 先挑中等的写,这里先写89是因为第90题是89的加强版。
剑指 Offer II 089. 房屋偷盗
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
暴力不过
class Solution {
int max=Integer.MIN_VALUE;
public int rob(int[] nums) {
if(nums.length==1){
return nums[0];
}
//这里也需要两个分支
help0(nums,0,0);
help0(nums,1,0);
return max;
}
public void help0(int[] nums,int i,int res) {
//我们i+1的位置不让走 我们可以走i+2,i+3的位置,而i+4的时候可以通过走
//i+2再走i+4 必定是单走一个i+4大,因此我们只需要走两个分支
if(i>nums.length-1){
max=Math.max(max,res);
return ;
}
if(i==nums.length-2){
max=Math.max(max,res+nums[i]);
return ;
}
if(i==nums.length-1){
max=Math.max(max,res+nums[i]);
return ;
}
help0(nums,i+2,res+nums[i]);
help0(nums,i+3,res+nums[i]);
}
}
dp1:我自己的思路
从第90题回来,还是看官方的题解吧,折磨
我自己的思路:先搞出来前三个点,第一二个点和原来一样,第三个点是第一个点和第三个点的和。这样前三个点每个点都是当前的最大值,第四个点i只需要比较i-2和i-3位置上哪个更大,更大的加上自己的值就是当前点能偷到的最多的钱。
这里感觉很像路径和的翻版,处理起来稍微复杂,启发的点在于从最后一个点往前推,经典自下而上的去推,去找规律。然后再自上而下的去根据规律获得值。
这个思路对比起来,也是有逻辑的,逻辑就是你去获取当前位置的最好的值,因此无法考虑i-1,得去看i-2和i-3中最大的,把每个结果都认为是结尾处的值。
class Solution {
public int rob(int[] nums) {
//观察这个数组就是一行,我们直接遍历两边这个数组也能得到结果
//通过数组去维护每个房屋的最大的偷的金额
//前三个数是固定住的 后面每个数是i-2和i-3中的更大的值
int n=nums.length;
if(n==1){return nums[0];}
if(n==2){return Math.max(nums[0],nums[1]);}
if(n==3){return Math.max(nums[0]+nums[2],nums[1]);}
nums[2]=nums[0]+nums[2];
for(int i=3;i<n;i++){
nums[i]=Math.max(nums[i-2],nums[i-3])+nums[i];
}
return Math.max(nums[n-1],nums[n-2]);
}
}
dp2:官方题解的思路
这里用到的是类似于分支的路径选择,前两个点处理一样,从第三个点开始视为k,衍生出两种情况:
- 偷第k间,就是前k-2间最高的金额+第k间金额。
- 不偷第k间,就是前k-1间最高的金额。
这样写出的代码:
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0];
//这个很关键嗷 每次第二个点都应当是最大的那个
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
}
这个思路对比起来,是放弃了一定的思考,因为没有dp[i-1]会一直更大(这里很难理解,感觉看官方更清晰一些),因此合理。感觉它的思路来源是从前往后走推出来的,因此更便于理解。
dp3:滚动数组优化空间
官方解因为只关心前两个和,就可以只保存两个值,first和second。
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
而我自己的话,需要搞一个长度为4的数组,一直滚动着走,大概会用到%那种,就不写了。
剑指 Offer II 090. 环形房屋偷盗
一个专业的小偷,计划偷窃一个环形街道上沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
这个题和89题就多了个环,所以我们抽象成一个函数,限定出两个范围,0-n-2和1-n-1这两个范围,分别取最大值并求最大值返回。
class Solution {
public int rob(int[] nums) {
int n=nums.length;
if(n==1){
return nums[0];
}
if(n==2){
return Math.max(nums[0], nums[1]);
}
return Math.max(help(nums,0,n-2),help(nums,1,n-1));
}
//找范围内的最大值
public int help(int[] nums,int start,int end) {
int first = nums[start];
//这个第二个数 必定是最大值
int second=Math.max(nums[start],nums[start+1]);
for(int i=start+2;i<=end;i++){
//左脚踩右脚
int temp = second;
second=Math.max(second,first+nums[i]);
first = temp;
}
return second;
}
}
总结
- 把89题抽象成函数,经历了一顿毒打 我想我应该已经背过了2和3
- 第二个数是前两个数的最大值
- 状态转移方程:res[i]=max(res[i-1],res[i-2]+nums[i])
- 可以左脚踩右脚来优化空间
剑指 Offer II 091. 粉刷房子
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
dp一眼丁真
就第一行是初始状态三个数,第二行往后,每个都调上一行的非自己列的最小的数即可,因为只有两个行硬相关所有可以直接优化。
咋找出来的呢,感觉这个规律比较明显,比上两道简单多了。
class Solution {
public int minCost(int[][] costs) {
int n=costs.length;
int[][] res = new int[n][3];
res[0][0]=costs[0][0];
res[0][1]=costs[0][1];
res[0][2]=costs[0][2];
for(int i=1;i<n;i++){
costs[i][0]=Math.min(costs[i-1][1],costs[i-1][2])+costs[i][0];
costs[i][1]=Math.min(costs[i-1][0],costs[i-1][2])+costs[i][1];
costs[i][2]=Math.min(costs[i-1][0],costs[i-1][1])+costs[i][2];
}
return Math.min(costs[n-1][0],Math.min(costs[n-1][2],costs[n-1][1]));
}
}
空间优化
两行,左脚踩右脚,O(1)的空间复杂度。
class Solution {
public int minCost(int[][] costs) {
int n=costs.length;
int[] a = new int[3];
int[] b = new int[3];
a[0]=costs[0][0];
a[1]=costs[0][1];
a[2]=costs[0][2];
int[] c;
for(int i=1;i<n;i++){
b[0]=Math.min(a[1],a[2])+costs[i][0];
b[1]=Math.min(a[0],a[2])+costs[i][1];
b[2]=Math.min(a[1],a[0])+costs[i][2];
c=b;
b=a;
a=c;
}
return Math.min(a[0],Math.min(a[2],a[1]));
}
}
剑指 Offer II 092. 翻转字符
如果一个由 '0' 和 '1' 组成的字符串,是以一些 '0'(可能没有 '0')后面跟着一些 '1'(也可能没有 '1')的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 '0' 和 '1' 组成的字符串 s,我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'。
返回使 s 单调递增 的最小翻转次数。
dp O(N)
这个题折磨了我好久,因为和平时的dp不一样,当然最后看起来很简单。。。。需要用到两行的空间。原理在注释里差不多全了。
这里记录下思路:根据已有序列去推下一位应该最小为多少,0的前面只能有0,因此无论怎么翻转都是0那一行的事,如果当前为1就吧前一位+1是这一位,如果是0就直接等于前一位。1的前面可以是0也可以是1,因此我们可以去dp数组中上一位最小的值放到这里。
class Solution {
public int minFlipsMonoIncr(String s) {
int n=s.length();
//[i][0]记录字符串s在i位置上,字符为0时的最小翻转次数
//[i][1]记录字符串s在i位置上,字符为1时的最小翻转次数
int[][] res = new int[n][2];
if(s.charAt(0)=='1'){
res[0][0]=1;
}else{
res[0][1]=1;
}
for(int i=1;i<n;i++){
if(s.charAt(i)=='0'){
//0的前面只能是0 所以需要加上上一个值和当前是否为0
res[i][0]=res[i-1][0];
//而1的时候 前面可以是0也可以是1 找到最小即可
res[i][1]=Math.min(res[i-1][0],res[i-1][1])+1;
}else{
res[i][0]=res[i-1][0]+1;
res[i][1]=Math.min(res[i-1][0],res[i-1][1]);
}
}
return Math.min(res[n-1][0],res[n-1][1]);
}
}
dp空间优化 O(1)
每个都只与前一个数有关,所以其实只保留两个数即可。
class Solution {
public int minFlipsMonoIncr(String s) {
int n=s.length();
int a=0,b=0;//a记录0那一行的最新 b记录1那一行的最新
if(s.charAt(0)=='1'){
a=1;
}else{
b=1;
}
for(int i=1;i<n;i++){
if(s.charAt(i)=='0'){
//a不需要变化。。
//而1的时候 前面可以是0也可以是1 找到最小即可
b=Math.min(a,b)+1;
}else{
b=Math.min(a,b);
a=a+1;
}
}
return Math.min(a,b);
}
}
总结
看起来只需要找出状态转移方程就能轻松解决问题,找不出来就寄了。多去分析问题本身,用实验的方法往前推进。
剑指 Offer II 093. 最长斐波那契数列(2022.7.3)
如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:
-
n >= 3
-
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)
暴力不过 O(N3)
一点优化没有,直接三次方
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n=arr.length;
int res = 0;
for(int i=0;i<n-2;i++){
for(int j=i+1;j<n-1;j++){
int a = arr[i];
int b = arr[j];//记录前两个数
int len=2;//记录长度
for(int z=j+1;z<n;z++){
if(arr[z]==(a+b)){
len++;
a=b;
b=arr[z];
}
}
res=Math.max(res,len);
}
}
return res==2?0:res;
}
}
3个for的dp 还是O(N3)
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n=arr.length;
int res = 0;
int[][] dp = new int[n][n];
for(int i=0;i<n-2;i++){
for(int j=i+1;j<n-1;j++){
int a = arr[i];
int b = arr[j];//记录前两个数
for(int z=j+1;z<n;z++){
if(arr[z]==(a+b)){
dp[j][z]=dp[i][j]+1;
res=Math.max(res,dp[j][z]);
}
}
}
}
return res==0?0:res+2;
}
}
根据Map减少一个for O(N2)
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n=arr.length;
int res = 0;
int[][] dp = new int[n][n];
HashMap<Integer,Integer> map = new HashMap<>();
for(int x=0;x<n;x++){
map.put(arr[x],x);
}
for(int i=0;i<n-2;i++){
for(int j=i+1;j<n-1;j++){
int z = map.getOrDefault(arr[i]+arr[j],-1);
if(z!=-1){
dp[j][z]=dp[i][j]+1;
res=Math.max(res,dp[j][z]);
}
}
}
return res==0?0:res+2;
}
}
dp➕双指针 最优解法 接近O(NlogN)
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n=arr.length;
int res = 0;
int[][] dp = new int[n][n];
for(int i=2;i<n;i++){
int a=0,b=n-1;
while(a<b){
int sum = arr[a]+arr[b];
if(arr[i]==sum){
dp[b][i]=dp[a][b]+1;
res=Math.max(res,dp[b][i]);
a++;b--;
}else if(arr[i]>sum){
a++;
}else{
b--;
}
}
}
return res==0?0:res+2;
}
}
总结
感觉这种题目,先定义dp数组的意义,最后两个数是最大的,从前面最小的0开始往后加。
然后利用for循环写出基础思路,然后用map,我觉得一般是做到这个程度。
双指针的这个,有点类似三数之和那种,归结为两个数的和并且利用顺序递增的规律完成(类似荷兰国旗)。
剑指 Offer II 086. 分割回文子字符串
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
回溯+dp
class Solution {
boolean[][] f;
List<List<String>> tmp = new ArrayList<List<String>>();
List<String> ans = new ArrayList<String>();
int n;
public String[][] partition(String s) {
n = s.length();
f = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
dfs(s, 0);
int rows = tmp.size();
String[][] ret = new String[rows][];
for (int i = 0; i < rows; ++i) {
ret[i] = tmp.get(i).toArray(new String[tmp.get(i).size()]);
}
return ret;
}
public void dfs(String s, int i) {
if (i == n) {
tmp.add(new ArrayList<String>(ans));
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) {
ans.add(s.substring(i, j + 1));
dfs(s, j + 1);
ans.remove(ans.size() - 1);
}
}
}
}
这个题首先是关于回文的,因此可以用上dp来判断是否是回文字符串。方程就是看s[i]s[j]&&dp【i+1】【j-1】是否为真,且ij以及i<j的二维表的字段都是真,因为代表一个字符的串和空串。这里dp的内容就解决了,"google"的dp表格如下。
下面是回溯的内容,第一次接触到这个。大致可以归纳总结为两个循环,首先是外层的循环,用i来控制行,从0开始往下走,j用来控制列。
举例:假设输入的为"google",先从(0,0)这一行往后走,由于(0,0)为真,添加g至ans,走到(1,1)为真,添加o至ans,依次走,直到走到最后(6,6),ans为【"g","o","o","g","l","e"】,这样添加到tmp中第一个结果。
然后开始回溯return,第一步回溯到(5,5),删掉"e",回到(4,4),删掉"l",j继续往后走(4,5),发现都为false,就继续回溯。走到(3,3)删掉"g",然后(3,4)(3,5)都为false;以此类推回到(1,1),删掉"o",此时ans中只有【"g"】,开始走(1,2)(1,5)因为(1,2)为真,加入"oo",我们继续添加(3,3)、(4,4)、(5,5),就完成了第二结果【"g","oo","g","l","e"】;继续回溯,走到(0,3)时候发现"goog"为开头的,继续加入(4,4)、(5,5)得到第三个结果,【"goog","l","e"】我们返回最终结果。
总结
我们可以看到,每一种字符串组合方法(O(2N)),下面的每个字符可能都要走到(O(N2),因此时间复杂度为O(2N*N2),回溯的精髓在于,保存已有情况,继续遍历下面的可能情况,一进一出来不断递归新的可能。
i作为行是通过用系统栈去记住的,j每次去遍历每一列的可能性,如果这一列为True,直接存入(i,j)的字符串,并且i跳到第j+1行。
这是我接触的第一个回溯题目,感觉很猛,自己完全想不出来那种。。。后面还要多加努力。
剑指 Offer II 087. 复原 IP
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
自己思路和问题
被上一题回文影响,这一题也直接奔着dp就去了,发现根本用不上捏。
回溯标准解
import java.util.StringJoiner;
class Solution {
static final int SEG_NUM = 4;
List<String> res;
String s;
int[] ip;
int n;
public List<String> restoreIpAddresses(String s) {
if(s.length() < 4 || s.length() > 12) { // 特判:排除非法输入
return new ArrayList<>();
}
this.res = new ArrayList<>();
this.s = s;
this.n = s.length();
this.ip = new int[SEG_NUM];
backtrack(0, 0);
return res;
}
private void backtrack(int i, int depth){ // depth跟踪ip节数
if(depth == SEG_NUM){ // 4节ip
if(i == n) { // 完整分割
StringJoiner sj = new StringJoiner(".");
for(int seg : ip) sj.add(Integer.toString(seg));
res.add(sj.toString());
}
return; // 若非完整分割,返回
}
// 剩余字符不足以提供剩余节数或超过剩余节数可能的最大字符数
if(n - i < SEG_NUM - depth || n - i > (SEG_NUM - depth) * 3) return; // 剪枝
int seg = 0;
for(int j = i; j < i + 3 && j < n; j++){
seg = seg * 10 + (s.charAt(j) - '0');
if(seg > 255 || seg < 0) return; // ip节合法检查(剪枝)
ip[depth] = seg;
backtrack(j + 1, depth + 1); // 使用数组,不必真的撤销
if(seg == 0) return; // "0"单独作为一节,不必再考察之后的j,否则继续考察后续的j
}
}
}
思路:完整切割s为四个相邻的子串,使得这四个子串组成一个合法ip,本质是穷尽有ip合法性约束的组合,容易看出是回溯问题。
照例先构思出问题树,问题树是一颗多叉树,树节点为s的子串。根以下第i层选出第i个ip节,故要dfs四层。每一个节点都有3个子节点(除非s的剩余字符不够),表示当前ip节(当前节点)的下一个ip节由1或2或3个后续连续字符组成。假设当前ip节的最后一个字符在s中的下标为i,则他的子节点从i+1开始。
总之,本问题是在一棵多叉树(基本上是一棵三叉树)上dfs搜索四层节点,使得这四个节点组成s,且为一个合法ip。
相比简单回溯模板,有变化的地方在于对'0'字符的处理,由于'0'字符除非单独作为一节,否则不能成为先导0。因此遇到'0'时,直接作为单独的ip节加入(return,不考虑后续字符作为本ip节)。另外就是ip节的合法性检查,合法则作为ip组成部分,继续dfs搜索,否则直接跳出处理本层下一个节点。
实际上由于s长度在[4, 12]范围内才需要处理(否则返回空列表),因此即便不考虑「剩余字符不足以提供剩余节数或超过剩余节数可能的最大字符数」(剪枝),效率也几乎没有差别。
总结
这个回溯依旧没有自己完成 嘤嘤嘤,很痛苦好吧,但是感觉快入门了好像,经典回溯的形式就是带着i走,让j从i开始遍历,用list加进去或者是数组添加,然后递归,然后出来的时候要list删掉或者数组覆盖掉,这就是经典的回溯模板。
剑指 Offer II 088. 爬楼梯的最少成本
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
dp
状态转移方程: dp[i]=Math.min(dp[i-2],dp[i-1])+cost[i],i>=2。结果取dp的最后两个数字的较小值。
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n=cost.length;
int[] dp = new int[n];
dp[0]=cost[0];
dp[1]=cost[1];
for(int i=2;i<n;i++){
dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];
}
return Math.min(dp[n-2],dp[n-1]);
}
}
总结
最小的啥啥啥一看就是dp的
这种搞个数组 写出来状态转移方程就完事了 最后取结果的时候小心下即可
不愧是是简单题 给了沃一点自信捏。
剑指 Offer II 085. 生成匹配的括号
正整数 n
代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
试图自己回溯 套公式套不出来
class Solution {
List<String> res = new ArrayList<>();
int n;
int[] sb ;
public List<String> generateParenthesis(int n) {
//这种生成啥 一看就是回溯
this.n=n;
sb=new int[n*2];
dfs(0,n*2);
return res;
}
public void dfs(int i,int len){
if(len==0){add}
for(j=i;j<len-i;j++){
sb[j]=1;
dfs(j+1,len++);
sb[];
if(sb.length()==n*2){
res.add(sb.toString());
sb.delete()
}
}
}
}
明明多叉树都能画出来,但是不知道为啥就是写不出来,哭出声。
感觉这个的难点在于,别的回溯都是剪枝,然后数组里面加一个再减一个,这个因为是成对的 按照公式没法减,需要加反括号的。。
回溯标准解 待写
剑指 Offer II 072. 求平方根
给定一个非负整数 x ,计算并返回 x 的平方根,即实现 int sqrt(int x) 函数。
正数的平方根有两个,只输出其中的正数平方根。
如果平方根不是整数,输出只保留整数的部分,小数部分将被舍去。
遍历
class Solution {
public int mySqrt(int x) {
if(x==2){return 1;}
for(int i=0;i<x;i++){
if(i*i>x){
return i-1;
}
if(i*i==x){
return i;
}
if(i*i==0&&i!=0){
//越界了
return 46340;
}
}
return x==1?1:0;
}
}
经典根据测试用例改。。实际上改成long能少一次判断
二分法
class Solution {
public int mySqrt(int x) {
int left=0;int right=x;int ans=-1;
while(left<=right){
int mid = (left-right)/2+right;
if((long)mid*mid<=x){
ans=mid;
left=mid+1;
}else{
right=mid-1;
}
}
return ans;
}
}
总结
二分 要注意:
- 越界的时候可以转long类型来完成题目
- left与right的关系 是否可以相等要判定
- 什么时候左移什么时候右移 在哪个区间对变量操作要分析
剑指 Offer II 073. 狒狒吃香蕉
狒狒喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
狒狒可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉,下一个小时才会开始吃另一堆的香蕉。
狒狒喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
二分
class Solution {
public int minEatingSpeed(int[] piles, int h) {
// 除了满足每个位置一个小时 还可以有剩余的时间去让一堆吃俩小时
//这样 我们可以先找到最大值M 从1-M二分 让数组根据二分的值s判断pile[i]/s的和==h
//尝试可能性 找到最小的k O(NlogN)
int right=0;
for(int a:piles){
right = Math.max(a,right);
}
int left = 1;int k=right;
while(left<right){
int mid = (right-left)/2+left;
int count = count(mid,piles);
if(count<=h){
//如果在速度mid下可以在 h小时内吃掉所有香蕉,则最小速度一定小于或等于speed,因此将上界调整为speed;
right=mid;
k=mid;//进来就说明找到了更小的speed 直接更新k
}else{
//时间大于等于 mid应当右移
left=mid+1;
}
}
return k;
}
public int count(int speed,int[] piles){
int res=0;
for(int a:piles){
res+=(a-1+speed)/speed;
}
return res;
}
}
二分还是不是很明白,左右便捷和while的控制
总结
鬼鬼 找一个bug找了好久
1.那个mid的公式 必须是大的减小的除以2再加上小的 否则出现负数会出问题。
2.向上取整公式:(a+b-1)/b
剑指 Offer II 074. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
排序+单调栈
class Solution {
public int[][] merge(int[][] intervals) {
//第一反应是用map (k,v)记录区间的左右每次进来新的去遍历整个区间map中有无合适的
//再一想感觉像单调栈,因为可以直接合并前后的值 这样先新建节点
//!woc我都写完了 我以为是顺序的 结果不是 我裂开 那就不是O(N)了 得排序了
Arrays.sort(intervals,(o1,o2)->(o1[0]-o2[0]));
//单调栈
Deque<Node> deque = new ArrayDeque<>();
deque.offerFirst(new Node(intervals[0][0],intervals[0][1]));
for(int i=1;i<intervals.length;){
int a = intervals[i][0];
int b = intervals[i][1];
Node temp = deque.peekLast();
if(deque.size()==0||temp.right<a){
//这个size==0的判断是如果新来的范围更大会移除上一个继续往下比较
//防止全部移空 报错空指针异常
deque.offerLast(new Node(a,b));
i++;
}else{
deque.removeLast();
//这里上一个数组右侧小于当前数组左侧 有重叠部分
if(temp.left>a){
//被完全覆盖了捏 移除上一个 并继续比较i不变
}else{
deque.offerLast(new Node(temp.left,Math.max(b,temp.right)));
i++;
}
}
}
int[][] res = new int[deque.size()][2];
int i=0;
while(!deque.isEmpty()){
Node temp = deque.removeFirst();
res[i][0]=temp.left;
res[i][1]=temp.right;
i++;
}
return res;
}
}
class Node{
int left;
int right;
Node(int a,int b){
left=a;
right=b;
}
}
我的第一反应是用到单调栈,其实不是,不过合并的逻辑和下一个方法的合并逻辑是一样的。走了很大的弯路,以后得多分析分析。
排序+遍历合并
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals,(o1,o2)->(o1[0]-o2[0]));
ArrayList<int[]> list = new ArrayList<>();
//遍历
List<int[]> merged = new ArrayList<int[]>();
for (int i = 0; i < intervals.length; ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) {
merged.add(new int[]{L, R});
} else {
merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
总结
多分析分析在动笔,区间的先排序再分析合并逻辑。。。
剑指 Offer II 075. 数组相对排序
给定两个数组,arr1 和 arr2,
- arr2 中的元素各不相同
- arr2 中的每个元素都出现在 arr1 中
对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
自己写的 O(N2)
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
int len=0;
for(int i=0;i<arr2.length;i++){
int x=arr2[i];
for(int j=len;j<arr1.length;j++){
if(arr1[j]==x){
int temp = arr1[len];
arr1[len]=x;
arr1[j]=temp;
len++;
}
}
}
Arrays.sort(arr1,len,arr1.length);
return arr1;
}
}
sort函数也是左闭右开
自定义排序 O(NlogN)复杂度
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
Map<Integer, Integer> map = new HashMap<>();
int length = arr2.length;
for (int i = 0; i < length; i++) {
map.put(arr2[i], i);
}
//int数组需要先装箱成Integer 后面再map映射回来
return Arrays.stream(arr1).boxed().sorted((i1, i2) -> {
//注意 这里i1是脚标比较大的那个元素 i2是角标比较小的那个元素
//如果返回值为负数,那么交换 如果为非负数则不交换
//通过对sort函数的自定义 完成题目
if (map.containsKey(i1) && map.containsKey(i2)) {
return map.get(i1) - map.get(i2);
} else if (map.containsKey(i1)) {
return -1;
} else if (map.containsKey(i2)) {
return 1;
} else {
return i1 - i2;
}
}).mapToInt(Integer::valueOf).toArray();
}
}
计数排序 O(N)
看一眼数据量,最多只涉及1001个数字(0到1000),立刻采用计数排序用微不足道的空间换取O(n)的时间复杂度,不需要真的排序。
- 创建一个大小为1001的计数数组count[],一个用于返回结果的res[]数组,大小与arr1相同。你当然也可以复用arr1来返回结果节省空间,但这将使得作为输入数据的arr1被改变,面试的时候如果想这样做,需要问清楚是否不再使用输入数据。
- 遍历arr1,令count[arr1[i]]++。
- 遍历一遍arr2,将count[arr2[i]]中的数字按顺序填入res[]数组中 。每填入一次,相应的count[arr2[i]]要减1。
- 最后再遍历count,将剩下的值不为0的元素下标放入res数组中。
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
int n1 = arr1.length, n2 = arr2.length;
int[] count = new int[1001], res = new int[n1];
for(int i = 0; i < n1; i++) count[arr1[i]]++;
int k = 0;
for(int i = 0; i < n2; i++) { // 将arr2中的数字输出到res
int val = arr2[i]; // 按顺序输出arr2的数字val
for(int j = count[val]; j >= 1; j--){ // 以val为坐标找到count中的计数j,将j个val输出到res[k]中,k依次递增
res[k] = val;
count[val]--; // 别忘了输出后要删除
k++;
}
}
for(int i = 0; i < count.length; i++){ // 将arr2以外的数字输出到res
int c = count[i];
for(; c >= 1; c--){
res[k] = i;
k++;
}
}
return res;
}
}
剑指 Offer II 076. 数组中的第 k 大的数字
给定整数数组 nums
和整数 k
,请返回数组中第 **k**
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
PriorityQueue小根堆
class Solution {
public int findKthLargest(int[] nums, int k) {
//维护一个K大小的东西 小根堆
//默认是小根堆 o1-o2是小根堆升序 o2-o1是大根堆降序
Queue<Integer> heap = new PriorityQueue<Integer>(k,(o1,o2)->{return o1-o2;});
for(int i:nums){
if(heap.size()<k){
heap.offer(i);
}else{
if(i>heap.peek()){
heap.poll();
heap.add(i);
}
}
}
return heap.peek();
}
}
额外空间O(logN) 时间O(N)
自定义快排实现
注意 这里因为用到了随机数字来放到结尾处,以及分治,所以时间复杂度是O(N) 是期望为线性的算法 额外空间O(logN)
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
//基于快排的写法
//这里的k是倒数的位置,转换成脚标
return quickSelect(nums,0,nums.length-1,nums.length-k);
}
public int quickSelect(int[] nums,int left,int right,int k){
int temp = randomPartition(nums,left,right);
if(temp==k){
return nums[temp];
}
return temp<k?quickSelect(nums,temp+1,right,k):quickSelect(nums,left,temp-1,k);
}
public int randomPartition(int[] nums,int left,int right){
int x = random.nextInt(right-left+1)+left;
swap(nums,x,right);
return partition(nums,left,right);
}
//根据right位置的元素为基准 找到这个元素在数组中的位置
public int partition(int[] nums,int left,int right){
int x = nums[right];
//i用于控制左边界 即比nums[x]小的部分
int i = left-1;
for(int j=left;j<right;j++){
//荷兰国旗的做法 直接把小的挪到最左侧
if(nums[j]<x){
swap(nums,++i,j);
}
}
//最终小于x的最右边界为i+1(不含小的) 把right位置的元素即x交换过来即可
swap(nums,i+1,right);
return i+1;
}
public void swap(int[] nums,int left,int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
自定义堆实现
class Solution {
public int findKthLargest(int[] nums, int k) {
//维护一个K大小的东西 小根堆
//默认是小根堆 o1-o2是小根堆升序 o2-o1是大根堆降序
int heapSize = k;
for(int i=0;i<heapSize;i++){
heapInsert(nums,i);
}
heapify(nums,0,heapSize);
for(int i=heapSize;i<nums.length;i++){
if(nums[i]>nums[0]){
swap(nums,0,i);
heapify(nums,0,heapSize);
}
}
return nums[0];
}
//将以index为根节点的字树弄成小根堆
public void heapify(int[] nums,int index,int heapSize){
int left = index*2+1;
while(left<heapSize){
int smallest = index*2+2<heapSize&&nums[left]>nums[left+1]?index*2+2:left;
if(nums[smallest]<nums[index]){
//新的最小值出现了 更新
swap(nums,smallest,index);
}else{
break;
}
index=smallest;
left=index*2+1;
}
}
public void heapInsert(int[] nums,int i){
//(i-1)/2这样节点脚标为偶数时才能找到自己的父节点
while(nums[i]<nums[(i-1)/2]){
swap(nums,i,(i-1)/2);
i=(i-1)/2;
}
}
public void swap(int[] nums,int left,int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
总共就两步 heapify和heapInsert,就完事了,背过这两部分代码吧。
剑指 Offer II 077. 链表排序
给定链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
简单粗暴
空间复杂度O(N)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
List<Integer> list = new ArrayList<>();
ListNode temp = head;
while(head!=null){
list.add(head.val);
head=head.next;
}
head=temp;
list.sort((o1,o2)->{return o1.compareTo(o2);});
for(int i=0;i<list.size();i++){
temp.val = list.get(i);
temp=temp.next;
}
return head;
}
}
自顶向下的归并算法
空间复杂度O(logN)
主要思路是分成两段 交给merge 再合并 简单的分治
但是 分成两段的过程需要注意:中点在偏后面,因为这是单链表,这样才能包含中点到前面list;终止条件也需要注意:要断开和后面的链接,让其成为一个单独的节点再合并
class Solution {
public ListNode sortList(ListNode head) {
//思考一下 归并排序咋写
//自顶向下
return sortList(head,null);
}
public ListNode sortList(ListNode head,ListNode tail) {
if(head==null){
return head;
}
if(head.next==tail){
head.next=null;
return head;
}
//这样slow在奇数的时候停在中间的后面一个,偶数的时候停在中心线的后面一个
ListNode slow=head,fast=head;
while(fast!=tail){
slow=slow.next;
fast=fast.next;
if(fast!=tail){
fast=fast.next;
}
}
return merge(sortList(head,slow),sortList(slow,tail));
}
//合并的时候排序两个list
public ListNode merge(ListNode list1,ListNode list2) {
ListNode dummyHead = new ListNode(0);
ListNode head = dummyHead;
ListNode temp1 = list1,temp2 = list2;
while(temp1!=null&&temp2!=null){
if(temp1.val<=temp2.val){
head.next=temp1;
temp1=temp1.next;
}else{
head.next=temp2;
temp2=temp2.next;
}
head=head.next;
}
if(temp1!=null){
head.next=temp1;
}else{
head.next=temp2;
}
return dummyHead.next;
}
}
自下向上的归并算法
空间复杂度O(1)
思路就是先按照小的分组,拿1个节点和1个节点合并(这个合并需要先拆开 然后merge 再连起来),对整个链表都这么做,可以得到每个长度为2的子链表都是有序的,然后拿2个节点和2个节点合并...长度每次翻倍,反复这么做,直到长度*2后比原长度大了 就结束了。
时间复杂度可以想象成完全二叉树树从下面往上走,每走一次合并一步,每个for走一层,和自上而下的时间复杂度相同也是O(NlogN).
但是在常数方便并不一样,从上图可知自下向上的时间更长,观察代码,最外层的for是O(logN),merge是O(N),而找到满足merge的链表需要遍历和merge遍历一样的长度,因此常数项应当是(2N)*logN,所以最终时间复杂度依旧是O(NlogN)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
//链表题都要加这个捏
if (head == null) {
return head;
}
//获取链表总长度
ListNode a = head;
int length = 0;
while(a!=null){
a=a.next;
length++;
}
ListNode dummyHead = new ListNode(0,head);
//pre用来控制 将链表拆开merge合并后 再把它们连到部分有序的链表后面
//subLength每次循环长度翻倍
for(int subLength=1;subLength<length;subLength<<=1){
//每一次for循环里面 都要从头开始遍历到尾
ListNode pre = dummyHead, cur = dummyHead.next;
while(cur!=null){
ListNode head1 = cur;
for(int i=1;i<subLength&&cur.next!=null;i++){
cur=cur.next;
}
ListNode head2 = cur.next;
//第一个list和后面断开
cur.next=null;
//这里cur有可能是null
cur=head2;
for(int i=1;i<subLength&&cur!=null&&cur.next!=null;i++){
cur=cur.next;
}
//如果cur为Null的话 next为mull 可以终止循环了
ListNode next=null;
if(cur!=null){
next=cur.next;
//第二个list和后面断开
cur.next=null;
}
//合并
ListNode merged = merge(head1,head2);
//把断开的有序链表连回去
pre.next=merged;
//找到下一个应该链接的地方 因为第二个list断开了 所以可以找到
while(pre.next!=null){
pre=pre.next;
}
//cur变为断开的下一个节点
cur=next;
}
}
return dummyHead.next;
}
//合并的时候排序两个list
public ListNode merge(ListNode list1,ListNode list2) {
ListNode dummyHead = new ListNode(0);
ListNode head = dummyHead;
ListNode temp1 = list1,temp2 = list2;
while(temp1!=null&&temp2!=null){
if(temp1.val<=temp2.val){
head.next=temp1;
temp1=temp1.next;
}else{
head.next=temp2;
temp2=temp2.next;
}
head=head.next;
}
if(temp1!=null){
head.next=temp1;
}else{
head.next=temp2;
}
return dummyHead.next;
}
}
剑指 Offer II 078. 合并排序链表
给定一个链表数组,每个链表都已经按升序排列。
请将所有链表合并到一个升序链表中,返回合并后的链表。
笨方法 直接遍历
最笨方案 用一个head 从头merge到尾 因为head 会越来越大 所以越来越慢 可以用归并来优化
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0){
return null;
}
ListNode head = null;
for(int i=0;i<lists.length;i++){
head = merge(head,lists[i]);
}
return head;
}
//合并的时候排序两个list
public ListNode merge(ListNode list1,ListNode list2) {
ListNode dummyHead = new ListNode(0);
ListNode head = dummyHead;
ListNode temp1 = list1,temp2 = list2;
while(temp1!=null&&temp2!=null){
if(temp1.val<=temp2.val){
head.next=temp1;
temp1=temp1.next;
}else{
head.next=temp2;
temp2=temp2.next;
}
head=head.next;
}
if(temp1!=null){
head.next=temp1;
}else{
head.next=temp2;
}
return dummyHead.next;
}
}
归并的merge
通过归并 让每次的合并链表长度一样长,而不是从头到尾那种到后面每次归并长度接近O(n),归并的合并长度最长也就N/2,因此省了很多merge的时间
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0){
return null;
}
return sort(lists,0,lists.length-1);
}
public ListNode sort(ListNode[] lists,int l,int r){
if(l==r){
return lists[l];
}
int mid = (l+r)/2;
ListNode x = sort(lists,l,mid);
ListNode y = sort(lists,mid+1,r);
return merge(x,y);
}
//合并的时候排序两个list
public ListNode merge(ListNode list1,ListNode list2) {
ListNode dummyHead = new ListNode(0);
ListNode head = dummyHead;
ListNode temp1 = list1,temp2 = list2;
while(temp1!=null&&temp2!=null){
if(temp1.val<=temp2.val){
head.next=temp1;
temp1=temp1.next;
}else{
head.next=temp2;
temp2=temp2.next;
}
head=head.next;
}
if(temp1!=null){
head.next=temp1;
}else{
head.next=temp2;
}
return dummyHead.next;
}
}
剑指 Offer II 079. 所有子集
给定一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
回溯解法
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
int n;
public List<List<Integer>> subsets(int[] nums) {
n=nums.length;
dfs(nums,0);
return res;
}
public void dfs(int[] nums,int i){
if(i==n){
res.add(new ArrayList<>(temp));
return;
}
//选择添加i位置元素 此时0~i-1位置都已经固定
temp.add(nums[i]);
//求解i~n位置内容
dfs(nums,i+1);
temp.remove(temp.size()-1);
//选择不添加i位置元素
dfs(nums,i+1);
}
}
感觉回溯的题目还是做得少,要注意选择,对问题要更加的抽象。比如这个题只需要考虑要不要添加当前位置的元素,两个方向的选择,有点像dp也。
二进制解法
通过二进制与十进制的关系来巧妙地完成运算
class Solution {
public List<List<Integer>> subsets(int[] nums) {
int n=nums.length;
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
//假设二进制的情况是011 mask代表3
for(int mask=0;mask<(1<<n);mask++){
//这里是遍历mask 011中对应位置的1存入temp中
for(int i=0;i<n;i++){
//让011一直右移判断是否是1
if((mask&(1<<i))!=0){
temp.add(nums[i]);
}
}
res.add(new ArrayList<>(temp));
temp.clear();
}
return res;
}
}
这两个解法时间都为O(2N*N) 有O(2N)个可能解,每个解需要O(N)的时间来构造 空间都为O(N)
剑指 Offer II 080. 含有 k 个元素的组合
给定两个整数 n
和 k
,返回 1 ... n
中所有可能的 k
个数的组合。
自己的写法 可优化的回溯
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
dfs(n+1,k,1,0);
return res;
}
//i用来记录走到哪了 last记录还能要几个
public void dfs(int n,int k,int i,int last){
if(i==n||last==k){
if(temp.size()==k){
res.add(new ArrayList<>(temp));
}
return;
}
temp.add(i);
dfs(n,k,i+1,last+1);
temp.remove(temp.size()-1);
dfs(n,k,i+1,last);
}
}
这里可以优化的点在于 last其实就是list的大小,可以去掉,然后还有一个剪枝内容,放到下个解法去讲。
回溯的最优解
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
dfs(n+1,k,1);
return res;
}
//i用来记录走到哪了 last记录还能要几个
public void dfs(int n,int k,int i){
//剪枝:当List中的个数+剩余的个数< k时,是必定凑不成合理的答案的
if((temp.size()+n-i)<k){
return;
}
//当大小=k时 直接返回
if(temp.size()==k){
res.add(new ArrayList<>(temp));
return;
}
//取消了i==n时的返回,因为如果cur=n+1时,size>k不可能存在,size==k在第二个if弹出
//size<k时,一定会在cur<n+1的时候不满足足够多的剩余数字条件被第一个if弹出
temp.add(i);
dfs(n,k,i+1);
temp.remove(temp.size()-1);
dfs(n,k,i+1);
}
}
我们需要多考虑剪枝,比如当List中的个数+剩余的个数< k时,是必定凑不成合理的答案的,以及不可能触发的if语句也要去掉。
其实还有一个二进制解法 但是过于麻烦 建议读者去题目处看官方题解