02基础问题入门

80 阅读11分钟

02基础问题入门

1、斐波那契数

题目简介:

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
示例:
输入: n = 2
输出: 1
解释: F(2) = F(1) + F(0) = 1 + 0 = 1

题解:

1、首先我们需要用到一个一维dp数组来保存递归的结果,dp[i]的含义就是第i个斐波那契数的数值是dp[i]。

2、而递推公式题目中已经明显的给出了,就是dp[i] = dp[i - 1] + dp[i - 2];

3、而关于dp数组的初始化,题目中也直接给出了:dp[0] = 0;dp[1] = 1;

4、关于遍历的顺序,应该采用顺序遍历,因为如果我们要想知道第i个斐波那契数,就必须知道它的前两个数值,因此遍历顺序是从前至后,顺序递归。

5、最后的dp[i]就是我们的第i个斐波那契数了。

    public int fib(int N) {
        if (N <= 1) return N;    //设置终止条件:如果给定位置是初始化位置,则直接返回
        int[] dp = new int[N + 1];    //创建一个dp数组,大小为:给定的N值 + 1
        dp[0] = 0; dp[1] = 1;    //初始化dp数组前两位,也就是斐波那契数列的前两位为:0、1
        for (int i = 2; i <= N; ++i) {    //从第2位开始求dp数组,直到N
            dp[i] = dp[i - 1] + dp[i - 2];    //而当前的dp[i] = 前两位之和
        }
        return dp[N];    //最后返回dp[N]的值,即为N位置的斐波那契数
    }

优化思路:

可以对上面解法进行空间上的进一步优化,由于当前数字只跟前两个数字有关,所以不需要保存整个数组,而是只需要保存前两个数字就行了,前一个数字用b表示,再前面的用a表示。a和b分别初始化为0和1,代表数组的前两个数字。然后从位置2开始更新,先算出a和b的和 sum,然后a更新为b,b更新为 sum。最后返回b即可。

    public int fib(int N) {
        if (N <= 1) return N;
        int a = 0, b = 1;    //用a表示第一位数,b表示第二位数
        for (int i = 2; i <= N; ++i) {    //同样从第二位开始求
            int sum = a + b;    //定义sum为:第一位的值 + 第二位的值
            a = b;    //更新a为b
            b = sum;    //更新b为此时的sum
        }
        return b;    //最后返回b的值
    }

2、爬楼梯

题目简介:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例:
输入: n = 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

题解:

1、首先定义一个一维数组dp来记录不同楼层的状态,dp[i]就代表爬到第i层有多少种方式。

2、通过题目我们知道,一次只有两种爬法,爬一楼或者爬两楼,因此dp[i]可以从两个方向推导出来,即dp[i-1]和dp[i-2],而dp[i-1]代表到达dp[i]前一楼的爬法,dp[i-2]代表到达dp[i]前两楼的爬法,二者之和就是dp[i]的方法之和

3、因为到达第一楼只有一种方式,而到达第二楼有两种方式,因此初始化dp[1]为1,dp[2]为2。

4、同样的,这道题和斐波那契一样,需要通过统计前面的方法来计算出后面的方法,因此是顺序遍历。

5、最后的dp[i]就是我们的到达第i层楼的方式和。

    public int climbStairs(int N) {
        if (N <= 1) return 1;    //设置终止条件,如果楼层数小于等于1,只有1种爬法
        int[] dp = new int[N+1];    //创建dp数组,大小为:N+1
        dp[1] = 1;dp[2] = 2;    //因为爬第一楼有1种方法,爬第二楼有2种方法
        int i;
        for(i = 3;i <= N;i++){    //我们所求的楼层应该从第三层开始,直至第N层
            dp[i] = dp[i-1] + dp[i-2];    //每层楼的方法数等于到达它前一楼的方法数 + 到达它前二楼的方法数
        }
        return dp[N];    //最后返回代表第N层楼的方法数dp[N]
    }

3、使用最小花费爬楼梯

题目简介:

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你计算并返回达到楼梯顶部的最低花费。

示例:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

题解:

题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。

1、定义一个一维数组dp来记录状态,dp[i]表示到达第i台阶所花费的最少体力为dp[i]。

