数学思维编程练习总结

目录

一、数学思维在编程中的应用分析

二、数学思维应用举例

(一)举例应用案例对性能提升的提升

(二)具体代码应用思维对比举例

传统三重循环法

Strassen算法

三、具体编程练习

1、Majority Element(求众数)

2、Majority Element II(求众数扩展)

3、Happy Number(快乐数)

4、Ugly Number(找到丑数)

5、Ugly Number(是否为丑数)

6、Palindrome Number(回文数)

7、Count Primes(计数质数)

8、Valid Number(有效数字)

9、Reverse Integer(整数反转)

10、Roman to Integer(罗马数字转整数)

11、BinaryOneNumbers(二进制中1的个数)

12、Form1ToNOneNumbers(从1到n整数中1出现的次数)

13、AddTwoIntegers(不用加减乘除做加法)

14、DivideTwoIntegers(两数相除)

15、SumForm1ToN(求1+2+3+···+n数字之和)

16、GCDAndLCM(最大公约数和最小公倍数)

17、Fraction Addition and Subtraction(分数加减运算)

18、ComplexNumber(实现两个复数的四则运算)

19、Complex Number Multiplication(复数乘法)

20、Factorial Trailing Zeroes(阶乘后的零)

21、Valid Perfect Square(有效的完全平方数)

22、Sqrt(x)(x的平方根)

23、Pow(x, n)(Pow函数,计算x的n次幂)

24、Super Pow(超级次方)

25、Fraction to Recurring Decimal(分数转循环小数)

26、Integer Replacement(整数替换)

27、Integer Break(整数拆分)

28、Arithmetic Slices(等差数列划分)

29、Beautiful Arrangement(漂亮数组)

30、Minimum Moves to Equal Array Elements(将数组元素相等的最小移动次数)

31、Maximum Points You Can Obtain from Cards(从卡牌中获得的最大点数)

32、Excel Sheet Column Title(Excel表列名称)

33、Excel Sheet Column Number(Excel表列序号)


干货分享,感谢您的阅读!

一、数学思维在编程中的应用分析

数学思维在编程中是非常重要的,它可以帮助我们解决各种问题并优化代码。下面是数学思维在编程中的一些应用分析:

综上所述,数学思维在编程中扮演着至关重要的角色。它不仅能帮助我们解决各种问题,还能优化代码和算法,提高程序的效率和可靠性。因此,作为程序员,拥有扎实的数学基础和数学思维是非常有益的。

二、数学思维应用举例

(一)举例应用案例对性能提升的提升

当应用数学思维解决问题时,有时可以带来性能提升。以下是几个案例来说明在编程中应用数学思维如何优化性能:

  1. 快速幂算法(Exponentiation by Squaring):在计算一个数的幂时,可以利用快速幂算法,将复杂度从O(n)降低到O(log n)。这种算法通过将幂次进行二进制拆解,避免了重复计算,从而大幅提升了计算速度。
  2. 素数判定与筛法:在判断一个数是否为素数时,可以使用数学方法如试除法或Miller-Rabin素性测试,通过选择合适的算法来减少不必要的计算,提高判定效率。同时,在生成素数序列时,也可以使用筛法(如埃拉托斯特尼筛法)来优化性能。
  3. 矩阵乘法优化:在处理大规模矩阵乘法时,使用传统的三重循环会导致O(n^3)的时间复杂度。但是,通过数学优化技巧,如Strassen算法和Coppersmith-Winograd算法,可以将矩阵乘法的时间复杂度降低到O(n^2.81)甚至更低,从而提高运算速度。
  4. 数据压缩:数学方法在数据压缩中也有广泛应用。例如,哈夫曼编码和算术编码是常用的数据压缩算法,它们利用数学统计方法来优化数据存储,从而实现更高的压缩率和更快的数据传输速度。
  5. 多项式求解:在处理多项式求解问题时,使用数学的牛顿迭代法或二分法,可以加速根的查找过程。这在图形学和计算机视觉中经常用于求解方程组或优化问题。

通过上述案例,我们可以看到,应用数学思维在编程中可以带来显著的性能提升。数学技巧可以减少不必要的计算和重复操作,优化算法复杂度,从而提高程序的效率和性能。因此,在编程过程中充分利用数学思维,将有助于解决复杂问题并实现更高效的程序。

(二)具体代码应用思维对比举例

当处理大规模矩阵乘法时,使用传统的三重循环会导致O(n^3)的时间复杂度。但是,通过数学优化技巧,如Strassen算法,可以将矩阵乘法的时间复杂度降低到O(n^2.81)。

下面我们将通过一个具体的示例代码来对比传统的矩阵乘法和使用Strassen算法优化后的矩阵乘法性能。

传统三重循环法

package org.zyf.javabasic.letcode.math.base;

import java.util.Random;

/**
 * @program: zyfboot-javabasic
 * @description: 传统三重循环法
 * @author: zhangyanfeng
 * @create: 2023-06-17 23:22
 **/
public class TraditionalMatrixMultiplication {
    public static void main(String[] args) {
        int n = 2000; // 增大矩阵规模以更好地测试性能
        int[][] A = generateRandomMatrix(n);
        int[][] B = generateRandomMatrix(n);

        long startTime = System.nanoTime();
        int[][] C = traditionalMatrixMultiplication(A, B);
        long endTime = System.nanoTime();

        System.out.println("传统方法耗时: " + (endTime - startTime) / 1e6 + " 毫秒");
    }

    public static int[][] traditionalMatrixMultiplication(int[][] A, int[][] B) {
        int n = A.length;
        int[][] C = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < n; k++) {
                    C[i][j] += A[i][k] * B[k][j];
                }
            }
        }
        return C;
    }

    public static int[][] generateRandomMatrix(int n) {
        Random rand = new Random();
        int[][] matrix = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                matrix[i][j] = rand.nextInt(10);
            }
        }
        return matrix;
    }
}

Strassen算法

package org.zyf.javabasic.letcode.math.base;

import java.util.Random;

/**
 * @program: zyfboot-javabasic
 * @description: Strassen算法
 * @author: zhangyanfeng
 * @create: 2023-06-17 23:34
 **/
public class StrassenMatrixMultiplication {
    private static final int THRESHOLD = 1000; // 阈值

    public static void main(String[] args) {
        int n = 2000; // 增大矩阵规模以更好地测试性能
        int[][] A = generateRandomMatrix(n);
        int[][] B = generateRandomMatrix(n);

        long startTime = System.nanoTime();
        int[][] C = strassenMatrixMultiplication(A, B);
        long endTime = System.nanoTime();

        System.out.println("Strassen算法耗时: " + (endTime - startTime) / 1e6 + " 毫秒");
    }

    public static int[][] strassenMatrixMultiplication(int[][] A, int[][] B) {
        int n = A.length;

        if (n <= THRESHOLD) {
            return traditionalMatrixMultiplication(A, B);
        }

        int newSize = n / 2;
        int[][] A11 = new int[newSize][newSize];
        int[][] A12 = new int[newSize][newSize];
        int[][] A21 = new int[newSize][newSize];
        int[][] A22 = new int[newSize][newSize];
        int[][] B11 = new int[newSize][newSize];
        int[][] B12 = new int[newSize][newSize];
        int[][] B21 = new int[newSize][newSize];
        int[][] B22 = new int[newSize][newSize];

        splitMatrix(A, A11, 0, 0);
        splitMatrix(A, A12, 0, newSize);
        splitMatrix(A, A21, newSize, 0);
        splitMatrix(A, A22, newSize, newSize);
        splitMatrix(B, B11, 0, 0);
        splitMatrix(B, B12, 0, newSize);
        splitMatrix(B, B21, newSize, 0);
        splitMatrix(B, B22, newSize, newSize);

        int[][] M1 = strassenMatrixMultiplication(addMatrices(A11, A22), addMatrices(B11, B22));
        int[][] M2 = strassenMatrixMultiplication(addMatrices(A21, A22), B11);
        int[][] M3 = strassenMatrixMultiplication(A11, subtractMatrices(B12, B22));
        int[][] M4 = strassenMatrixMultiplication(A22, subtractMatrices(B21, B11));
        int[][] M5 = strassenMatrixMultiplication(addMatrices(A11, A12), B22);
        int[][] M6 = strassenMatrixMultiplication(subtractMatrices(A21, A11), addMatrices(B11, B12));
        int[][] M7 = strassenMatrixMultiplication(subtractMatrices(A12, A22), addMatrices(B21, B22));

        int[][] C11 = addMatrices(subtractMatrices(addMatrices(M1, M4), M5), M7);
        int[][] C12 = addMatrices(M3, M5);
        int[][] C21 = addMatrices(M2, M4);
        int[][] C22 = addMatrices(subtractMatrices(addMatrices(M1, M3), M2), M6);

        int[][] C = new int[n][n];
        joinMatrix(C, C11, 0, 0);
        joinMatrix(C, C12, 0, newSize);
        joinMatrix(C, C21, newSize, 0);
        joinMatrix(C, C22, newSize, newSize);

        return C;
    }

    public static void splitMatrix(int[][] P, int[][] C, int iB, int jB) {
        for (int i1 = 0, i2 = iB; i1 < C.length; i1++, i2++) {
            for (int j1 = 0, j2 = jB; j1 < C.length; j1++, j2++) {
                C[i1][j1] = P[i2][j2];
            }
        }
    }

    public static void joinMatrix(int[][] P, int[][] C, int iB, int jB) {
        for (int i1 = 0, i2 = iB; i1 < C.length; i1++, i2++) {
            for (int j1 = 0, j2 = jB; j1 < C.length; j1++, j2++) {
                P[i2][j2] = C[i1][j1];
            }
        }
    }

    public static int[][] addMatrices(int[][] A, int[][] B) {
        int n = A.length;
        int[][] C = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                C[i][j] = A[i][j] + B[i][j];
            }
        }
        return C;
    }

    public static int[][] subtractMatrices(int[][] A, int[][] B) {
        int n = A.length;
        int[][] C = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                C[i][j] = A[i][j] - B[i][j];
            }
        }
        return C;
    }

    public static int[][] generateRandomMatrix(int n) {
        Random rand = new Random();
        int[][] matrix = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                matrix[i][j] = rand.nextInt(10);
            }
        }
        return matrix;
    }

    public static void printMatrix(int[][] matrix) {
        for (int[] row : matrix) {
            for (int val : row) {
                System.out.print(val + " ");
            }
            System.out.println();
        }
    }

    public static int[][] traditionalMatrixMultiplication(int[][] A, int[][] B) {
        int n = A.length;
        int[][] C = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < n; k++) {
                    C[i][j] += A[i][k] * B[k][j];
                }
            }
        }
        return C;
    }
}

Strassen算法的复杂性和开销在处理小矩阵时可能并不如传统的三重循环法高效。这是因为Strassen算法涉及更多的矩阵分割和合并操作,对于小矩阵来说,这些额外的操作可能会抵消其在乘法计算中节省的时间。所以我们在Strassen算法做一些改进和注意事项:

  1. 调整阈值: 在递归到一定深度后,可以切换到传统的矩阵乘法方法,这样可以减少Strassen算法的开销。
  2. 大矩阵测试: Strassen算法在处理大矩阵时,优势更加明显。因此,可以尝试用更大规模的矩阵来测试。
  3. 优化实现: 确保Strassen算法的实现足够优化,例如减少不必要的内存分配和拷贝操作。

通过运行代码结果如下:

传统方法耗时: 10418.161708 毫秒
Strassen算法耗时: 2574.788709 毫秒

我们可以观察到使用Strassen算法优化后的矩阵乘法在大规模矩阵计算中性能表现更优。尤其在矩阵规模较大时,性能提升尤为明显。

三、具体编程练习

1、Majority Element(求众数)

题目描述:给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 n/2 的元素。你可以假设数组是非空的,并且数组中的众数永远存在。

示例 1:输入:[3,2,3]     输出:3

示例 2:输入:[2,2,1,1,1,2,2]     输出:2

解题思路

最优解法可以通过摩尔投票法(Boyer-Moore Voting Algorithm)来实现求众数。摩尔投票法是一种用于求众数的高效算法,基本思想是通过遍历数组,利用投票的方式来找到众数

具体步骤如下:

  1. 初始化两个变量 `candidate` 和 `count`,`candidate` 用于保存当前的候选众数,`count` 用于保存当前候选众数出现的次数,初始值分别为数组的第一个元素和 1。
  2. 遍历数组中的每个元素,如果当前元素和 `candidate` 相同,则 `count` 加1,否则 `count` 减1。
  3. 在遍历过程中,如果 `count` 减为0,说明当前的 `candidate` 不再是候选众数,需要将 `candidate` 更新为当前元素,并将 `count` 重新设置为1。
  4. 遍历完成后,`candidate` 中保存的即为众数。

由于众数的出现次数大于 n/2,所以在整个遍历过程中,众数出现的次数一定大于其他元素。因此,在最后得到的 `candidate` 就是众数。

该算法的时间复杂度为 O(n),其中 n 是数组的大小。因为需要遍历整个数组进行投票计算。而空间复杂度为 O(1),只需要常数级的额外空间来保存候选众数和计数器。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 n/2 的元素。
 * 你可以假设数组是非空的,并且数组中的众数永远存在。
 * <p>
 * 示例 1:输入:[3,2,3]     输出:3
 * 示例 2:输入:[2,2,1,1,1,2,2]     输出:2
 * @date 2023/7/11  23:08
 */
public class MajorityElement {

    /**
     * 最优解法可以通过摩尔投票法(Boyer-Moore Voting Algorithm)来实现求众数。
     * 摩尔投票法是一种用于求众数的高效算法,基本思想是通过遍历数组,利用投票的方式来找到众数。
     * 具体步骤如下:
     * 1. 初始化两个变量 `candidate` 和 `count`,`candidate` 用于保存当前的候选众数,
     * `count` 用于保存当前候选众数出现的次数,初始值分别为数组的第一个元素和 1。
     * 2. 遍历数组中的每个元素,如果当前元素和 `candidate` 相同,则 `count` 加1,否则 `count` 减1。
     * 3. 在遍历过程中,如果 `count` 减为0,说明当前的 `candidate` 不再是候选众数,需要将 `candidate` 更新为当前元素,并将 `count` 重新设置为1。
     * 4. 遍历完成后,`candidate` 中保存的即为众数。
     * 由于众数的出现次数大于 n/2,所以在整个遍历过程中,众数出现的次数一定大于其他元素。因此,在最后得到的 `candidate` 就是众数。
     * 该算法的时间复杂度为 O(n),其中 n 是数组的大小。因为需要遍历整个数组进行投票计算。
     * 而空间复杂度为 O(1),只需要常数级的额外空间来保存候选众数和计数器。
     */
    public static int majorityElement(int[] nums) {
        // 初始化候选众数
        int candidate = nums[0];
        // 初始化候选众数出现的次数
        int count = 1;

        // 遍历数组
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == candidate) {
                // 当前元素和候选众数相同,计数加1
                count++;
            } else {
                // 当前元素和候选众数不同,计数减1
                count--;
                if (count == 0) {
                    // 当计数减为0,更新候选众数为当前元素,并将计数重置为1
                    candidate = nums[i];
                    count = 1;
                }
            }
        }

        return candidate;
    }

    public static void main(String[] args) {
        int[] nums1 = {3, 2, 3};
        int[] nums2 = {2, 2, 1, 1, 1, 2, 2};

        System.out.println("Input: [3, 2, 3] Output: " + majorityElement(nums1));
        System.out.println("Input: [2, 2, 1, 1, 1, 2, 2] Output: " + majorityElement(nums2));
    }
}

2、Majority Element II(求众数扩展)

题目描述:给定一个大小为 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。

示例 1:输入:nums = [3,2,3]  输出:[3]

示例 2:输入:nums = [1]        输出:[1]

示例 3:输入:nums = [1,2]     输出:[1,2]

提示:

  • 1 <= nums.length <= 5 * 104
  • -109 <= nums[i] <= 109

进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1)的算法解决此问题。

解题思路

可以使用著名的 摩尔投票算法的扩展 来解决,该算法通常用于在时间复杂度为 O(n) 和空间复杂度为 O(1) 的条件下,找出数组中超过 n/2 次的元素。为了找出数组中出现次数超过 n/3 的元素,可以进行适当的扩展。

  • 如果一个元素在数组中出现次数超过 n/3,那么最多只能有两个这样的元素。
  • 因此,我们可以使用摩尔投票法的扩展版来找出最多两个候选者。