2、同样的,因为题目中已经明确指出一次只能跳一楼或两楼,因此我们如果要计算dp[i]的值可以通过计算dp[i-1]和dp[i-2]来获得,而如果从dp[i-1]跳到dp[i],需要花费的体力为:dp[i-1] + cost[i-1];而dp[i-1]到dp[i]花费的体力为:dp[i-2] + cost[i-2]。那究竟是从dp[i-1]起跳呢还是dp[i-2]呢,我们所要选择的应当是二者中的较小者,即min((dp[i-1] + cost[i-1]),(dp[i-2] + cost[i-2]))

3、如题目自述一样,我们到达第0个台阶或者第1个台阶花费的体力为0,因此初始化dp[0] = 0,dp[1] = 0。

4、当然这也是一个由前至后的顺序推导问题,因此顺序遍历。

5、我们只需要返回dp[len]就行,因为计算的是到达楼顶的最小花费。

    public int minCostClimbingStairs(int[] cost) {
            int len = cost.length;    //获取cost数组长度len,也就是总共楼高
            int[] dp = new int[len + 1];    //创建dp数组,大小为len + 1
            dp[0] = 0;dp[1] = 0;    //当我们跳第0台阶或者第1台阶时不花费体力
            for (int i = 2; i <= len; i++) {    //从第2个台阶开始计算,直到楼顶len
                dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);    //到达当前台阶i的最小花费dp[i]等于: (到达上一层最小花费+此层楼起跳花费)和(到达上上一层楼最小话费+此层楼起跳花费)两者中的较小者
            }
            return dp[len];    //最后返回dp[len],表示到达第len台阶时的最小花费
    }

4、不同路径I

题目简介:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少条不同的路径? 不同路径1.png

示例:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

题解:

机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。

1、确定dp[]数组,dp[i][j]表示从(0,0)出发,到达(i,j)有dp[i][j]条不同的路径。

2、因为机器人的移动只能向下或者向右,因此求dp[i][j]可以通过求dp[i-1][j]和dp[i][j-1]之和得出。

3、因为从(0, 0)的位置到(i, 0)的路径只有一条,毕竟机器人只能向右或向下,那么dp[0][j]也同理,所以dp[i][0]和dp[0][j]都赋值为1.因此递推公式为:dp[i][j] = dp[i-1][j]+dp[i][j-1]。

4、dp[i][j]是从上方和左方获得,因此也是从左到右顺序遍历即可。

5、最后返回dp[i-1][j-1]的值即表示到达(i,j)这个点的总路径。

    public static int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];    //创建一个二维数组dp,记录每个坐标的状态
        for (int i = 0; i < m; i++) {    //将第一列全部赋值为1,代表一直向下走
            dp[i][0] = 1;
        }
        for (int i = 0; i < n; i++) {    //将第一行全部赋值为1,代表一直向右走
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; i++) {    //i代表纵坐标,从1开始
            for (int j = 1; j < n; j++) {    //j代表横坐标,从1开始
                dp[i][j] = dp[i-1][j]+dp[i][j-1];    //当前坐标点的路径总数 = 上一个点的路径总数 + 左边一个点的路径总数
            }
        }
        return dp[m-1][n-1];    //最后返回dp[m-1][n-1],表示到达(m,n)的路径总数
    }

5、不同路径II

题目简介:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? 网格中的障碍物和空位置分别用 1 和 0来表示。

不同路径2.png

示例:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

题解:

1、确定dp数组,dp[i][j]表示从(0,0)到(i,j)有dp[i][j]条路径。

2、虽然这里和上一道题的递推公式差不多,但这道题存在障碍,因此需要在进行递推之前进行障碍判断,即判断当前这个点的值是否为0,如果是0代表不是障碍,则进行递推公式dp[i][j] = dp[i-1][j] + dp[i][j-1]。

3、本质上其实还是和上一道题的初始化差不多,但是此题多了障碍,于是有一个要考虑的点,就是在第一行的障碍之后和第一列障碍之下的路是走不通的。因此要把障碍之后的路赋值为0.因此在遍历第一行或者第一列的时候要加上限制条件,即判断当前点的值是否为0。

4、同样也是从左至右顺序遍历。