具体步骤如下:

  1. 初始化两个变量 `candidate` 和 `count`,`candidate` 用于保存当前的候选众数,`count` 用于保存当前候选众数出现的次数,初始值分别为数组的第一个元素和 1。
  2. 遍历数组中的每个元素,如果当前元素和 `candidate` 相同,则 `count` 加1,否则 `count` 减1。
  3. 在遍历过程中,如果 `count` 减为0,说明当前的 `candidate` 不再是候选众数,需要将 `candidate` 更新为当前元素,并将 `count` 重新设置为1。
  4. 遍历完成后,`candidate` 中保存的即为众数。

时间复杂度:O(n),因为数组遍历了两次。

空间复杂度:O(1),因为只使用了常量级别的额外空间。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: zyfboot-javabasic
 * @description: 摩尔投票算法的扩展
 * @author: zhangyanfeng
 * @create: 2023-08-17 22:26
 **/
public class MajorityElementFinder {
    public static List<Integer> majorityElement(int[] nums) {
        List<Integer> result = new ArrayList<>();
        if (nums == null || nums.length == 0) {
            return result;
        }

        // Step 1: Find potential candidates
        int candidate1 = 0, candidate2 = 0;
        int count1 = 0, count2 = 0;

        for (int num : nums) {
            if (num == candidate1) {
                count1++;
            } else if (num == candidate2) {
                count2++;
            } else if (count1 == 0) {
                candidate1 = num;
                count1 = 1;
            } else if (count2 == 0) {
                candidate2 = num;
                count2 = 1;
            } else {
                count1--;
                count2--;
            }
        }

        // Step 2: Verify the candidates
        count1 = 0;
        count2 = 0;

        for (int num : nums) {
            if (num == candidate1) {
                count1++;
            } else if (num == candidate2) {
                count2++;
            }
        }

        int n = nums.length;
        if (count1 > n / 3) {
            result.add(candidate1);
        }
        if (count2 > n / 3) {
            result.add(candidate2);
        }

        return result;
    }

    public static void main(String[] args) {
        // 示例测试
        int[] nums1 = {3, 2, 3};
        System.out.println(majorityElement(nums1)); // 输出:[3]

        int[] nums2 = {1};
        System.out.println(majorityElement(nums2)); // 输出:[1]

        int[] nums3 = {1, 2};
        System.out.println(majorityElement(nums3)); // 输出:[1, 2]
    }
}

3、Happy Number(快乐数)

具体可见散列表相关知识及编程练习总结_张彦峰ZYF的博客-CSDN博客中的第10题。

4、Ugly Number(找到丑数)

具体可见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第13题(主要求解的是:求按从小到大的顺序的第 N 个丑数)。

5、Ugly Number(是否为丑数

题目描述:丑数 就是只包含质因数 23 和 5 的正整数。

给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。

示例 1:输入:n = 6 输出:true 解释:6 = 2 × 3

示例 2:输入:n = 1 输出:true 解释:1 没有质因数,因此它的全部质因数是 {2, 3, 5} 的空集。习惯上将其视作第一个丑数。

示例 3:输入:n = 14 输出:false 解释:14 不是丑数,因为它包含了另外一个质因数 7 。

提示:

  • -231 <= n <= 231 - 1

解题思路

丑数是指仅包含质因数 2、3 和 5 的正整数。因此,我们需要不断地将输入整数 n 除以 2、3 和 5,直到不能整除为止。若最后的结果为 1,则该整数是丑数;否则,不是丑数。

  • 如果 n 小于或等于 0,则 n 不是丑数,因为丑数定义中仅包含正整数。
  • 特殊情况下,1 被视为丑数。

基本步骤

  1. 如果 n <= 0,直接返回 false。
  2. 当 n 能整除 2 时,不断地将 n 除以 2。
  3. 当 n 能整除 3 时,不断地将 n 除以 3。
  4. 当 n 能整除 5 时,不断地将 n 除以 5。
  5. 最后检查 n 是否变为 1,若是,则 n 是丑数,否则不是。

时间复杂度:O(log n),因为每次除法操作都会减小 n,最多执行 O(log n) 次。

空间复杂度:O(1),只使用了常量级别的额外空间。

具体代码展示

package org.zyf.javabasic.letcode.jzoffer;

/**
 * @program: zyfboot-javabasic
 * @description: 判断一个数是否为丑数
 * @author: zhangyanfeng
 * @create: 2024-08-17 23:32
 **/
public class UglyNumberChecker {
    public static boolean isUgly(int n) {
        if (n <= 0) {
            return false;
        }

        // 逐个除以 2、3 和 5
        for (int factor : new int[]{2, 3, 5}) {
            while (n % factor == 0) {
                n /= factor;
            }
        }

        // 检查最终 n 是否为 1
        return n == 1;
    }

    public static void main(String[] args) {
        // 示例测试
        System.out.println(isUgly(6));   // 输出:true
        System.out.println(isUgly(1));   // 输出:true
        System.out.println(isUgly(14));  // 输出:false
    }
}

6、Palindrome Number(回文数)

题目描述:给定一个整数 x,判断它是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例 1:输入:x = 121      输出:true

示例 2:输入:x = -121    输出:false

解释:从左向右读为 "121",从右向左读为 "121-"。因此它不是一个回文数。

示例 3:输入:x = 10       输出:false

解释:从右向左读为 "01"。因此它不是一个回文数。

示例 4:输入:x = -101    输出:false

提示:

-2^31 <= x <= 2^31 - 1

注意:解决此问题时,请考虑整数的边界情况。

解题思路

最优解法可以通过以下步骤来判断一个整数是否为回文数:

  1. 将整数转换为字符串,便于进行字符的比较。
  2. 使用双指针法,一个指针从字符串的左侧开始,另一个指针从右侧开始,逐个字符进行比较。
  3. 如果对应位置的字符相同,则继续比较下一个位置;如果对应位置的字符不同,则说明整数不是回文数,直接返回false。
  4. 如果左指针大于等于右指针,说明整数是回文数,返回true。

该算法的时间复杂度为 O(n),其中 n 为整数的位数,因为最坏情况下需要遍历整个字符串。而空间复杂度为 O(n),需要将整数转换为字符串来存储。

需要注意的是,在转换整数为字符串时,由于整数可能有负号,因此在比较时需要考虑正负号,负号位于字符串的首位,不会影响回文数的判断。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个整数 x,判断它是否是回文数。
 * 回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
 * <p>
 * 示例 1:输入:x = 121      输出:true
 * 示例 2:输入:x = -121    输出:false
 * 解释:从左向右读为 "121",从右向左读为 "121-"。因此它不是一个回文数。
 * 示例 3:输入:x = 10       输出:false
 * 解释:从右向左读为 "01"。因此它不是一个回文数。
 * 示例 4:输入:x = -101    输出:false
 * <p>
 * 提示:
 * -2^31 <= x <= 2^31 - 1
 * 注意:解决此问题时,请考虑整数的边界情况。
 * @date 2023/7/3  23:31
 */
public class PalindromeNumber {

    /**
     * 最优解法可以通过以下步骤来判断一个整数是否为回文数:
     * 1. 将整数转换为字符串,便于进行字符的比较。
     * 2. 使用双指针法,一个指针从字符串的左侧开始,另一个指针从右侧开始,逐个字符进行比较。
     * 3. 如果对应位置的字符相同,则继续比较下一个位置;如果对应位置的字符不同,则说明整数不是回文数,直接返回false。
     * 4. 如果左指针大于等于右指针,说明整数是回文数,返回true。
     * 该算法的时间复杂度为 O(n),其中 n 为整数的位数,因为最坏情况下需要遍历整个字符串。
     * 而空间复杂度为 O(n),需要将整数转换为字符串来存储。
     * 需要注意的是,在转换整数为字符串时,由于整数可能有负号,因此在比较时需要考虑正负号,负号位于字符串的首位,不会影响回文数的判断。
     */
    public static boolean isPalindrome(int x) {
        // 如果整数为负数,直接返回false
        if (x < 0) {
            return false;
        }

        // 将整数转换为字符串
        String str = String.valueOf(x);

        // 左指针
        int left = 0;
        // 右指针
        int right = str.length() - 1;

        // 使用双指针法进行比较
        while (left < right) {
            // 如果对应位置的字符不同,返回false
            if (str.charAt(left) != str.charAt(right)) {
                return false;
            }
            // 继续比较下一个位置
            left++;
            right--;
        }

        // 如果左指针大于等于右指针,说明整数是回文数,返回true
        return true;
    }

    public static void main(String[] args) {
        int x1 = 121;
        int x2 = -121;
        int x3 = 10;
        int x4 = -101;

        System.out.println("Input: " + x1 + " Output: " + isPalindrome(x1));
        System.out.println("Input: " + x2 + " Output: " + isPalindrome(x2));
        System.out.println("Input: " + x3 + " Output: " + isPalindrome(x3));
        System.out.println("Input: " + x4 + " Output: " + isPalindrome(x4));
    }
}

7、Count Primes(计数质数)

题目描述:统计所有小于非负整数 n 的质数的数量。

示例 1:输入:n = 10.     输出:4   解释:小于 10 的质数一共有 2, 3, 5, 7 四个。

示例 2:输入:n = 0       输出:0

示例 3:输入:n = 1        输出:0

提示:

- 0 <= n <= 5 * 10^6

解题思路

最优解法可以通过埃拉托斯特尼筛选法(Sieve of Eratosthenes)来实现计数质数的数量。

埃拉托斯特尼筛选法是一种用于查找质数的经典算法,其基本思想是通过标记法,将不是质数的数排除,最终剩下的即为质数。具体步骤如下:

  1. 创建一个大小为 n 的布尔数组 primes,用于标记每个数字是否是质数。
  2. 初始化数组中的所有元素为 true,表示所有数字都是质数。
  3. 从 2 开始遍历数组,对于每个质数 p,将所有 p 的倍数标记为 false,表示它们不是质数。
  4. 继续遍历数组,直到所有小于 n 的质数和其倍数都被标记为 false。
  5. 统计数组中所有标记为 true 的元素的个数,即为小于 n 的质数的数量。

该算法的时间复杂度为 O(n * log(log(n))),其中 n 是给定的非负整数。因为在每次遍历时,将当前质数的所有倍数标记为 false,而每个合数最多只会被标记一次。而空间复杂度为 O(n),需要额外的布尔数组来标记每个数字是否是质数。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 统计所有小于非负整数 n 的质数的数量。
 * <p>
 * 示例 1:输入:n = 10.     输出:4
 * 解释:小于 10 的质数一共有 2, 3, 5, 7 四个。
 * 示例 2:输入:n = 0       输出:0
 * 示例 3:输入:n = 1        输出:0
 * <p>
 * 提示:
 * - 0 <= n <= 5 * 10^6
 * @date 2023/7/10  23:05
 */
public class CountPrimes {

    /**
     * 最优解法可以通过埃拉托斯特尼筛选法(Sieve of Eratosthenes)来实现计数质数的数量。
     * 埃拉托斯特尼筛选法是一种用于查找质数的经典算法,其基本思想是通过标记法,将不是质数的数排除,最终剩下的即为质数。
     * <p>
     * 具体步骤如下:
     * 1. 创建一个大小为 n 的布尔数组 primes,用于标记每个数字是否是质数。
     * 2. 初始化数组中的所有元素为 true,表示所有数字都是质数。
     * 3. 从 2 开始遍历数组,对于每个质数 p,将所有 p 的倍数标记为 false,表示它们不是质数。
     * 4. 继续遍历数组,直到所有小于 n 的质数和其倍数都被标记为 false。
     * 5. 统计数组中所有标记为 true 的元素的个数,即为小于 n 的质数的数量。
     * 该算法的时间复杂度为 O(n * log(log(n))),其中 n 是给定的非负整数。
     * 因为在每次遍历时,将当前质数的所有倍数标记为 false,而每个合数最多只会被标记一次。
     * 而空间复杂度为 O(n),需要额外的布尔数组来标记每个数字是否是质数。
     */
    public static int countPrimes(int n) {
        if (n <= 2) {
            return 0;
        }

        boolean[] primes = new boolean[n];
        // 初始化数组,假设所有数字都是质数
        for (int i = 2; i < n; i++) {
            primes[i] = true;
        }

        // 埃拉托斯特尼筛选法,从 2 开始遍历
        for (int i = 2; i * i < n; i++) {
            if (primes[i]) {
                // 将当前质数 i 的倍数标记为 false,因为它们不是质数
                for (int j = i * i; j < n; j += i) {
                    primes[j] = false;
                }
            }
        }

        int count = 0;
        // 统计数组中标记为 true 的元素的个数,即为质数的数量
        for (int i = 2; i < n; i++) {
            if (primes[i]) {
                count++;
            }
        }

        return count;
    }

    public static void main(String[] args) {
        int n1 = 10;
        int n2 = 0;
        int n3 = 1;

        System.out.println("Input: " + n1 + " Output: " + countPrimes(n1));
        System.out.println("Input: " + n2 + " Output: " + countPrimes(n2));
        System.out.println("Input: " + n3 + " Output: " + countPrimes(n3));
    }
}

8、Valid Number(有效数字)

题目描述:验证给定的字符串是否可以解释为一个有效的数字。

有效数字可以分成以下几个部分:

  1.  一个 小数 或者 整数
  2. (可选) 一个 'e' 或 'E' ,后面跟着一个 整数

小数(Decimal)可以分成以下几个部分:

  1. (可选) 一个符号字符('+' 或 '-')
  2. 至少一个数字,包括在一个点 '.' 下面的
  3. (可选) 跟着一个点 '.',后面再跟着至少一个数字

整数(Integer)可以分成以下几个部分:

  1.  (可选) 一个符号字符('+' 或 '-')
  2. 至少一个数字

部分有效数字列举如下:

"+100"   "5e2"   "-123"    "3.1416"   "-1E-16"

部分无效数字列举如下:

 "12e"   "1a3.14"    "1.2.3"    "+-5"    "12e+5.4"

说明:

  • 在本题中,没有什么前置或后置的空格。
  • 一个有效的数字(满足上述条件)可以有前导空格或后导空格。
  • 但是,不能有中间空格。

解题思路

最优解法可以通过使用有限状态自动机(Finite State Machine)来实现有效数字的验证。

有限状态自动机是一种表示有限个状态及其状态之间的转移的数学模型。在本题中,可以将有效数字的验证过程抽象为一系列状态和状态之间的转移。

具体步骤如下:

  1. 定义一个状态机,包含以下状态:

       - 空格状态:表示前导空格。

       - 符号状态:表示符号字符。

       - 整数状态:表示整数部分。

       - 小数点状态:表示小数点。

       - 小数状态:表示小数部分。

       - 幂符号状态:表示幂符号 'e' 或 'E'。

       - 幂整数状态:表示幂的整数部分。

       - 后导空格状态:表示后导空格。

  2. 定义状态之间的转移规则,根据输入字符决定状态之间的转移。根据题目要求,可以列举以下情况:

       - 如果当前状态是空格状态,遇到空格继续保持空格状态,遇到符号字符转移到符号状态,遇到数字转移到整数状态,遇到小数点转移到小数点状态。

       - 如果当前状态是符号状态,遇到数字转移到整数状态,遇到小数点转移到小数点状态。

       - 如果当前状态是整数状态,遇到数字继续保持整数状态,遇到小数点转移到小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。

       - 如果当前状态是小数点状态,遇到数字转移到小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。

       - 如果当前状态是小数状态,遇到数字继续保持小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。

       - 如果当前状态是幂符号状态,遇到符号字符转移到符号状态,遇到数字转移到幂整数状态。

       - 如果当前状态是幂整数状态,遇到数字继续保持幂整数状态,遇到空格转移到后导空格状态。

       - 如果当前状态是后导空格状态,遇到空格继续保持后导空格状态。

  3. 定义一个布尔数组,用于表示每个状态是否是接受状态。在本题中,接受状态包括整数状态、小数状态、幂整数状态和后导空格状态。
  4. 依次处理输入字符,并根据转移规则更新状态,如果最终的状态是接受状态,则表示输入字符串是有效的数字。

该算法的时间复杂度为 O(n),其中 n 是输入字符串的长度。因为需要遍历整个输入字符串来进行状态转移。而空间复杂度为 O(1),只需要常数级的额外空间来保存状态和转移规则。

该题还可以见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第22题。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 验证给定的字符串是否可以解释为一个有效的数字。
 * <p>
 * 有效数字可以分成以下几个部分:
 * 1. 一个 小数 或者 整数
 * 2. (可选) 一个 'e' 或 'E' ,后面跟着一个 整数
 * <p>
 * 小数(Decimal)可以分成以下几个部分:
 * 1. (可选) 一个符号字符('+' 或 '-')
 * 2. 至少一个数字,包括在一个点 '.' 下面的
 * 3. (可选) 跟着一个点 '.',后面再跟着至少一个数字
 * <p>
 * 整数(Integer)可以分成以下几个部分:
 * 1. (可选) 一个符号字符('+' 或 '-')
 * 2. 至少一个数字
 * <p>
 * 部分有效数字列举如下:
 * - "+100"
 * - "5e2"
 * - "-123"
 * - "3.1416"
 * - "-1E-16"
 * <p>
 * 部分无效数字列举如下:
 * - "12e"
 * - "1a3.14"
 * - "1.2.3"
 * - "+-5"
 * - "12e+5.4"
 * <p>
 * 说明:
 * - 在本题中,没有什么前置或后置的空格。
 * - 一个有效的数字(满足上述条件)可以有前导空格或后导空格。
 * - 但是,不能有中间空格。
 * @date 2023/7/15  23:34
 */
public class ValidNumber {

    /**
     * 最优解法可以通过使用有限状态自动机(Finite State Machine)来实现有效数字的验证。
     * 有限状态自动机是一种表示有限个状态及其状态之间的转移的数学模型。在本题中,可以将有效数字的验证过程抽象为一系列状态和状态之间的转移。
     * 具体步骤如下:
     * 1. 定义一个状态机,包含以下状态:
     * - 空格状态:表示前导空格。
     * - 符号状态:表示符号字符。
     * - 整数状态:表示整数部分。
     * - 小数点状态:表示小数点。
     * - 小数状态:表示小数部分。
     * - 幂符号状态:表示幂符号 'e' 或 'E'。
     * - 幂整数状态:表示幂的整数部分。
     * - 后导空格状态:表示后导空格。
     * 2. 定义状态之间的转移规则,根据输入字符决定状态之间的转移。根据题目要求,可以列举以下情况:
     * - 如果当前状态是空格状态,遇到空格继续保持空格状态,遇到符号字符转移到符号状态,遇到数字转移到整数状态,遇到小数点转移到小数点状态。
     * - 如果当前状态是符号状态,遇到数字转移到整数状态,遇到小数点转移到小数点状态。
     * - 如果当前状态是整数状态,遇到数字继续保持整数状态,遇到小数点转移到小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。
     * - 如果当前状态是小数点状态,遇到数字转移到小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。
     * - 如果当前状态是小数状态,遇到数字继续保持小数状态,遇到幂符号转移到幂符号状态,遇到空格转移到后导空格状态。
     * - 如果当前状态是幂符号状态,遇到符号字符转移到符号状态,遇到数字转移到幂整数状态。
     * - 如果当前状态是幂整数状态,遇到数字继续保持幂整数状态,遇到空格转移到后导空格状态。
     * - 如果当前状态是后导空格状态,遇到空格继续保持后导空格状态。
     * 3. 定义一个布尔数组,用于表示每个状态是否是接受状态。在本题中,接受状态包括整数状态、小数状态、幂整数状态和后导空格状态。
     * 4. 依次处理输入字符,并根据转移规则更新状态,如果最终的状态是接受状态,则表示输入字符串是有效的数字。
     * 该算法的时间复杂度为 O(n),其中 n 是输入字符串的长度。因为需要遍历整个输入字符串来进行状态转移。
     * 而空间复杂度为 O(1),只需要常数级的额外空间来保存状态和转移规则。
     */
    public static boolean isNumber(String s) {
        // 初始化状态为 0,表示空格状态
        int state = 0;
        // 定义状态之间的转移规则
        int[][] transfer = {
                {0, 1, 6, 2, -1},
                {-1, -1, 6, 2, -1},
                {-1, -1, 3, -1, -1},
                {8, -1, 3, -1, 4},
                {-1, 7, 5, -1, -1},
                {8, -1, 5, -1, -1},
                {8, -1, 6, 3, 4},
                {-1, -1, 5, -1, -1},
                {8, -1, -1, -1, -1}
        };
        // 定义接受状态
        int[] accept = {0, 0, 0, 1, 0, 1, 1, 0, 1};

        // 依次处理输入字符
        for (char c : s.toCharArray()) {
            // 获取字符的类型
            int type = getType(c);
            if (type == -1) {
                // 非法字符,直接返回 false
                return false;
            }
            // 更新状态
            state = transfer[state][type];
            if (state == -1) {
                // 遇到不可转移的状态,表示输入字符串不是有效数字
                return false;
            }
        }

        // 判断最终状态是否是接受状态
        return accept[state] == 1;
    }

    public static int getType(char c) {
        if (c == ' ') {
            // 空格字符
            return 0;
        }
        if (c == '+' || c == '-') {
            // 符号字符
            return 1;
        }
        if (c >= '0' && c <= '9') {
            // 数字字符
            return 2;
        }
        if (c == '.') {
            // 小数点字符
            return 3;
        }
        if (c == 'e' || c == 'E') {
            // 幂符号字符
            return 4;
        }
        // 非法字符
        return -1;
    }

    public static void main(String[] args) {
        String s1 = "0";
        String s2 = " 0.1 ";
        String s3 = "abc";
        String s4 = "1 a";
        String s5 = "2e10";
        String s6 = " -90e3   ";
        String s7 = " 1e";
        String s8 = "e3";
        String s9 = " 6e-1";
        String s10 = " 99e2.5 ";
        String s11 = "53.5e93";
        String s12 = " --6 ";
        String s13 = "-+3";
        String s14 = "95a54e53";

        System.out.println("Input: \"" + s1 + "\" Output: " + isNumber(s1));
        System.out.println("Input: \"" + s2 + "\" Output: " + isNumber(s2));
        System.out.println("Input: \"" + s3 + "\" Output: " + isNumber(s3));
        System.out.println("Input: \"" + s4 + "\" Output: " + isNumber(s4));
        System.out.println("Input: \"" + s5 + "\" Output: " + isNumber(s5));
        System.out.println("Input: \"" + s6 + "\" Output: " + isNumber(s6));
        System.out.println("Input: \"" + s7 + "\" Output: " + isNumber(s7));
        System.out.println("Input: \"" + s8 + "\" Output: " + isNumber(s8));
        System.out.println("Input: \"" + s9 + "\" Output: " + isNumber(s9));
        System.out.println("Input: \"" + s10 + "\" Output: " + isNumber(s10));
        System.out.println("Input: \"" + s11 + "\" Output: " + isNumber(s11));
        System.out.println("Input: \"" + s12 + "\" Output: " + isNumber(s12));
        System.out.println("Input: \"" + s13 + "\" Output: " + isNumber(s13));
        System.out.println("Input: \"" + s14 + "\" Output: " + isNumber(s14));
    }
}

9、Reverse Integer(整数反转)

题目描述:给定一个 32 位有符号整数 x,将整数 x 反转。如果反转后的整数超出了 32 位有符号整数的范围 [-2^31, 2^31 - 1],则返回 0。

注意:假设我们的环境只能存储得下这个 32 位整数的范围,故请不要使用 64 位整数类型。

示例 1:输入:x = 123      输出:321

示例 2:输入:x = -123    输出:-321

示例 3:输入:x = 120      输出:21

示例 4:输入:x = 0          输出:0

提示:

-2^31 <= x <= 2^31 - 1

解题思路

最优解法可以通过以下步骤来实现整数反转:

  1. 初始化一个变量 `result` 用于保存反转后的结果,初始值为0。
  2. 使用循环,将输入整数 `x` 的每一位依次取出,然后加入到 `result` 中。可以使用取模运算和除法运算来获取每一位的值。
  3. 在每一步中,需要检查 `result` 是否会溢出 32 位有符号整数的范围。即检查 `result` 是否小于最小值 `-2^31` 或大于最大值 `2^31 - 1`,如果是,则返回0。
  4. 循环结束后,`result` 就是反转后的整数,将其返回。

在此算法中,不需要使用额外的数据结构,只用了常数级的额外空间,因此是最优解法。同时,算法的时间复杂度为 O(log10(x)),其中 x 是输入整数的位数。这是因为算法的循环次数与输入整数的位数相关。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个 32 位有符号整数 x,将整数 x 反转。
 * 如果反转后的整数超出了 32 位有符号整数的范围 [-2^31, 2^31 - 1],则返回 0。
 * 注意:假设我们的环境只能存储得下这个 32 位整数的范围,故请不要使用 64 位整数类型。
 * <p>
 * 示例 1:输入:x = 123      输出:321
 * 示例 2:输入:x = -123    输出:-321
 * 示例 3:输入:x = 120      输出:21
 * 示例 4:输入:x = 0          输出:0
 * <p>
 * 提示:
 * -2^31 <= x <= 2^31 - 1
 * 最优解法分析
 * @date 2023/7/10  23:27
 */
public class ReverseInteger {
    /**
     * 最优解法可以通过以下步骤来实现整数反转:
     * 1. 初始化一个变量 `result` 用于保存反转后的结果,初始值为0。
     * 2. 使用循环,将输入整数 `x` 的每一位依次取出,然后加入到 `result` 中。可以使用取模运算和除法运算来获取每一位的值。
     * 3. 在每一步中,需要检查 `result` 是否会溢出 32 位有符号整数的范围。
     * 即检查 `result` 是否小于最小值 `-2^31` 或大于最大值 `2^31 - 1`,如果是,则返回0。
     * 4. 循环结束后,`result` 就是反转后的整数,将其返回。
     * 在此算法中,不需要使用额外的数据结构,只用了常数级的额外空间,因此是最优解法。
     * 同时,算法的时间复杂度为 O(log10(x)),其中 x 是输入整数的位数。
     * 这是因为算法的循环次数与输入整数的位数相关。
     */
    public static int reverse(int x) {
        int result = 0;
        while (x != 0) {
            // 获取当前x的最后一位
            int digit = x % 10;
            // 判断反转后是否会溢出范围
            if (result > Integer.MAX_VALUE / 10
                    || (result == Integer.MAX_VALUE / 10 && digit > 7)) {
                return 0;
            }
            if (result < Integer.MIN_VALUE / 10
                    || (result == Integer.MIN_VALUE / 10 && digit < -8)) {
                return 0;
            }
            // 将当前位加入到结果中
            result = result * 10 + digit;
            // 去掉x的最后一位
            x /= 10;
        }
        return result;
    }

    public static void main(String[] args) {
        int x1 = 123;
        int x2 = -123;
        int x3 = 120;
        int x4 = 0;

        System.out.println("Input: " + x1 + " Output: " + reverse(x1));
        System.out.println("Input: " + x2 + " Output: " + reverse(x2));
        System.out.println("Input: " + x3 + " Output: " + reverse(x3));
        System.out.println("Input: " + x4 + " Output: " + reverse(x4));
    }
}

10、Roman to Integer(罗马数字转整数)

题目描述:罗马数字包含以下七种字符:I、V、X、L、C、D 和 M。

字符          数值

I             1

V             5

X             10

L             50

C             100

D             500

M             1000

例如,罗马数字 2 写做 II,即为两个并列的 1。12 写做 XII,即为 X + II。 27 写做 XXVII,即为 XX + V + II。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • - I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • - X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
  • - C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。

示例 1:输入:s = "III"                   输出:3

示例 2:输入:s = "IV"                  输出:4

示例 3:输入:s = "IX"                  输出:9

示例 4:输入:s = "LVIII"              输出:58

解释:L = 50,V = 5,III = 3。

示例 5:输入:s = "MCMXCIV"    输出:1994

解释:M = 1000,CM = 900,XC = 90,IV = 4。

提示:

  • - 1 <= s.length <= 15
  • - s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
  • - 题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内。

解题思路

最优解法可以通过以下步骤来实现罗马数字转整数:

  1. 初始化一个变量 `result` 用于保存最终的整数结果,初始值为0。
  2. 遍历输入的罗马数字字符串,从左到右依次处理每个字符。
  3. 对于每个字符,根据其对应的数值加或减到 `result` 中,同时考虑特殊情况下的组合数值。
  4. 特殊情况下的组合数值是指在罗马数字中,某个较小的字符出现在较大的字符的左侧,此时需要减去较小字符对应的数值,并加上较大字符对应的数值。
  5. 完成整个字符串的遍历后,`result` 即为转换后的整数结果。

该算法的时间复杂度为 O(n),其中 n 是罗马数字字符串的长度,因为需要遍历整个字符串。而空间复杂度为 O(1),只用了常数级的额外空间。

由于罗马数字的规则比较简单,只需一次遍历即可完成转换,所以这个解法是最优解法。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.HashMap;
import java.util.Map;

/**
 * @author yanfengzhang
 * @description 罗马数字包含以下七种字符:I、V、X、L、C、D 和 M。
 * 字符          数值
 * I             1
 * V             5
 * X             10
 * L             50
 * C             100
 * D             500
 * M             1000
 * 例如,罗马数字 2 写做 II,即为两个并列的 1。12 写做 XII,即为 X + II。 27 写做 XXVII,即为 XX + V + II。
 * 通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。
 * 数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
 * - I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
 * - X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
 * - C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
 * 给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。
 * <p>
 * 示例 1:输入:s = "III"                   输出:3
 * 示例 2:输入:s = "IV"                  输出:4
 * 示例 3:输入:s = "IX"                  输出:9
 * 示例 4:输入:s = "LVIII"              输出:58
 * 解释:L = 50,V = 5,III = 3。
 * 示例 5:输入:s = "MCMXCIV"    输出:1994
 * 解释:M = 1000,CM = 900,XC = 90,IV = 4。
 * <p>
 * 提示:
 * - 1 <= s.length <= 15
 * - s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
 * - 题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内。
 * @date 2023/7/5  22:35
 */
public class RomanAndInteger {
    /**
     * 最优解法可以通过以下步骤来实现罗马数字转整数:
     * 1. 初始化一个变量 `result` 用于保存最终的整数结果,初始值为0。
     * 2. 遍历输入的罗马数字字符串,从左到右依次处理每个字符。
     * 3. 对于每个字符,根据其对应的数值加或减到 `result` 中,同时考虑特殊情况下的组合数值。
     * 4. 特殊情况下的组合数值是指在罗马数字中,某个较小的字符出现在较大的字符的左侧,此时需要减去较小字符对应的数值,并加上较大字符对应的数值。
     * 5. 完成整个字符串的遍历后,`result` 即为转换后的整数结果。
     * 该算法的时间复杂度为 O(n),其中 n 是罗马数字字符串的长度,因为需要遍历整个字符串。
     * 而空间复杂度为 O(1),只用了常数级的额外空间。
     */
    public static int romanToInt(String s) {
        // 创建罗马数字字符与对应数值的映射表
        Map<Character, Integer> romanMap = new HashMap<>();
        romanMap.put('I', 1);
        romanMap.put('V', 5);
        romanMap.put('X', 10);
        romanMap.put('L', 50);
        romanMap.put('C', 100);
        romanMap.put('D', 500);
        romanMap.put('M', 1000);

        int result = 0;
        // 用于记录前一个字符的数值
        int prevValue = 0;

        for (int i = s.length() - 1; i >= 0; i--) {
            char currentChar = s.charAt(i);
            int currentValue = romanMap.get(currentChar);

            // 根据当前字符的数值和前一个字符的数值,判断是否需要减去较小字符对应的数值
            if (currentValue < prevValue) {
                result -= currentValue;
            } else {
                result += currentValue;
            }

            // 更新prevValue为当前字符的数值
            prevValue = currentValue;
        }

        return result;
    }

    public static void main(String[] args) {
        String s1 = "III";
        String s2 = "IV";
        String s3 = "IX";
        String s4 = "LVIII";
        String s5 = "MCMXCIV";

        System.out.println("Input: " + s1 + " Output: " + romanToInt(s1));
        System.out.println("Input: " + s2 + " Output: " + romanToInt(s2));
        System.out.println("Input: " + s3 + " Output: " + romanToInt(s3));
        System.out.println("Input: " + s4 + " Output: " + romanToInt(s4));
        System.out.println("Input: " + s5 + " Output: " + romanToInt(s5));
    }
}

11、BinaryOneNumbers(二进制中1的个数)

具体见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第36题。

12、Form1ToNOneNumbers(1n整数中1出现的次数)

具体见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第42题。

13、AddTwoIntegers(不用加减乘除做加法)

具体见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第41题。

14、DivideTwoIntegers(两数相除)

题目描述:给定两个整数,被除数和除数,将被除数除以除数并返回商,不能使用除法、乘法和取模运算。如果结果溢出,则返回最大的有符号32位整数(2^31 - 1)。

例如:输入:被除数 dividend = 10,除数 divisor = 3    输出:商为 3

解题思路

最优解法是使用二进制搜索(Binary Search)来解决这个问题。通过使用位运算和二进制搜索,我们可以高效地找到两数相除的商。

首先,我们将被除数和除数都转换成绝对值,这是因为符号只影响最终的结果而不影响计算过程。然后,我们用二进制搜索的方式来逐步逼近商的值。

我们从被除数中减去除数的倍数,每次将除数左移一位,直到减去除数的倍数小于除数。这样可以确保每次找到最大的倍数。然后,我们记录下找到的倍数,并将剩余的被除数继续进行二进制搜索。直到被除数小于除数时,搜索结束。

最后,我们根据输入的符号来确定最终的商的符号,并根据题目要求检查结果是否溢出。

这种二进制搜索解法的时间复杂度为O(log N),其中N是被除数的绝对值,因为在每次搜索中,我们每次都能将被除数减少一半。空间复杂度为O(1),因为只使用了有限的变量来保存结果。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定两个整数,被除数和除数,将被除数除以除数并返回商,不能使用除法、乘法和取模运算。如果结果溢出,则返回最大的有符号32位整数(2^31 - 1)。
 * 例如:输入:被除数 dividend = 10,除数 divisor = 3    输出:商为 3
 * @date 2023/7/16  23:48
 */
public class DivideTwoIntegers {
    /**
     * 最优解法是使用二进制搜索(Binary Search)来解决这个问题。通过使用位运算和二进制搜索,我们可以高效地找到两数相除的商。
     * 首先,我们将被除数和除数都转换成绝对值,这是因为符号只影响最终的结果而不影响计算过程。然后,我们用二进制搜索的方式来逐步逼近商的值。
     * 我们从被除数中减去除数的倍数,每次将除数左移一位,直到减去除数的倍数小于除数。
     * 这样可以确保每次找到最大的倍数。然后,我们记录下找到的倍数,并将剩余的被除数继续进行二进制搜索。直到被除数小于除数时,搜索结束。
     * 最后,我们根据输入的符号来确定最终的商的符号,并根据题目要求检查结果是否溢出。
     * 这种二进制搜索解法的时间复杂度为O(log N),其中N是被除数的绝对值,
     * 因为在每次搜索中,我们每次都能将被除数减少一半。空间复杂度为O(1),因为只使用了有限的变量来保存结果。
     */
    public static int divide(int dividend, int divisor) {
        // 处理特殊情况:除数为0或者被除数为最小值且除数为-1(会导致溢出)
        if (divisor == 0 || (dividend == Integer.MIN_VALUE && divisor == -1)) {
            // 返回最大有符号32位整数表示无效
            return Integer.MAX_VALUE;
        }

        // 确定商的符号
        boolean negative = (dividend < 0) ^ (divisor < 0);

        // 将被除数和除数都转换为正数处理
        long absDividend = Math.abs((long) dividend);
        long absDivisor = Math.abs((long) divisor);

        int result = 0;
        while (absDividend >= absDivisor) {
            long temp = absDivisor;
            long multiple = 1;
            // 通过二进制搜索,每次找到最大的倍数
            while (absDividend >= (temp << 1)) {
                temp <<= 1;
                multiple <<= 1;
            }
            absDividend -= temp;
            result += (int) multiple;
        }

        // 根据符号确定最终的商的符号
        return negative ? -result : result;
    }

    public static void main(String[] args) {
        int dividend = 10;
        int divisor = 3;
        int result = divide(dividend, divisor);
        // 输出:3
        System.out.println("商为: " + result);
    }
}

15、SumForm1ToN(1+2+3+···+n数字之和)

具体可见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第40题。

16、GCDAndLCM(最大公约数和最小公倍数)

具体可见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第63题。

17、Fraction Addition and Subtraction(分数加减运算)

题目描述:给定一个表示分数加减表达式的字符串,你需要返回一个字符串,表示该表达式的结果。整数部分的分数格式为整数或分数(例如 2,-2,1/2,-1/2),对于这样的表达式中的每一个操作符(+、-、*、/),你都需要给出结果并将结果用分数格式作为最简分数。

整数的除法只保留整数部分。你需要返回一个字符串,其中包含整数部分和分数部分,如果两个部分存在的话。

示例 1:输入: "-1/2+1/2"     输出: "0/1"

示例 2:输入: "-1/2+1/2+1/3"     输出: "1/3"

示例 3:输入: "1/3-1/2"      输出: "-1/6"

示例 4:输入: "5/3+1/3"     输出: "2/1"

提示:

- 输入和输出都是字符串形式的,每个输入表示一个分数表达式,而输出包含整数部分和分数部分。输入的范围是 [-10,000, 10,000]。

解题思路

最优解法可以通过求最小公倍数和通分的方式来实现分数加减运算。具体步骤如下:

  1. 定义一个函数来求两个整数的最大公约数(Greatest Common Divisor,简称GCD)。
  2. 定义一个函数来求两个整数的最小公倍数(Least Common Multiple,简称LCM),根据 GCD 和公式 LCM(a, b) = a * b / GCD(a, b) 计算。
  3. 定义一个函数来对分数进行通分,使其分母相同。通过求得的最小公倍数将分数通分。
  4. 定义一个函数来将两个通分后的分数进行加减运算,并将结果化简成最简分数。

该算法的时间复杂度取决于通分和化简过程中的计算。在本题中,分数通分和化简的时间复杂度可以认为是常数级别。因此,整体的时间复杂度为 O(1)。而空间复杂度为 O(1),只需要常数级的额外空间来保存结果。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个表示分数加减表达式的字符串,你需要返回一个字符串,表示该表达式的结果。
 * 整数部分的分数格式为整数或分数(例如 2,-2,1/2,-1/2),
 * 对于这样的表达式中的每一个操作符(+、-、*、/),你都需要给出结果并将结果用分数格式作为最简分数。
 * 整数的除法只保留整数部分。你需要返回一个字符串,其中包含整数部分和分数部分,如果两个部分存在的话。
 * <p>
 * 示例 1:输入: "-1/2+1/2"     输出: "0/1"
 * 示例 2:输入: "-1/2+1/2+1/3"     输出: "1/3"
 * 示例 3:输入: "1/3-1/2"      输出: "-1/6"
 * 示例 4:输入: "5/3+1/3"     输出: "2/1"
 * <p>
 * 提示:
 * - 输入和输出都是字符串形式的,每个输入表示一个分数表达式,而输出包含整数部分和分数部分。
 * 输入的范围是 [-10,000, 10,000]。
 * @date 2023/7/17  23:51
 */
public class FractionAdditionSubtraction {

    /**
     * 最优解法可以通过求最小公倍数和通分的方式来实现分数加减运算。
     * 具体步骤如下:
     * 1. 定义一个函数来求两个整数的最大公约数(Greatest Common Divisor,简称GCD)。
     * 2. 定义一个函数来求两个整数的最小公倍数(Least Common Multiple,简称LCM),根据 GCD 和公式 LCM(a, b) = a * b / GCD(a, b) 计算。
     * 3. 定义一个函数来对分数进行通分,使其分母相同。通过求得的最小公倍数将分数通分。
     * 4. 定义一个函数来将两个通分后的分数进行加减运算,并将结果化简成最简分数。
     * 该算法的时间复杂度取决于通分和化简过程中的计算。在本题中,分数通分和化简的时间复杂度可以认为是常数级别。
     * 因此,整体的时间复杂度为 O(1)。而空间复杂度为 O(1),只需要常数级的额外空间来保存结果。
     */
    public static String fractionAddition(String expression) {
        // 使用正则表达式拆分分数表达式
        String[] fractions = expression.split("(?=[-+])");
        // 初始化分子为0
        int numerator = 0;
        // 初始化分母为1
        int denominator = 1;

        for (String fraction : fractions) {
            // 获取分数的分子和分母
            int[] parts = getParts(fraction);
            int num = parts[0];
            int den = parts[1];

            // 通分,更新分子和分母
            numerator = numerator * den + num * denominator;
            denominator *= den;

            // 化简分子和分母,保证最简分数
            int gcd = getGCD(Math.abs(numerator), denominator);
            numerator /= gcd;
            denominator /= gcd;
        }

        return numerator + "/" + denominator;
    }

    /**
     * 辅助函数,获取分数的分子和分母
     */
    private static int[] getParts(String fraction) {
        String[] parts = fraction.split("/");
        int numerator = Integer.parseInt(parts[0]);
        int denominator = Integer.parseInt(parts[1]);
        return new int[]{numerator, denominator};
    }

    /**
     * 辅助函数,求两个整数的最大公约数
     */
    private static int getGCD(int a, int b) {
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }

    public static void main(String[] args) {
        String expression1 = "-1/2+1/2";
        String expression2 = "-1/2+1/2+1/3";
        String expression3 = "1/3-1/2";
        String expression4 = "5/3+1/3";

        System.out.println("Input: \"" + expression1 + "\" Output: " + fractionAddition(expression1));
        System.out.println("Input: \"" + expression2 + "\" Output: " + fractionAddition(expression2));
        System.out.println("Input: \"" + expression3 + "\" Output: " + fractionAddition(expression3));
        System.out.println("Input: \"" + expression4 + "\" Output: " + fractionAddition(expression4));
    }
}

18、ComplexNumber(实现两个复数的四则运算)

题目描述:要求实现两个复数的四则运算,包括加法、减法、乘法和除法。

复数由实部和虚部组成,一般表示为a + bi,其中a为实部,b为虚部,i为虚数单位,满足i^2 = -1。

两个复数的四则运算规则如下:

  1. 复数加法:(a + bi) + (c + di) = (a + c) + (b + d)i
  2. 复数减法:(a + bi) - (c + di) = (a - c) + (b - d)i
  3. 复数乘法:(a + bi) * (c + di) = (ac - bd) + (ad + bc)i
  4. 复数除法:(a + bi) / (c + di) = (ac + bd) / (c^2 + d^2) + (bc - ad)i / (c^2 + d^2)

其中,a、b、c和d均为实数。

为了实现这些运算,你可以定义一个复数类,其中包含实部和虚部作为成员变量,并编写方法来进行四则运算。

解题思路

两个复数的四则运算规则如下:

  1. 复数加法:(a + bi) + (c + di) = (a + c) + (b + d)i
  2. 复数减法:(a + bi) - (c + di) = (a - c) + (b - d)i
  3. 复数乘法:(a + bi) * (c + di) = (ac - bd) + (ad + bc)i
  4. 复数除法:(a + bi) / (c + di) = (ac + bd) / (c^2 + d^2) + (bc - ad)i / (c^2 + d^2)

其中,a、b、c和d均为实数。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 要求实现两个复数的四则运算,包括加法、减法、乘法和除法。
 * 复数由实部和虚部组成,一般表示为a + bi,其中a为实部,b为虚部,i为虚数单位,满足i^2 = -1。
 * 两个复数的四则运算规则如下:
 * 1.	复数加法:(a + bi) + (c + di) = (a + c) + (b + d)i
 * 2.	复数减法:(a + bi) - (c + di) = (a - c) + (b - d)i
 * 3.	复数乘法:(a + bi) * (c + di) = (ac - bd) + (ad + bc)i
 * 4.	复数除法:(a + bi) / (c + di) = (ac + bd) / (c^2 + d^2) + (bc - ad)i / (c^2 + d^2)
 * 其中,a、b、c和d均为实数。
 * 为了实现这些运算,你可以定义一个复数类,其中包含实部和虚部作为成员变量,并编写方法来进行四则运算。
 * @date 2023/7/19  23:41
 */
public class ComplexNumber {
    // 实部
    private double real;
    // 虚部
    private double imaginary;

    public ComplexNumber(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // 复数加法
    public ComplexNumber add(ComplexNumber other) {
        return new ComplexNumber(this.real + other.real, this.imaginary + other.imaginary);
    }

    // 复数减法
    public ComplexNumber subtract(ComplexNumber other) {
        return new ComplexNumber(this.real - other.real, this.imaginary - other.imaginary);
    }

    // 复数乘法
    public ComplexNumber multiply(ComplexNumber other) {
        double newReal = this.real * other.real - this.imaginary * other.imaginary;
        double newImaginary = this.real * other.imaginary + this.imaginary * other.real;
        return new ComplexNumber(newReal, newImaginary);
    }

    // 复数除法
    public ComplexNumber divide(ComplexNumber other) {
        double denominator = other.real * other.real + other.imaginary * other.imaginary;
        double newReal = (this.real * other.real + this.imaginary * other.imaginary) / denominator;
        double newImaginary = (this.imaginary * other.real - this.real * other.imaginary) / denominator;
        return new ComplexNumber(newReal, newImaginary);
    }

    @Override
    public String toString() {
        return real + " + " + imaginary + "i";
    }

    public static void main(String[] args) {
        ComplexNumber num1 = new ComplexNumber(3, 2);
        ComplexNumber num2 = new ComplexNumber(1, -1);

        // 四则运算示例并验证
        System.out.println("Number 1: " + num1);
        System.out.println("Number 2: " + num2);
        // 复数加法
        System.out.println("Addition: " + num1.add(num2));
        // 复数减法
        System.out.println("Subtraction: " + num1.subtract(num2));
        // 复数乘法
        System.out.println("Multiplication: " + num1.multiply(num2));
        // 复数除法
        System.out.println("Division: " + num1.divide(num2));
    }

}

19、Complex Number Multiplication(复数乘法)

题目描述:给定两个表示复数的字符串 num1 和 num2,返回两个复数的乘积。字符串表示的复数以 "a+bi" 的形式,其中 a 和 b 是实部和虚部,i 表示虚数单位。

注意:

  1. 输入字符串中的虚数单位 i 以及两个复数的虚部 b 均不包含字符 i。
  2. 输入字符串中的虚数单位 i 仅用于表示虚部。

示例 1:输入:num1 = "1+1i", num2 = "1+1i"     输出:"0+2i"

解释:(1 + i) * (1 + i) = 1 + i + i + i^2 = 1 + 2i

示例 2:输入:num1 = "1+-1i", num2 = "1+-1i"     输出:"0+-2i"

解释:(1 - i) * (1 - i) = 1 - i - i + i^2 = 1 - 2i

提示:

  • - 输入字符串 num1 和 num2 长度均不超过 100。
  • - 输入字符串中的实部和虚部都是整数,取值范围是 [-100, 100]。
  • - 输出结果的实部和虚部也都将表示为整数。

解题思路

最优解法可以通过将复数分割为实部和虚部,然后进行计算得到乘积。

假设两个复数分别为 "a+bi" 和 "c+di",其中 a、b、c 和 d 都是整数。它们的乘积可以展开为:(a+bi) * (c+di) = ac + adi + bci + bdi^2。

因为 i^2 = -1,所以结果可以简化为:(a+bi) * (c+di) = (ac - bd) + (ad + bc)i。

可以看到,实部的计算是 a * c - b * d,虚部的计算是 a * d + b * c。通过这两个计算得到最终的实部和虚部,然后将其拼接为结果字符串。

具体步骤如下:

  1. 将 num1 和 num2 按照 "+" 进行拆分,得到两个数组:[a, bi] 和 [c, di]。
  2. 将 a、b、c 和 d 解析为整数。
  3. 计算实部和虚部的结果:实部为 ac - bd,虚部为 ad + bc。
  4. 将实部和虚部拼接为结果字符串,并返回。

该算法的时间复杂度为 O(1),因为只进行了常数次计算。而空间复杂度为 O(1),因为只需要常数的额外空间。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定两个表示复数的字符串 num1 和 num2,返回两个复数的乘积。字符串表示的复数以 "a+bi" 的形式,其中 a 和 b 是实部和虚部,i 表示虚数单位。
 * 注意:
 * 1. 输入字符串中的虚数单位 i 以及两个复数的虚部 b 均不包含字符 i。
 * 2. 输入字符串中的虚数单位 i 仅用于表示虚部。
 * <p>
 * 示例 1:输入:num1 = "1+1i", num2 = "1+1i"     输出:"0+2i"
 * 解释:(1 + i) * (1 + i) = 1 + i + i + i^2 = 1 + 2i
 * 示例 2:输入:num1 = "1+-1i", num2 = "1+-1i"     输出:"0+-2i"
 * 解释:(1 - i) * (1 - i) = 1 - i - i + i^2 = 1 - 2i
 * <p>
 * 提示:
 * - 输入字符串 num1 和 num2 长度均不超过 100。
 * - 输入字符串中的实部和虚部都是整数,取值范围是 [-100, 100]。
 * - 输出结果的实部和虚部也都将表示为整数。
 * @date 2023/6/28  23:10
 */
public class ComplexNumberMultiplication {

    /**
     * 最优解法可以通过将复数分割为实部和虚部,然后进行计算得到乘积。
     * 假设两个复数分别为 "a+bi" 和 "c+di",其中 a、b、c 和 d 都是整数。
     * 它们的乘积可以展开为:(a+bi) * (c+di) = ac + adi + bci + bdi^2。
     * 因为 i^2 = -1,所以结果可以简化为:(a+bi) * (c+di) = (ac - bd) + (ad + bc)i。
     * 可以看到,实部的计算是 a * c - b * d,虚部的计算是 a * d + b * c。
     * 通过这两个计算得到最终的实部和虚部,然后将其拼接为结果字符串。
     * 具体步骤如下:
     * 1. 将 num1 和 num2 按照 "+" 进行拆分,得到两个数组:[a, bi] 和 [c, di]。
     * 2. 将 a、b、c 和 d 解析为整数。
     * 3. 计算实部和虚部的结果:实部为 ac - bd,虚部为 ad + bc。
     * 4. 将实部和虚部拼接为结果字符串,并返回。
     * 该算法的时间复杂度为 O(1),因为只进行了常数次计算。而空间复杂度为 O(1),因为只需要常数的额外空间。
     */
    public static String complexNumberMultiply(String num1, String num2) {
        // 将 num1 和 num2 按照 "+" 进行拆分,得到两个数组:[a, bi] 和 [c, di]。
        String[] num1Arr = num1.split("\\+");
        String[] num2Arr = num2.split("\\+");

        // 将 a、b、c 和 d 解析为整数。
        int a = Integer.parseInt(num1Arr[0]);
        int b = Integer.parseInt(num1Arr[1].substring(0, num1Arr[1].length() - 1));
        int c = Integer.parseInt(num2Arr[0]);
        int d = Integer.parseInt(num2Arr[1].substring(0, num2Arr[1].length() - 1));

        // 计算实部和虚部的结果:实部为 ac - bd,虚部为 ad + bc。
        int real = a * c - b * d;
        int imag = a * d + b * c;

        // 将实部和虚部拼接为结果字符串,并返回。
        return real + "+" + imag + "i";
    }

    public static void main(String[] args) {
        String num1 = "1+1i";
        String num2 = "1+1i";

        System.out.println("Input: num1 = " + num1 + ", num2 = " + num2 + " Output: " + complexNumberMultiply(num1, num2));
    }

}

20、Factorial Trailing Zeroes(阶乘后的零)

题目描述:给定一个整数 n,返回 n! 结果尾数中零的数量。

示例 1:输入:3.    输出:0

解释:3! = 6,尾数中没有零。

示例 2:输入:5      输出:1

解释:5! = 120,尾数中有一个零。

提示:

- 0 <= n <= 10^4

解题思路

最优解法可以通过统计质因数 5 的个数来计算 n! 结果尾数中零的数量。

在 n! 中,尾数中的零是由质因数 2 和质因数 5 相乘得到的。而在阶乘的过程中,2 的个数一定多于 5 的个数,因为偶数比 5 的倍数多得多。

因此,计算 n! 结果尾数中零的数量,只需要计算质因数 5 的个数即可。

具体步骤如下:

  1. 初始化一个变量 `count` 为 0,用于保存质因数 5 的个数。
  2. 遍历从 1 到 n 的每个整数 i,对每个 i 进行如下操作:

       - 计算 i 的因数中 5 的个数,方法为将 i 除以 5,得到的结果为质因数 5 的个数。

       - 将质因数 5 的个数累加到 `count` 中。

  3. 遍历完成后,`count` 即为 n! 结果尾数中零的数量。

该算法的时间复杂度为 O(log(n)),其中 n 是给定的整数。因为需要遍历从 1 到 n 的每个整数 i 进行计算。而空间复杂度为 O(1),只需要常数级的额外空间来保存计数器。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个整数 n,返回 n! 结果尾数中零的数量。
 * <p>
 * 示例 1:输入:3.    输出:0
 * 解释:3! = 6,尾数中没有零。
 * 示例 2:输入:5      输出:1
 * 解释:5! = 120,尾数中有一个零。
 * <p>
 * 提示:
 * - 0 <= n <= 10^4
 * @date 2023/7/12  23:11
 */
public class FactorialTrailingZeroes {

    /**
     * 最优解法可以通过统计质因数 5 的个数来计算 n! 结果尾数中零的数量。
     * 在 n! 中,尾数中的零是由质因数 2 和质因数 5 相乘得到的。
     * 而在阶乘的过程中,2 的个数一定多于 5 的个数,因为偶数比 5 的倍数多得多。
     * 因此,计算 n! 结果尾数中零的数量,只需要计算质因数 5 的个数即可。
     * 具体步骤如下:
     * 1. 初始化一个变量 `count` 为 0,用于保存质因数 5 的个数。
     * 2. 遍历从 1 到 n 的每个整数 i,对每个 i 进行如下操作:
     * - 计算 i 的因数中 5 的个数,方法为将 i 除以 5,得到的结果为质因数 5 的个数。
     * - 将质因数 5 的个数累加到 `count` 中。
     * 3. 遍历完成后,`count` 即为 n! 结果尾数中零的数量。
     * 该算法的时间复杂度为 O(log(n)),其中 n 是给定的整数。
     * 因为需要遍历从 1 到 n 的每个整数 i 进行计算。
     * 而空间复杂度为 O(1),只需要常数级的额外空间来保存计数器。
     */
    public static int trailingZeroes(int n) {
        // 初始化质因数 5 的个数
        int count = 0;

        // 遍历从 1 到 n 的每个整数 i
        for (int i = 1; i <= n; i++) {
            int num = i;
            // 计算 i 的因数中 5 的个数
            while (num % 5 == 0) {
                count++;
                num /= 5;
            }
        }

        return count;
    }

    public static void main(String[] args) {
        int n1 = 3;
        int n2 = 5;
        int n3 = 10;

        System.out.println("Input: " + n1 + " Output: " + trailingZeroes(n1));
        System.out.println("Input: " + n2 + " Output: " + trailingZeroes(n2));
        System.out.println("Input: " + n3 + " Output: " + trailingZeroes(n3));
    }
}

21、Valid Perfect Square(有效的完全平方数)

题目描述:给定一个正整数 num,编写一个函数判断是否为有效的完全平方数。

说明:不要使用任何内置的库函数,如 sqrt。

示例 1:输入:num = 16      输出:true

示例 2:输入:num = 14      输出:false

提示:

- 1 <= num <= 2^31 - 1

解题思路

最优解法可以通过二分查找来判断一个正整数是否为有效的完全平方数。

具体步骤如下:

  1. 初始化两个变量 `left` 和 `right`,分别表示二分查找的左边界和右边界,初始值为 1 和 `num`。
  2. 在循环中,不断缩小查找范围,直到 `left` 大于等于 `right` 为止:

       - 计算中间值 `mid`,等于 `left` 和 `right` 的平均值。

       - 计算 `mid` 的平方值 `midSquared`。

       - 如果 `midSquared` 等于 `num`,说明 `mid` 是有效的完全平方数,返回 true。

       - 如果 `midSquared` 小于 `num`,说明完全平方数在右半部分,更新 `left` 为 `mid + 1`。

       - 如果 `midSquared` 大于 `num`,说明完全平方数在左半部分,更新 `right` 为 `mid - 1`。

  3. 如果循环结束时仍未找到有效的完全平方数,返回 false。

该算法的时间复杂度为 O(log(num)),其中 num 是给定的正整数。因为每次查找都会将查找范围缩小一半。而空间复杂度为 O(1),只需要常数级的额外空间来保存变量。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个正整数 num,编写一个函数判断是否为有效的完全平方数。
 * 说明:不要使用任何内置的库函数,如 sqrt。
 * <p>
 * 示例 1:输入:num = 16      输出:true
 * 示例 2:输入:num = 14      输出:false
 * <p>
 * 提示:
 * - 1 <= num <= 2^31 - 1
 * @date 2023/7/13  23:14
 */
public class ValidPerfectSquare {

    /**
     * 最优解法可以通过二分查找来判断一个正整数是否为有效的完全平方数。
     * 具体步骤如下:
     * 1. 初始化两个变量 `left` 和 `right`,分别表示二分查找的左边界和右边界,初始值为 1 和 `num`。
     * 2. 在循环中,不断缩小查找范围,直到 `left` 大于等于 `right` 为止:
     * - 计算中间值 `mid`,等于 `left` 和 `right` 的平均值。
     * - 计算 `mid` 的平方值 `midSquared`。
     * - 如果 `midSquared` 等于 `num`,说明 `mid` 是有效的完全平方数,返回 true。
     * - 如果 `midSquared` 小于 `num`,说明完全平方数在右半部分,更新 `left` 为 `mid + 1`。
     * - 如果 `midSquared` 大于 `num`,说明完全平方数在左半部分,更新 `right` 为 `mid - 1`。
     * 3. 如果循环结束时仍未找到有效的完全平方数,返回 false。
     * 该算法的时间复杂度为 O(log(num)),其中 num 是给定的正整数。
     * 因为每次查找都会将查找范围缩小一半。而空间复杂度为 O(1),只需要常数级的额外空间来保存变量。
     */
    public static boolean isPerfectSquare(int num) {
        // 二分查找的左边界
        long left = 1;
        // 二分查找的右边界
        long right = num;

        while (left <= right) {
            // 计算中间值
            long mid = left + (right - left) / 2;
            // 计算中间值的平方值
            long midSquared = mid * mid;

            if (midSquared == num) {
                // 如果平方值等于 num,说明找到了有效的完全平方数
                return true;
            } else if (midSquared < num) {
                // 如果平方值小于 num,说明完全平方数在右半部分
                left = mid + 1;
            } else {
                // 如果平方值大于 num,说明完全平方数在左半部分
                right = mid - 1;
            }
        }

        // 循环结束时仍未找到有效的完全平方数,返回 false
        return false;
    }

    public static void main(String[] args) {
        int num1 = 16;
        int num2 = 14;

        System.out.println("Input: " + num1 + " Output: " + isPerfectSquare(num1));
        System.out.println("Input: " + num2 + " Output: " + isPerfectSquare(num2));
    }
}

22、Sqrt(x)(x的平方根)

题目描述:实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。

示例 1:输入:x = 4    输出:2

示例 2:输入:x = 8    输出:2

解释:8 的平方根是 2.82842...,返回整数部分 2。

提示:

- 0 <= x <= 2^31 - 1

解题思路

最优解法可以通过二分查找法来实现平方根的计算。

  1. 初始化两个变量 `left` 和 `right`,分别表示平方根的搜索范围,初始值为 0 和 `x`。
  2. 使用二分查找法在范围内查找平方根,不断更新 `left` 和 `right` 直到它们相邻或重合。
  3. 在每次二分查找中,计算 `mid` 作为 `left` 和 `right` 的中间值,然后计算 `mid` 的平方与 `x` 进行比较。
  4. 如果 `mid` 的平方小于等于 `x`,则将 `left` 更新为 `mid + 1`,继续向右搜索。
  5. 如果 `mid` 的平方大于 `x`,则将 `right` 更新为 `mid - 1`,继续向左搜索。
  6. 当二分查找结束时,返回 `right`,它即为平方根的整数部分。

该算法的时间复杂度为 O(log(x)),其中 x 是输入的非负整数。因为在每次二分查找中,搜索范围将减少一半,所以时间复杂度是对数级别的。而空间复杂度为 O(1),只使用了常数级的额外空间。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 实现 int sqrt(int x) 函数。
 * 计算并返回 x 的平方根,其中 x 是非负整数。
 * 由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。
 * <p>
 * 示例 1:输入:x = 4    输出:2
 * 示例 2:输入:x = 8    输出:2
 * 解释:8 的平方根是 2.82842...,返回整数部分 2。
 * <p>
 * 提示:
 * - 0 <= x <= 2^31 - 1
 * @date 2023/7/6  23:45
 */
public class SqrtX {

    /**
     * 最优解法可以通过二分查找法来实现平方根的计算。
     * 1. 初始化两个变量 `left` 和 `right`,分别表示平方根的搜索范围,初始值为 0 和 `x`。
     * 2. 使用二分查找法在范围内查找平方根,不断更新 `left` 和 `right` 直到它们相邻或重合。
     * 3. 在每次二分查找中,计算 `mid` 作为 `left` 和 `right` 的中间值,然后计算 `mid` 的平方与 `x` 进行比较。
     * 4. 如果 `mid` 的平方小于等于 `x`,则将 `left` 更新为 `mid + 1`,继续向右搜索。
     * 5. 如果 `mid` 的平方大于 `x`,则将 `right` 更新为 `mid - 1`,继续向左搜索。
     * 6. 当二分查找结束时,返回 `right`,它即为平方根的整数部分。
     * 该算法的时间复杂度为 O(log(x)),其中 x 是输入的非负整数。
     * 因为在每次二分查找中,搜索范围将减少一半,所以时间复杂度是对数级别的。
     * 而空间复杂度为 O(1),只使用了常数级的额外空间。
     */
    public static int mySqrt(int x) {
        if (x == 0 || x == 1) {
            return x;
        }

        int left = 0;
        int right = x;

        while (left <= right) {
            // 防止整型溢出
            int mid = left + (right - left) / 2;
            // 使用 long 类型防止平方时整型溢出
            long square = (long) mid * mid;

            if (square == x) {
                return mid;
            } else if (square < x) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        // 当循环结束时,right 即为平方根的整数部分
        return right;
    }

    public static void main(String[] args) {
        int x1 = 4;
        int x2 = 8;

        System.out.println("Input: " + x1 + " Output: " + mySqrt(x1));
        System.out.println("Input: " + x2 + " Output: " + mySqrt(x2));
    }
}

23、Pow(x, n)(Pow函数,计算x的n次幂)

题目描述:实现 pow(x, n) ,即计算 x 的 n 次幂函数。

示例 1:输入:x = 2.00000, n = 10     输出:1024.00000

示例 2:输入:x = 2.10000, n = 3.      输出:9.26100

示例 3:输入:x = 2.00000, n = -2     输出:0.25000

解释:2^-2 = 1/2^2 = 1/4 = 0.25

提示:

- -100.0 < x < 100.0

- -2^31 <= n <= 2^31-1

- -10^4 <= x^n <= 10^4

注意:解决此问题时,请不要使用库函数来实现 pow(x, n) 。

解题思路

该题还可以见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第37题。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 实现 pow(x, n) ,即计算 x 的 n 次幂函数。
 * <p>
 * 示例 1:输入:x = 2.00000, n = 10     输出:1024.00000
 * 示例 2:输入:x = 2.10000, n = 3.      输出:9.26100
 * 示例 3:输入:x = 2.00000, n = -2     输出:0.25000
 * 解释:2^-2 = 1/2^2 = 1/4 = 0.25
 * <p>
 * 提示:
 * - -100.0 < x < 100.0
 * - -2^31 <= n <= 2^31-1
 * - -10^4 <= x^n <= 10^4
 * 注意:解决此问题时,请不要使用库函数来实现 pow(x, n) 。
 * @date 2023/7/7  23:48
 */
public class PowXN {

    public double myPow(double x, int n) {
        // 处理 n 为负数的情况
        long N = n;
        if (N < 0) {
            x = 1 / x;
            N = -N;
        }

        double result = 1.0;
        double currentProduct = x;

        // 迭代计算 x 的 N 次幂
        while (N > 0) {
            // 如果当前指数为奇数,则将当前 x 乘入结果中
            if ((N % 2) == 1) {
                result *= currentProduct;
            }
            // 将指数减半,x 平方
            currentProduct *= currentProduct;
            N /= 2;
        }

        return result;
    }

    public static void main(String[] args) {
        double x1 = 2.00000;
        int n1 = 10;

        double x2 = 2.10000;
        int n2 = 3;

        double x3 = 2.00000;
        int n3 = -2;

        System.out.println("Input: " + x1 + ", " + n1 + " Output: " + myPow(x1, n1));
        System.out.println("Input: " + x2 + ", " + n2 + " Output: " + myPow(x2, n2));
        System.out.println("Input: " + x3 + ", " + n3 + " Output: " + myPow(x3, n3));
    }
}

24、Super Pow(超级次方)

题目描述:给定两个整数 a 和 b,其中 a 是一个正整数,并且 b 是一个非常大的正整数,你需要计算 a 的超级次方。

超级次方是指一个整数的 b 次方的末尾数字。

示例 1:输入:a = 2, b = [3]     输出:8

示例 2:输入:a = 2, b = [1, 0]     输出:1024

示例 3:输入:a = 1, b = [4, 3, 2, 1, 0]     输出:1

示例 4:输入:a = 2147483647, b = [2, 0]     输出:1198

提示:

  • - 1 <= a <= 231 - 1
  • - 1 <= b.length <= 2000
  • - 0 <= b[i] <= 9
  • - b 不含前导 0

解题思路

最优解法可以通过递归和模幂运算来实现超级次方计算。

递归的思想是将超级次方的计算拆分成两部分:

  1. 计算 a 的 b 的最后一位次方(也就是 a 的 b[b.length - 1] 次方)。
  2. 计算 a 的超级次方除去最后一位的部分。

然后将这两部分相乘,得到最终的超级次方结果。

模幂运算可以用于快速计算 a 的某次方对某个数 p 取模的结果。在本题中,需要将结果对 1337 取模。具体步骤如下:

  1. 定义一个递归函数 `powMod(a, k)`,用于计算 a 的 k 次方对 1337 取模的结果。
  2. 在 `powMod` 函数中,使用递归的方式计算 a 的超级次方除去最后一位的部分的 k 次方对 1337 取模的结果,并记为 `x`。
  3. 然后计算 a 的最后一位次方(也就是 a 的 b[b.length - 1] 次方)对 1337 取模的结果,并记为 `y`。
  4. 最终结果为 `x * y` 对 1337 取模的结果。

该算法的时间复杂度为 O(logn),其中 n 是数组 b 表示的非常大的正整数。因为在递归过程中,每次将 b 的长度减半,所以时间复杂度是对数级别。而空间复杂度为 O(logn),因为递归的深度是 logn。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定两个整数 a 和 b,其中 a 是一个正整数,并且 b 是一个非常大的正整数,你需要计算 a 的超级次方。
 * 超级次方是指一个整数的 b 次方的末尾数字。
 * <p>
 * 示例 1:输入:a = 2, b = [3]     输出:8
 * 示例 2:输入:a = 2, b = [1, 0]     输出:1024
 * 示例 3:输入:a = 1, b = [4, 3, 2, 1, 0]     输出:1
 * 示例 4:输入:a = 2147483647, b = [2, 0]     输出:1198
 * <p>
 * 提示:
 * - 1 <= a <= 231 - 1
 * - 1 <= b.length <= 2000
 * - 0 <= b[i] <= 9
 * - b 不含前导 0
 * @date 2023/7/18  23:56
 */
public class SuperPow {

    /**
     * 最优解法可以通过递归和模幂运算来实现超级次方计算。
     * 递归的思想是将超级次方的计算拆分成两部分:
     * 1. 计算 a 的 b 的最后一位次方(也就是 a 的 b[b.length - 1] 次方)。
     * 2. 计算 a 的超级次方除去最后一位的部分。
     * 然后将这两部分相乘,得到最终的超级次方结果。
     * 模幂运算可以用于快速计算 a 的某次方对某个数 p 取模的结果。在本题中,需要将结果对 1337 取模。
     * 具体步骤如下:
     * 1. 定义一个递归函数 `powMod(a, k)`,用于计算 a 的 k 次方对 1337 取模的结果。
     * 2. 在 `powMod` 函数中,使用递归的方式计算 a 的超级次方除去最后一位的部分的 k 次方对 1337 取模的结果,并记为 `x`。
     * 3. 然后计算 a 的最后一位次方(也就是 a 的 b[b.length - 1] 次方)对 1337 取模的结果,并记为 `y`。
     * 4. 最终结果为 `x * y` 对 1337 取模的结果。
     * 该算法的时间复杂度为 O(logn),其中 n 是数组 b 表示的非常大的正整数。
     * 因为在递归过程中,每次将 b 的长度减半,所以时间复杂度是对数级别。
     * 而空间复杂度为 O(logn),因为递归的深度是 logn。
     */
    public static int superPow(int a, int[] b) {
        return powMod(a, b, b.length);
    }

    private static int powMod(int a, int[] b, int len) {
        if (len == 0) {
            return 1;
        }
        int lastDigit = b[len - 1];
        // 计算 a 的超级次方除去最后一位的部分的结果
        int x = powMod(a, b, len - 1);
        // 计算 a 的最后一位次方的结果
        int y = powMod(a, 1, lastDigit);
        // 将结果对 1337 取模并返回
        return x * y % 1337;
    }

    private static int powMod(int a, int k, int mod) {
        a %= mod;
        int result = 1;
        for (int i = 0; i < k; i++) {
            result = result * a % mod;
        }
        return result;
    }

    public static void main(String[] args) {
        int a1 = 2;
        int[] b1 = {3};
        int a2 = 2;
        int[] b2 = {1, 0};
        int a3 = 1;
        int[] b3 = {4, 3, 2, 1, 0};
        int a4 = 2147483647;
        int[] b4 = {2, 0};

        System.out.println("Input: a = " + a1 + ", b = " + Arrays.toString(b1) + " Output: " + superPow(a1, b1));
        System.out.println("Input: a = " + a2 + ", b = " + Arrays.toString(b2) + " Output: " + superPow(a2, b2));
        System.out.println("Input: a = " + a3 + ", b = " + Arrays.toString(b3) + " Output: " + superPow(a3, b3));
        System.out.println("Input: a = " + a4 + ", b = " + Arrays.toString(b4) + " Output: " + superPow(a4, b4));
    }
}

25、Fraction to Recurring Decimal(分数转循环小数)

题目描述:给定两个整数,分别表示分数的分子 `numerator` 和分母 `denominator`,以字符串形式返回小数。如果小数部分是循环的,则将循环的部分括在括号内。如果存在多个答案,只需返回任意一个。

示例 1:输入:numerator = 1, denominator = 2.         输出:"0.5"

示例 2:输入:numerator = 2, denominator = 1          输出:"2"

示例 3:输入:numerator = 2, denominator = 3         输出:"0.(6)"

示例 4:输入:numerator = 4, denominator = 333     输出:"0.(012)"

示例 5:输入:numerator = 1, denominator = 5          输出:"0.2"

提示:

  • - -2^31 <= numerator, denominator <= 2^31 - 1
  • - denominator != 0

解题思路

最优解法可以通过模拟长除法来实现分数转循环小数。

  1. 首先判断结果的正负号,如果分子和分母的符号不一致,则结果为负数。
  2. 计算结果的整数部分,通过 `numerator / denominator` 可以得到整数部分 `integerPart`。
  3. 计算结果的小数部分,通过 `numerator % denominator` 可以得到小数部分的分子 `remainder`。
  4. 创建一个哈希表用于记录小数部分的余数以及对应的位置,然后开始进行循环小数的计算。
  5. 将 `remainder` 乘以 10,得到下一位小数的分子 `nextNumerator`,同时更新 `remainder` 为 `nextNumerator % denominator`。
  6. 在每次计算下一位小数时,检查当前的 `remainder` 是否在哈希表中,如果是,说明出现了循环,将循环部分括在括号内即可。
  7. 重复步骤 5 和 6 直到 `remainder` 为 0 或者出现循环为止。

该算法的时间复杂度为 O(denominator),其中 denominator 为分母的值。因为每次计算下一位小数时,分子 `remainder` 的值会在 0 到 `denominator-1` 之间,最多需要进行 `denominator` 次计算。而空间复杂度为 O(denominator),需要额外的哈希表来记录小数部分的余数。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.HashMap;
import java.util.Map;

/**
 * @author yanfengzhang
 * @description 给定两个整数,分别表示分数的分子 `numerator` 和分母 `denominator`,以字符串形式返回小数。
 * 如果小数部分是循环的,则将循环的部分括在括号内。
 * 如果存在多个答案,只需返回任意一个。
 * <p>
 * 示例 1:输入:numerator = 1, denominator = 2.         输出:"0.5"
 * 示例 2:输入:numerator = 2, denominator = 1          输出:"2"
 * 示例 3:输入:numerator = 2, denominator = 3         输出:"0.(6)"
 * 示例 4:输入:numerator = 4, denominator = 333     输出:"0.(012)"
 * 示例 5:输入:numerator = 1, denominator = 5          输出:"0.2"
 * <p>
 * 提示:
 * - -2^31 <= numerator, denominator <= 2^31 - 1
 * - denominator != 0
 * @date 2023/7/8  23:52
 */
public class FractionToRecurringDecimal {

    /**
     * 最优解法可以通过模拟长除法来实现分数转循环小数。
     * 1. 首先判断结果的正负号,如果分子和分母的符号不一致,则结果为负数。
     * 2. 计算结果的整数部分,通过 `numerator / denominator` 可以得到整数部分 `integerPart`。
     * 3. 计算结果的小数部分,通过 `numerator % denominator` 可以得到小数部分的分子 `remainder`。
     * 4. 创建一个哈希表用于记录小数部分的余数以及对应的位置,然后开始进行循环小数的计算。
     * 5. 将 `remainder` 乘以 10,得到下一位小数的分子 `nextNumerator`,同时更新 `remainder` 为 `nextNumerator % denominator`。
     * 6. 在每次计算下一位小数时,检查当前的 `remainder` 是否在哈希表中,如果是,说明出现了循环,将循环部分括在括号内即可。
     * 7. 重复步骤 5 和 6 直到 `remainder` 为 0 或者出现循环为止。
     * 该算法的时间复杂度为 O(denominator),其中 denominator 为分母的值。
     * 因为每次计算下一位小数时,分子 `remainder` 的值会在 0 到 `denominator-1` 之间,最多需要进行 `denominator` 次计算。
     * 而空间复杂度为 O(denominator),需要额外的哈希表来记录小数部分的余数。
     */
    public static String fractionToDecimal(int numerator, int denominator) {
        // 处理结果的符号
        StringBuilder result = new StringBuilder();
        if ((numerator < 0 && denominator > 0)
                || (numerator > 0 && denominator < 0)) {
            result.append("-");
        }

        // 计算整数部分
        long integerPart = Math.abs((long) numerator / denominator);
        result.append(integerPart);

        // 计算小数部分
        long remainder = Math.abs((long) numerator % denominator);
        if (remainder == 0) {
            return result.toString();
        }

        result.append(".");
        // 记录小数部分的余数和对应位置
        Map<Long, Integer> map = new HashMap<>();
        while (remainder != 0) {
            // 检查当前余数是否在哈希表中,如果是,说明出现循环
            if (map.containsKey(remainder)) {
                int index = map.get(remainder);
                // 非循环部分
                String nonRecurringPart = result.substring(0, index);
                // 循环部分
                String recurringPart = result.substring(index);
                return nonRecurringPart + "(" + recurringPart + ")";
            }

            // 记录余数的位置
            map.put(remainder, result.length());
            // 余数乘以10得到下一位小数的分子
            remainder *= 10;
            // 计算下一位小数
            result.append(remainder / denominator);
            // 更新余数
            remainder %= denominator;
        }

        return result.toString();
    }

    public static void main(String[] args) {
        int numerator1 = 1;
        int denominator1 = 2;

        int numerator2 = 2;
        int denominator2 = 1;

        int numerator3 = 2;
        int denominator3 = 3;

        int numerator4 = 4;
        int denominator4 = 333;

        int numerator5 = 1;
        int denominator5 = 5;

        System.out.println("Input: " + numerator1 + ", " + denominator1 + " Output: " + fractionToDecimal(numerator1, denominator1));
        System.out.println("Input: " + numerator2 + ", " + denominator2 + " Output: " + fractionToDecimal(numerator2, denominator2));
        System.out.println("Input: " + numerator3 + ", " + denominator3 + " Output: " + fractionToDecimal(numerator3, denominator3));
        System.out.println("Input: " + numerator4 + ", " + denominator4 + " Output: " + fractionToDecimal(numerator4, denominator4));
        System.out.println("Input: " + numerator5 + ", " + denominator5 + " Output: " + fractionToDecimal(numerator5, denominator5));
    }
}

26、Integer Replacement(整数替换)

题目描述:给定一个正整数 n,你可以做如下操作:

  1. 如果 n 是偶数,则用 n / 2 替换 n。
  2. 如果 n 是奇数,则可以用 n + 1 或 n - 1 替换 n。

将 n 变为 1 所需的最小替换次数是多少?

示例 1:输入:8     输出:3      解释:8 -> 4 -> 2 -> 1

示例 2:输入:7      输出:4     解释:7 -> 8 -> 4 -> 2 -> 1 或 7 -> 6 -> 3 -> 2 -> 1

提示:

- 1 <= n <= 2^31 - 1

解题思路

最优解法可以使用递归或迭代来实现整数替换的计算。

1. 递归方法:

   - 对于偶数 n,直接将 n 除以 2,并对 n/2 继续递归调用。

   - 对于奇数 n,可以选择将 n + 1 或 n - 1,然后对 (n+1)/2 或 (n-1)/2 继续递归调用。

   - 递归的结束条件是 n = 1,此时不再调用递归,返回步骤数。

   - 在递归过程中,通过比较两种替换方式得到的步骤数,选择步数较小的方案。

2. 迭代方法:

   - 使用一个 while 循环,当 n 不等于 1 时进行迭代。

   - 对于偶数 n,直接将 n 除以 2。

   - 对于奇数 n,比较 n + 1 和 n - 1 两种替换方式得到的步骤数,选择步数较小的方案,并更新 n 的值。

   - 循环结束时,返回步骤数。

无论是递归还是迭代,算法的时间复杂度都是 O(logn),其中 n 是输入的正整数。因为在每一次递归或迭代中,n 的值都会减少一半。而空间复杂度为 O(logn),因为递归或迭代的深度是 logn。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个正整数 n,你可以做如下操作:
 * 1. 如果 n 是偶数,则用 n / 2 替换 n。
 * 2. 如果 n 是奇数,则可以用 n + 1 或 n - 1 替换 n。
 * 将 n 变为 1 所需的最小替换次数是多少?
 * <p>
 * 示例 1:输入:8     输出:3      解释:8 -> 4 -> 2 -> 1
 * 示例 2:输入:7      输出:4     解释:7 -> 8 -> 4 -> 2 -> 1 或 7 -> 6 -> 3 -> 2 -> 1
 * <p>
 * 提示:
 * - 1 <= n <= 2^31 - 1
 * @date 2023/7/3  23:04
 */
public class IntegerReplacement {

    /**
     * 递归方法:
     * - 对于偶数 n,直接将 n 除以 2,并对 n/2 继续递归调用。
     * - 对于奇数 n,可以选择将 n + 1 或 n - 1,然后对 (n+1)/2 或 (n-1)/2 继续递归调用。
     * - 递归的结束条件是 n = 1,此时不再调用递归,返回步骤数。
     * - 在递归过程中,通过比较两种替换方式得到的步骤数,选择步数较小的方案。
     */
    public static int integerReplacement(int n) {
        return integerReplacementHelper(n, 0);
    }

    /**
     * 辅助函数,递归实现整数替换的计算
     */
    private static int integerReplacementHelper(int n, int count) {
        if (n == 1) {
            return count;
        }

        if (n % 2 == 0) {
            return integerReplacementHelper(n / 2, count + 1);
        } else {
            int countPlusOne = integerReplacementHelper(n + 1, count + 1);
            int countMinusOne = integerReplacementHelper(n - 1, count + 1);
            return Math.min(countPlusOne, countMinusOne);
        }
    }

    /**
     * 迭代方法:
     * - 使用一个 while 循环,当 n 不等于 1 时进行迭代。
     * - 对于偶数 n,直接将 n 除以 2。
     * - 对于奇数 n,比较 n + 1 和 n - 1 两种替换方式得到的步骤数,选择步数较小的方案,并更新 n 的值。
     * - 循环结束时,返回步骤数。
     */
    public static int integerReplacement2(int n) {
        int count = 0;

        while (n != 1) {
            if (n % 2 == 0) {
                // 偶数,直接除以2
                n /= 2;
            } else {
                if ((n + 1) % 4 == 0 && n != 3) {
                    // 如果 n+1 后可以被4整除,且 n 不等于3,优先选择 n+1
                    n++;
                } else {
                    // 否则选择 n-1
                    n--;
                }
            }

            count++;
        }

        return count;
    }

    public static void main(String[] args) {
        int n1 = 8;
        int n2 = 7;

        System.out.println("Input: " + n1 + " Output: " + integerReplacement(n1));
        System.out.println("Input: " + n2 + " Output: " + integerReplacement(n2));
    }

}

27、Integer Break(整数拆分)

题目描述:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。

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

示例 2:输入:10     输出:36.     解释:10 = 3 + 3 + 4,3 × 3 × 4 = 36。

说明:你可以假设 n 不小于 2 且不大于 58。

解题思路

最优解法可以通过动态规划来实现整数拆分问题。

动态规划的思想是将问题拆分成子问题,并保存子问题的解,避免重复计算,从而得到最优解。

具体步骤如下:

  1. 创建一个大小为 n+1 的数组 dp,用于保存拆分整数 i 所能得到的最大乘积。
  2. 初始化 dp[0] 和 dp[1] 为 0,因为 0 和 1 不能拆分。
  3. 从 i = 2 开始遍历到 n,对每个整数 i 进行如下操作:

       - 定义两个指针 left 和 right,分别指向 1 和 i-1,表示 i 可以拆分成两个整数 1 和 i-1。

       - 在循环中,不断缩小 left 和 right 的范围,计算 left 和 right 对应的最大乘积,并将其与当前 dp[i] 比较,更新 dp[i] 的值为较大的那个。

  4. 遍历完成后,dp[n] 即为整数 n 拆分后能得到的最大乘积。

该算法的时间复杂度为 O(n^2),其中 n 是给定的正整数。因为需要遍历从 2 到 n 的每个整数,对每个整数进行拆分计算。而空间复杂度为 O(n),需要额外的数组来保存子问题的解。

该题还可以见动态规划总结与编程练习_动态规划编程_张彦峰ZYF的博客-CSDN博客中的第5题。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。
 * <p>
 * 示例 1:输入:2.     输出:1.    解释:2 = 1 + 1,1 × 1 = 1。
 * 示例 2:输入:10     输出:36.     解释:10 = 3 + 3 + 4,3 × 3 × 4 = 36。
 * 说明:你可以假设 n 不小于 2 且不大于 58。
 * @date 2023/7/14  23:18
 */
public class IntegerBreak {

    /**
     * 最优解法可以通过动态规划来实现整数拆分问题。
     * 动态规划的思想是将问题拆分成子问题,并保存子问题的解,避免重复计算,从而得到最优解。
     * 具体步骤如下:
     * 1. 创建一个大小为 n+1 的数组 dp,用于保存拆分整数 i 所能得到的最大乘积。
     * 2. 初始化 dp[0] 和 dp[1] 为 0,因为 0 和 1 不能拆分。
     * 3. 从 i = 2 开始遍历到 n,对每个整数 i 进行如下操作:
     * - 定义两个指针 left 和 right,分别指向 1 和 i-1,表示 i 可以拆分成两个整数 1 和 i-1。
     * - 在循环中,不断缩小 left 和 right 的范围,计算 left 和 right 对应的最大乘积,并将其与当前 dp[i] 比较,更新 dp[i] 的值为较大的那个。
     * 4. 遍历完成后,dp[n] 即为整数 n 拆分后能得到的最大乘积。
     * 该算法的时间复杂度为 O(n^2),其中 n 是给定的正整数。因为需要遍历从 2 到 n 的每个整数,对每个整数进行拆分计算。而空间复杂度为 O(n),需要额外的数组来保存子问题的解。
     */
    public static int integerBreak(int n) {
        // 创建大小为 n+1 的数组用于保存拆分整数 i 所能得到的最大乘积
        int[] dp = new int[n + 1];
        // 初始化 dp[0] 为 0,因为 0 不能拆分
        dp[0] = 0;
        // 初始化 dp[1] 为 0,因为 1 不能拆分
        dp[1] = 0;

        // 从 i = 2 开始遍历到 n
        for (int i = 2; i <= n; i++) {
            // 定义两个指针 left 和 right,分别指向 1 和 i-1
            int left = 1;
            int right = i - 1;
            // 初始化 dp[i] 为 i-1,因为整数 i 可以拆分成两个整数 1 和 i-1
            dp[i] = i - 1;

            // 在循环中,不断缩小 left 和 right 的范围
            while (left <= right) {
                // 计算 left 和 right 对应的最大乘积,并将其与当前 dp[i] 比较,更新 dp[i] 的值为较大的那个
                dp[i] = Math.max(dp[i], Math.max(left, dp[left]) * Math.max(right, dp[right]));
                left++;
                right--;
            }
        }

        // dp[n] 即为整数 n 拆分后能得到的最大乘积
        return dp[n];
    }

    public static void main(String[] args) {
        int n1 = 2;
        int n2 = 10;

        System.out.println("Input: " + n1 + " Output: " + integerBreak(n1));
        System.out.println("Input: " + n2 + " Output: " + integerBreak(n2));
    }

}

28、Arithmetic Slices(等差数列划分)

题目描述:如果一个数列至少有三个元素,并且任意相邻元素之间的差值相同,则称其为等差数列。给定一个由 N 个元素组成的数列 A,返回 A 中所有等差数列的个数。

示例 1:输入:[2, 4, 6, 8, 10]     输出:7

解释:所有的等差数列为:[2, 4, 6],[4, 6, 8],[6, 8, 10],[2, 4, 6, 8],[4, 6, 8, 10],[2, 4, 6, 8, 10],[2, 6, 10]。

示例 2:输入:[7, 7, 7, 7]     输出:3

解释:所有的等差数列为:[7, 7, 7],[7, 7, 7, 7],[7, 7, 7, 7, 7]。

提示:

  1. - 1 <= A.length <= 1000
  2. - -10^9 <= A[i] <= 10^9

解题思路

最优解法可以通过动态规划来实现等差数列划分的计算。

动态规划的思想是从小规模的子问题开始,逐步推导到大规模的问题。在本题中,可以定义一个动态规划数组 dp,其中 dp[i] 表示以第 i 个元素为结尾的等差数列的个数。

状态转移方程为:

  • - 如果 A[i] - A[i-1] == A[i-1] - A[i-2],说明前三个元素可以组成一个等差数列,dp[i] = dp[i-1] + 1。
  • - 否则,dp[i] = 0,因为不能构成等差数列。

最终的结果就是 dp 数组中所有元素的和,即所有等差数列的个数。具体步骤如下:

  1. 初始化动态规划数组 dp,长度为 N,并将所有元素初始化为 0。
  2. 从 i = 2 开始遍历数组 A,根据状态转移方程更新 dp 数组。
  3. 将 dp 数组中所有元素相加,得到最终结果。

该算法的时间复杂度为 O(N),其中 N 是数组 A 的长度。因为只需要遍历数组一次即可计算 dp 数组。而空间复杂度为 O(N),需要额外的数组来保存 dp 数组。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 如果一个数列至少有三个元素,并且任意相邻元素之间的差值相同,则称其为等差数列。
 * 给定一个由 N 个元素组成的数列 A,返回 A 中所有等差数列的个数。
 * <p>
 * 示例 1:输入:[2, 4, 6, 8, 10]     输出:7
 * 解释:所有的等差数列为:[2, 4, 6],[4, 6, 8],[6, 8, 10],[2, 4, 6, 8],[4, 6, 8, 10],[2, 4, 6, 8, 10],[2, 6, 10]。
 * 示例 2:输入:[7, 7, 7, 7]     输出:3
 * 解释:所有的等差数列为:[7, 7, 7],[7, 7, 7, 7],[7, 7, 7, 7, 7]。
 * <p>
 * 提示:
 * - 1 <= A.length <= 1000
 * - -10^9 <= A[i] <= 10^9
 * @date 2023/7/19  23:00
 */
public class ArithmeticSlices {
    /**
     * 最优解法可以通过动态规划来实现等差数列划分的计算。
     * 动态规划的思想是从小规模的子问题开始,逐步推导到大规模的问题。
     * 在本题中,可以定义一个动态规划数组 dp,其中 dp[i] 表示以第 i 个元素为结尾的等差数列的个数。
     * 状态转移方程为:
     * - 如果 A[i] - A[i-1] == A[i-1] - A[i-2],说明前三个元素可以组成一个等差数列,dp[i] = dp[i-1] + 1。
     * - 否则,dp[i] = 0,因为不能构成等差数列。
     * 最终的结果就是 dp 数组中所有元素的和,即所有等差数列的个数。
     * 具体步骤如下:
     * 1. 初始化动态规划数组 dp,长度为 N,并将所有元素初始化为 0。
     * 2. 从 i = 2 开始遍历数组 A,根据状态转移方程更新 dp 数组。
     * 3. 将 dp 数组中所有元素相加,得到最终结果。
     * 该算法的时间复杂度为 O(N),其中 N 是数组 A 的长度。
     * 因为只需要遍历数组一次即可计算 dp 数组。而空间复杂度为 O(N),需要额外的数组来保存 dp 数组。
     */
    public static int numberOfArithmeticSlices(int[] A) {
        int n = A.length;
        // 初始化动态规划数组 dp
        int[] dp = new int[n];
        // 初始化等差数列个数
        int count = 0;

        for (int i = 2; i < n; i++) {
            // 判断是否构成等差数列
            if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
                // 更新 dp 数组
                dp[i] = dp[i - 1] + 1;
                // 更新等差数列个数
                count += dp[i];
            }
        }

        return count;
    }

    public static void main(String[] args) {
        int[] A1 = {2, 4, 6, 8, 10};
        int[] A2 = {7, 7, 7, 7};

        System.out.println("Input: " + Arrays.toString(A1) + " Output: " + numberOfArithmeticSlices(A1));
        System.out.println("Input: " + Arrays.toString(A2) + " Output: " + numberOfArithmeticSlices(A2));
    }
}

29、Beautiful Arrangement(漂亮数组)

题目描述:假设有一个从 1 到 N 的整数数组,如果在数组中的每个 i 处都满足下列两个条件之一,则该数组为漂亮数组:

  • 第 i 位上的数字可以被 i 整除。
  •  i 可以被第 i 位上的数字整除。

现在给定一个整数 N,请返回任意一个漂亮数组。

示例 1:输入:N = 4     输出:[2, 1, 4, 3]

示例 2:输入:N = 5     输出:[3, 1, 2, 5, 4]

提示:

- 1 <= N <= 1000

解题思路

最优解法可以通过分治法来实现漂亮数组的生成。

分治法的思想是将问题拆分成更小的子问题,然后将子问题的解合并得到原问题的解。对于漂亮数组问题,可以将数组拆分成两个子数组,分别满足条件 1 和条件 2。具体步骤如下:

  1. 创建一个数组 result,用于保存漂亮数组。
  2. 递归地生成满足条件 1 和条件 2 的两个子数组,分别记为 left 和 right。
  3. 将 left 和 right 数组合并,并添加到 result 数组中。
  4. 返回 result 数组。

生成满足条件 1 的子数组可以通过对原数组中的偶数索引进行递归生成。生成满足条件 2 的子数组可以通过对原数组中的奇数索引进行递归生成。

该算法的时间复杂度为 O(NlogN),其中 N 是输入的整数 N。因为在每次递归中,数组的长度减半。而空间复杂度为 O(N),需要额外的数组来保存 result 数组。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 假设有一个从 1 到 N 的整数数组,如果在数组中的每个 i 处都满足下列两个条件之一,则该数组为漂亮数组:
 * 1. 第 i 位上的数字可以被 i 整除。
 * 2. i 可以被第 i 位上的数字整除。
 * 现在给定一个整数 N,请返回任意一个漂亮数组。
 * <p>
 * 示例 1:输入:N = 4     输出:[2, 1, 4, 3]
 * 示例 2:输入:N = 5     输出:[3, 1, 2, 5, 4]
 * <p>
 * 提示:
 * - 1 <= N <= 1000
 * @date 2023/6/22  23:13
 */
public class BeautifulArrangement {

    /**
     * 最优解法可以通过分治法来实现漂亮数组的生成。
     * 分治法的思想是将问题拆分成更小的子问题,然后将子问题的解合并得到原问题的解。
     * 对于漂亮数组问题,可以将数组拆分成两个子数组,分别满足条件 1 和条件 2。
     * 具体步骤如下:
     * 1. 创建一个数组 result,用于保存漂亮数组。
     * 2. 递归地生成满足条件 1 和条件 2 的两个子数组,分别记为 left 和 right。
     * 3. 将 left 和 right 数组合并,并添加到 result 数组中。
     * 4. 返回 result 数组。
     * 生成满足条件 1 的子数组可以通过对原数组中的偶数索引进行递归生成。
     * 生成满足条件 2 的子数组可以通过对原数组中的奇数索引进行递归生成。
     * 该算法的时间复杂度为 O(NlogN),其中 N 是输入的整数 N。
     * 因为在每次递归中,数组的长度减半。而空间复杂度为 O(N),需要额外的数组来保存 result 数组。
     */
    public static int[] beautifulArray(int N) {
        int[] result = new int[N];
        if (N == 1) {
            result[0] = 1;
            return result;
        }

        // 生成满足条件1的子数组,通过对原数组中的偶数索引进行递归生成
        int[] left = beautifulArray((N + 1) / 2);
        // 生成满足条件2的子数组,通过对原数组中的奇数索引进行递归生成
        int[] right = beautifulArray(N / 2);

        // 合并 left 和 right 数组,并添加到 result 数组中
        for (int i = 0; i < left.length; i++) {
            result[i] = 2 * left[i] - 1;
        }
        for (int i = 0; i < right.length; i++) {
            result[i + left.length] = 2 * right[i];
        }

        return result;
    }

    public static void main(String[] args) {
        int N1 = 4;
        int N2 = 5;

        System.out.println("Input: N = " + N1 + " Output: " + Arrays.toString(beautifulArray(N1)));
        System.out.println("Input: N = " + N2 + " Output: " + Arrays.toString(beautifulArray(N2)));
    }
}

30、Minimum Moves to Equal Array Elements(将数组元素相等的最小移动次数)

题目描述:给定一个非空整数数组,找出使所有数组元素相等所需的最小移动次数。在每次移动中,你可以选择任意一个数组元素,将它增加 1 或减少 1。你可以假设数组的长度不超过 10000。

示例 1:输入:[1, 2, 3]     输出:2

解释:只需要将 2 减少 1,即可使数组元素都相等,最小移动次数为 2。

示例 2:输入:[1, 10, 2, 9]     输出:16

解释:只需要将 10 减少 8,2 增加 8,9 减少 8,即可使数组元素都相等,最小移动次数为 16。

提示:

  • - 数组中的整数范围为 [-10^9, 10^9]。
  • - 数组的长度不超过 10000。

解题思路

最优解法可以通过对数组进行排序来实现。

假设数组为 nums,排序后的数组为 sortedNums。要使所有数组元素相等,可以选择将数组元素调整到中位数的值,即 sortedNums[(n-1)/2],其中 n 是数组长度。

为什么选择中位数呢?因为中位数是一组数据中居中位置的数,可以保证使得数组元素相等时,移动次数最少。如果选择其他的数,比如最小值或最大值,那么需要更多的移动次数。

具体步骤如下:

  1. 对数组 nums 进行排序,得到 sortedNums。
  2. 计算中位数值 median = sortedNums[(n-1)/2]。
  3. 遍历数组 nums,计算每个元素与 median 的差值的绝对值,并累加得到最小移动次数。

该算法的时间复杂度为 O(nlogn),其中 n 是数组长度。因为对数组进行排序需要 O(nlogn) 的时间。而空间复杂度为 O(n),需要额外的数组来保存 sortedNums。

具体代码展示

package org.zyf.javabasic.letcode.math;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定一个非空整数数组,找出使所有数组元素相等所需的最小移动次数。
 * 在每次移动中,你可以选择任意一个数组元素,将它增加 1 或减少 1。
 * 你可以假设数组的长度不超过 10000。
 * <p>
 * 示例 1:输入:[1, 2, 3]     输出:2
 * 解释:只需要将 2 减少 1,即可使数组元素都相等,最小移动次数为 2。
 * 示例 2:输入:[1, 10, 2, 9]     输出:16
 * 解释:只需要将 10 减少 8,2 增加 8,9 减少 8,即可使数组元素都相等,最小移动次数为 16。
 * <p>
 * 提示:
 * - 数组中的整数范围为 [-10^9, 10^9]。
 * - 数组的长度不超过 10000。
 * @date 2023/6/25  23:17
 */
public class MinMoves {

    /**
     * 最优解法可以通过对数组进行排序来实现。
     * 假设数组为 nums,排序后的数组为 sortedNums。
     * 要使所有数组元素相等,可以选择将数组元素调整到中位数的值,即 sortedNums[(n-1)/2],其中 n 是数组长度。
     * 为什么选择中位数呢?
     * 因为中位数是一组数据中居中位置的数,可以保证使得数组元素相等时,移动次数最少。
     * 如果选择其他的数,比如最小值或最大值,那么需要更多的移动次数。
     * 具体步骤如下:
     * 1. 对数组 nums 进行排序,得到 sortedNums。
     * 2. 计算中位数值 median = sortedNums[(n-1)/2]。
     * 3. 遍历数组 nums,计算每个元素与 median 的差值的绝对值,并累加得到最小移动次数。
     * 该算法的时间复杂度为 O(nlogn),其中 n 是数组长度。因为对数组进行排序需要 O(nlogn) 的时间。
     * 而空间复杂度为 O(n),需要额外的数组来保存 sortedNums。
     */
    public static int minMoves(int[] nums) {
        // 对数组进行排序
        Arrays.sort(nums);

        int n = nums.length;
        // 计算中位数
        int median = nums[(n - 1) / 2];

        int minMoves = 0;
        // 遍历数组,计算每个元素与中位数的差值的绝对值,并累加得到最小移动次数
        for (int num : nums) {
            minMoves += Math.abs(num - median);
        }

        return minMoves;
    }

    public static void main(String[] args) {
        int[] nums1 = {1, 2, 3};
        int[] nums2 = {1, 10, 2, 9};

        System.out.println("Input: " + Arrays.toString(nums1) + " Output: " + minMoves(nums1));
        System.out.println("Input: " + Arrays.toString(nums2) + " Output: " + minMoves(nums2));
    }
}

31、Maximum Points You Can Obtain from Cards(从卡牌中获得的最大点数)

题目描述:当给定一行卡牌,每张卡牌上都有一个正整数代表点数。你可以选择从行的开头或末尾取任意数量的卡牌,但至少要选择一张。问题是要求你计算出能够获得的最大总点数。

例如:输入:[1, 2, 3, 4, 5, 6, 1],K = 3    输出:12(选择点数为6、5和1的卡牌)

解题思路

最优解法是使用动态规划(Dynamic Programming)来解决这个问题。我们可以通过构建一个二维数组来存储中间结果,并利用状态转移方程来计算最大总点数。

假设给定的卡牌数组为nums,长度为n,我们定义二维数组dp,其中dp[i][j]表示在前i个卡牌中选择j张卡牌时可以获得的最大总点数。状态转移方程如下:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] + nums[i])