5、最后返回dp[i-1][j-1]表示(i,j)点的路径。

    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;    //m为列数
        int n = obstacleGrid[0].length;    //n为行数
        int[][] dp = new int[m][n];    //创建dp二维数组,大小为m*n
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {    //如果在起点处或者终点处遇到障碍,直接返回0
            return 0;
        }
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {    //初始化第一列并判断当前点是否为障碍,遇到障碍就跳出停止初始化
            dp[i][0] = 1;
        }
        for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {    //初始化第一行并判断当前点是否为障碍,遇到障碍就跳出停止初始化
            dp[0][j] = 1;
        }
        for (int i = 1; i < m; i++) {    //遍历整个坐标图
            for (int j = 1; j < n; j++) {
                dp[i][j] = (obstacleGrid[i][j] == 0) ? dp[i - 1][j] + dp[i][j - 1] : 0;    //首先是判断当前点是否为障碍,如果不是则将上一个的路径和左边一个的路径之和赋值给它,如果是则直接赋值为0
            }
        }
        return dp[m - 1][n - 1];    //最后返回dp[m-1][n-1],代表到达(m,n)点的路径数
    }

6、整数拆分

题目简介:

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。

示例:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

题解:

1、确定dp数组,dp[i]表示分拆数字i可以得到的最大乘积为dp[i]。数组大小为 n+1,值均初始化为1,因为正整数的乘积不会小于1。

2、确定递推公式:可以从3开始遍历,因为n是从2开始的,而2只能拆分为两个1,乘积还是1。i从3遍历到n,对于每个i,需要遍历所有小于i的数字,因为这些都是潜在的拆分情况,对于任意小于i的数字j,首先计算拆分为两个数字的乘积,即j乘以 i-j,然后是拆分为多个数字的情况,这里就要用到 dp[i-j] 了,这个值表示数字 i-j 任意拆分可得到的最大乘积,再乘以j就是数字i可拆分得到的乘积,取二者的较大值来更新 dp[i]。即dp[i] = max(dp[i],max((i-j) * j,dp[i-j] * j))。

3、dp的初始化,因为0和1不能拆分,因此初始化无意义,我们从2开始初始化,即dp[2] = 1.

4、确定遍历顺序:dp[i]是依靠dp[i-j]确定的,因此必须是顺序遍历。先有dp[i - j]再有dp[i]。

5、距离推导dp数组

    public int integerBreak(int n) {    
        int[] dp = new int[n+1];    //创建dp数组,大小为n+1
        dp[2] = 1;    //初始化最小可拆分数2,其最大乘积为1
        for(int i = 3; i <= n; i++) {    //i代表我们查找的那个数,从3开始,直到n
            for(int j = 1; j <= i-j; j++) {    //j代表可拆分序列,最大值为 i-j,再大只不过是重复而已
                dp[i] = Math.max(dp[i], Math.max(j*(i-j), j*dp[i-j]));    //j * (i - j) 是单纯的把整数 i 拆分为两个数 也就是 i,i-j ,再相乘;而j * dp[i - j]是将 i 拆分成两个以及两个以上的个数,再相乘。最后再与自身比较返回三者中的最大值。
            }
        }
        return dp[n];    //最后返回dp[n]代表n这个数最大可拆分的乘积
    }

7、不同的二叉搜索树

题目简介:

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。 二插.png

示例:
输入: n = 3
输出: 5

题解:

1、确定dp数组:dp[i]表示i个不同元素节点组成的二叉搜索树的个数为dp[i]。

2、确定递推公式:dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量],j相当于是头结点的元素,从1遍历到i为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量。

3、dp的初始化:从递归公式来看,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。所以初始化dp[0] = 1

4、确定遍历顺序:从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。那么遍历i里面每一个数作为头结点的状态,用j来遍历。

5、5. 举例推导dp数组。

    public int numTrees(int n) {
        int[] dp = new int[n + 1];    //创建一个dp数组,大小为n+1
        dp[0] = 1;dp[1] = 1;    //当0个节点和1个节点时,有1种情况,因此赋值为1
        for (int i = 2; i <= n; i++) {    //i表示节点数量,从2个起,直到n
            for (int j = 1; j <= i; j++) {    //j表示i下包含多少个节点,最少为1,最大不超过i
                dp[i] += dp[j - 1] * dp[i - j];    //对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加;一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
            }
        }
        return dp[n];    //返回dp[n]表示有n个节点是2有dp[n]种情况
    }