其中dp[i-1][j]表示不选第i张卡牌时的最大总点数,dp[i-1][j-1] + nums[i]表示选第i张卡牌时的最大总点数。

初始化dp数组时,dp[0][0]为0,dp[0][1]到dp[0][n]为nums[0]到nums[n-1]的累加和。

最后,我们需要返回dp[n][k],其中n为卡牌数组的长度,k为需要选择的卡牌数量。

这种动态规划解法的时间复杂度为O(n*k),其中n为卡牌数组的长度,k为需要选择的卡牌数量。这是因为我们需要填充一个大小为n*(k+1)的二维数组。空间复杂度也为O(n*k)。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 当给定一行卡牌,每张卡牌上都有一个正整数代表点数。
 * 你可以选择从行的开头或末尾取任意数量的卡牌,但至少要选择一张。
 * 问题是要求你计算出能够获得的最大总点数。
 * 例如:输入:[1, 2, 3, 4, 5, 6, 1],K = 3    输出:15(选择点数为6、5和4的卡牌)
 * @date 2023/6/29  23:20
 */
public class MaximumPointsFromCards {
    /**
     * 最优解法是使用动态规划(Dynamic Programming)来解决这个问题。
     * 我们可以通过构建一个二维数组来存储中间结果,并利用状态转移方程来计算最大总点数。
     * 假设给定的卡牌数组为nums,长度为n,我们定义二维数组dp,其中dp[i][j]表示在前i个卡牌中选择j张卡牌时可以获得的最大总点数。
     * 状态转移方程如下:
     * dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] + nums[i])
     * 其中dp[i-1][j]表示不选第i张卡牌时的最大总点数,dp[i-1][j-1] + nums[i]表示选第i张卡牌时的最大总点数。
     * 初始化dp数组时,dp[0][0]为0,dp[0][1]到dp[0][n]为nums[0]到nums[n-1]的累加和。
     * 最后,我们需要返回dp[n][k],其中n为卡牌数组的长度,k为需要选择的卡牌数量。
     * 这种动态规划解法的时间复杂度为O(n*k),其中n为卡牌数组的长度,k为需要选择的卡牌数量。
     * 这是因为我们需要填充一个大小为n*(k+1)的二维数组。空间复杂度也为O(n*k)。
     */
    public static int maxPoints(int[] nums, int k) {
        int n = nums.length;
        int[][] dp = new int[n + 1][k + 1];

        // 初始化第一行,表示前0个卡牌时的最大总点数为0
        for (int i = 0; i <= k; i++) {
            dp[0][i] = 0;
        }

        // 填充dp数组
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= k; j++) {
                // 不选第i张卡牌时的最大总点数
                int notPickCard = dp[i - 1][j];
                // 选第i张卡牌时的最大总点数
                int pickCard = dp[i - 1][j - 1] + nums[i - 1];
                // 取两者中的较大值作为dp[i][j]的值
                dp[i][j] = Math.max(notPickCard, pickCard);
            }
        }

        // 返回最终结果,即从前n个卡牌中选择k张卡牌时的最大总点数
        return dp[n][k];
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6, 1};
        int k = 3;
        int result = maxPoints(nums, k);
        System.out.println("最大总点数为: " + result);
    }

}

32、Excel Sheet Column Title(Excel表列名称)

题目描述:给定一个正整数,返回它在 Excel 表中相对应的列名称。

例如,

1 -> A

2 -> B

3 -> C

...

26 -> Z

27 -> AA

28 -> AB

...

示例 1:输入:columnNumber = 1.       输出:"A"

示例 2:输入:columnNumber = 28     输出:"AB"

示例 3:输入:columnNumber = 701.   输出:"ZY"

示例 4:输入:columnNumber = 2147483647     输出:"FXSHRXW"

提示:

- 1 <= columnNumber <= 2^31 - 1

解题思路

最优解法可以通过进制转换来实现Excel表列名称的计算。

  1. 从给定的正整数开始,不断地将其除以 26,并将余数转换为对应的字符。
  2. 将余数对应的字符拼接到结果字符串的最前面。
  3. 更新原始的正整数为商,然后继续进行上述步骤,直到正整数变为0为止。

例如,对于 28,首先 28 / 26 = 1 余 2,对应的字符为 B,所以将 B 拼接到结果字符串的最前面。然后更新正整数为 1,继续进行计算,此时 1 / 26 = 0 余 1,对应的字符为 A,将 A 拼接到结果字符串的最前面。最终得到结果 "AB"。

该算法的时间复杂度为 O(log(columnNumber)),其中 columnNumber 是给定的正整数。因为在每次计算时,正整数都会减少一半,最多需要进行 log(columnNumber) 次计算。而空间复杂度为 O(log(columnNumber)),需要额外的字符串来保存结果。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个正整数,返回它在 Excel 表中相对应的列名称。
 * <p>
 * 例如,
 * 1 -> A
 * 2 -> B
 * 3 -> C
 * ...
 * 26 -> Z
 * 27 -> AA
 * 28 -> AB
 * ...
 * <p>
 * 示例 1:输入:columnNumber = 1.       输出:"A"
 * 示例 2:输入:columnNumber = 28     输出:"AB"
 * 示例 3:输入:columnNumber = 701.   输出:"ZY"
 * 示例 4:输入:columnNumber = 2147483647     输出:"FXSHRXW"
 * <p>
 * 提示:
 * - 1 <= columnNumber <= 2^31 - 1
 * @date 2023/7/9  23:07
 */
public class ExcelSheetColumnTitle {

    /**
     * 最优解法可以通过进制转换来实现Excel表列名称的计算。
     * 1. 从给定的正整数开始,不断地将其除以 26,并将余数转换为对应的字符。
     * 2. 将余数对应的字符拼接到结果字符串的最前面。
     * 3. 更新原始的正整数为商,然后继续进行上述步骤,直到正整数变为0为止。
     * 例如,对于 28,首先 28 / 26 = 1 余 2,对应的字符为 B,所以将 B 拼接到结果字符串的最前面。
     * 然后更新正整数为 1,继续进行计算,此时 1 / 26 = 0 余 1,对应的字符为 A,将 A 拼接到结果字符串的最前面。最终得到结果 "AB"。
     * 该算法的时间复杂度为 O(log(columnNumber)),其中 columnNumber 是给定的正整数。
     * 因为在每次计算时,正整数都会减少一半,最多需要进行 log(columnNumber) 次计算。
     * 而空间复杂度为 O(log(columnNumber)),需要额外的字符串来保存结果。
     */
    public static String convertToTitle(int columnNumber) {
        StringBuilder result = new StringBuilder();

        while (columnNumber > 0) {
            // 余数,减1是因为字符从 A 开始计数
            int remainder = (columnNumber - 1) % 26;
            // 将余数转换为对应的字符
            char ch = (char) ('A' + remainder);
            // 将字符拼接到结果字符串的最前面
            result.insert(0, ch);
            // 更新原始的正整数为商
            columnNumber = (columnNumber - 1) / 26;
        }

        return result.toString();
    }

    public static void main(String[] args) {
        int columnNumber1 = 1;
        int columnNumber2 = 28;
        int columnNumber3 = 701;
        int columnNumber4 = 2147483647;

        System.out.println("Input: " + columnNumber1 + " Output: " + convertToTitle(columnNumber1));
        System.out.println("Input: " + columnNumber2 + " Output: " + convertToTitle(columnNumber2));
        System.out.println("Input: " + columnNumber3 + " Output: " + convertToTitle(columnNumber3));
        System.out.println("Input: " + columnNumber4 + " Output: " + convertToTitle(columnNumber4));
    }
}

33、Excel Sheet Column Number(Excel表列序号)

题目描述:给定一个Excel表格中的列名称,返回其相应的列序号。

例如,

A -> 1

B -> 2

C -> 3

...

Z -> 26

AA -> 27

AB -> 28

...

示例 1:输入:columnTitle = "A"        输出:1

示例 2:输入:columnTitle = "AB”    输出:28

示例 3:输入:columnTitle = "ZY"    输出:701

示例 4:输入:columnTitle = "FXSHRXW"   输出:2147483647

提示:

- 1 <= columnTitle.length <= 7

- columnTitle 仅由大写英文组成- columnTitle 在范围 ["A", "FXSHRXW"] 内

解题思路

最优解法可以通过进制转换来实现Excel表列序号的计算。

  1. 从右向左遍历列名称的每个字符,将每个字符对应的数字值乘以 26 的幂,然后累加到结果中。
  2. 由于 Excel 表格中的列名称是26进制的表示,所以从右向左遍历可以保证按权重从小到大计算。
  3. 计算完成后得到的结果即为对应的列序号。

例如,对于 "AB",从右向左遍历可以得到 A=1 和 B=2,计算结果为 1 * 26^1 + 2 * 26^0 = 26 + 2 = 28,得到正确的列序号。

该算法的时间复杂度为 O(n),其中 n 是列名称的长度。因为需要遍历列名称的每个字符进行计算。而空间复杂度为 O(1),只需要常数级的额外空间来保存计算结果。

具体代码展示

package org.zyf.javabasic.letcode.math;

/**
 * @author yanfengzhang
 * @description 给定一个Excel表格中的列名称,返回其相应的列序号。
 * <p>
 * 例如,
 * A -> 1
 * B -> 2
 * C -> 3
 * ...
 * Z -> 26
 * AA -> 27
 * AB -> 28
 * ...
 * <p>
 * 示例 1:输入:columnTitle = "A"        输出:1
 * 示例 2:输入:columnTitle = "AB”    输出:28
 * 示例 3:输入:columnTitle = "ZY"    输出:701
 * 示例 4:输入:columnTitle = "FXSHRXW"   输出:2147483647
 * <p>
 * 提示:
 * - 1 <= columnTitle.length <= 7
 * - columnTitle 仅由大写英文组成
 * - columnTitle 在范围 ["A", "FXSHRXW"] 内
 * @date 2023/7/9  22:58
 */
public class ExcelSheetColumnNumber {

    /**
     * 最优解法可以通过进制转换来实现Excel表列序号的计算。
     * 1. 从右向左遍历列名称的每个字符,将每个字符对应的数字值乘以 26 的幂,然后累加到结果中。
     * 2. 由于 Excel 表格中的列名称是26进制的表示,所以从右向左遍历可以保证按权重从小到大计算。
     * 3. 计算完成后得到的结果即为对应的列序号。
     * 例如,对于 "AB",从右向左遍历可以得到 A=1 和 B=2,计算结果为 1 * 26^1 + 2 * 26^0 = 26 + 2 = 28,得到正确的列序号。
     * 该算法的时间复杂度为 O(n),其中 n 是列名称的长度。因为需要遍历列名称的每个字符进行计算。
     * 而空间复杂度为 O(1),只需要常数级的额外空间来保存计算结果。
     */
    public static int titleToNumber(String columnTitle) {
        int result = 0;
        // 幂,初始为0
        int power = 0;

        // 从右向左遍历列名称的每个字符
        for (int i = columnTitle.length() - 1; i >= 0; i--) {
            char ch = columnTitle.charAt(i);
            // 将字符对应的数字值计算出来
            int digit = ch - 'A' + 1;

            // 将每个字符对应的数字值乘以 26 的幂,并累加到结果中
            result += digit * Math.pow(26, power);
            // 幂加1,以便计算下一位的权重
            power++;
        }

        return result;
    }

    public static void main(String[] args) {
        String columnTitle1 = "A";
        String columnTitle2 = "AB";
        String columnTitle3 = "ZY";
        String columnTitle4 = "FXSHRXW";

        System.out.println("Input: " + columnTitle1 + " Output: " + titleToNumber(columnTitle1));
        System.out.println("Input: " + columnTitle2 + " Output: " + titleToNumber(columnTitle2));
        System.out.println("Input: " + columnTitle3 + " Output: " + titleToNumber(columnTitle3));
        System.out.println("Input: " + columnTitle4 + " Output: " + titleToNumber(columnTitle4));
    }
}

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张彦峰ZYF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值