数组知识及编程练习总结

目录

一、背景知识

二、数组的应用

(一)spring源码的应用

(二)日常开发中的数组应用

三、相关编程练习

1、两数之和 (Two Sum)

2、三数之和 (Three Sum)

3、最接近的三数之和 (3Sum Closest) 

备注:Arrays.sort底层原理

4、移动零 (Move Zeroes)

5、旋转数组 (Rotate Array)

6、搜索旋转排序数组 (Search in Rotated Sorted Array)

7、寻找旋转排序数组中的最小值 (Find Minimum in Rotated Sorted Array)

8、加一 (Plus One)

9、存在重复元素 (Contains Duplicate)

10、寻找数组的中心索引 (Find Pivot Index)

11、翻转对 (Reverse Pairs)

12、只出现一次的数字 (Single Number)

13、合并两个有序数组 (Merge Sorted Array)

14、合并区间 (Merge Intervals)

15、最大子序和 (Maximum Subarray)

备注1:动态规划算法思想解题步骤

16、最长连续递增序列 (Longest Continuous Increasing Subsequence)

17、最长公共前缀 (Longest Common Prefix)题目描述:

18、移除元素 (Remove Element)

19、除自身以外数组的乘积 (Product of Array Except Self)

20、颜色分类 (Sort Colors)

21、数组区间段加和

22、二维矩阵的第K大数

23、升序数组的两数之和

24、二维数组找到某个数

25、给定数组中寻找和大于等于给定值的最短子数组长度

26、数组的全排列

27、多数元素(Majority Element)

28、从1开始顺时针螺旋填充矩阵

29、26个字母按照顺时针螺旋填充矩阵

30、找到数组 A 元素组成的小于 n 的最大整数

31.合并N个有序数组

32、数组差值最小值


干货分享,感谢您的阅读!祝你逢考必过!

一、背景知识

  • 数组是一种线性数据结构,用于存储一组同类型的数据。
  • 数组是一段连续的内存空间,每个元素占用固定的空间大小。

 它可以被看作是一个盒子,其中每个元素被编号,且每个元素可以通过它的下标(索引)来访问。数组的下标从0开始,即第一个元素的下标是0,第二个元素的下标是1,以此类推。

在数组中,元素的类型是固定的,例如整数型数组只能存储整数,字符型数组只能存储字符。在创建数组时,需要指定数组的大小,即需要存储的元素数量。这个大小在数组创建后是无法改变的。

访问数组中的元素是通过下标进行的。我们可以使用下标来获取、修改和删除数组中的元素。需要注意的是,如果试图访问超出数组范围的元素,会导致越界错误。

在数组中添加或删除元素是一个比较麻烦的过程。当需要添加或删除元素时,需要创建一个新的数组,并将原数组的元素复制到新数组中,然后再添加或删除新元素。这个过程需要额外的时间和空间成本。

在算法中,数组是一个非常基础且常用的数据结构。很多算法都是基于数组进行操作的,例如排序、搜索、二分查找等。因此,对于数组的掌握和理解非常重要。

二、数组的应用

(一)spring源码的应用

在 Spring 源码中,数组是一个非常常见的数据结构,它被广泛用于各种场景,例如:

  • Bean 数组:在 Spring 的 IoC 容器中,Bean 定义是以数组的形式存在的,每个数组元素都代表一个 Bean。

  • 请求处理链:Spring MVC 中的请求处理链,实际上是由一个 HandlerInterceptor 数组组成的,这个数组存储了所有的拦截器,它们按顺序依次对请求进行处理。

  • 拦截器链:在 Spring AOP 中,代理对象的方法调用会依次经过一系列的拦截器,这些拦截器同样是以数组的形式存在的。

  • 事件监听器数组:在 Spring 的事件机制中,所有的事件监听器都被存储在一个监听器数组中,当事件发生时,依次触发监听器处理。

  • 权限过滤器链:在 Spring Security 中,请求的处理需要经过一系列的权限过滤器,这些过滤器同样是以数组的形式存在的。

(二)日常开发中的数组应用

在日常开发中,数组应用的场景还是很多的。下面列举一些常见的场景:

  • 缓存的数据结构:我们经常使用数组来实现缓存结构,比如在一些计算场景下我们需要缓存之前的计算结果,而数组可以方便地进行随机访问。

  • 数据库中存储大量数据:数据库中很多数据存储是以数组的方式进行的,可以使用数组进行数据分页和快速检索。

  • 图像处理和音视频处理:在图像处理和音视频处理领域,我们经常需要对大量的像素点或音视频数据进行处理,而这些数据可以使用数组进行存储和处理。

  • 排序和查找:在算法领域,数组也是最常用的数据结构之一,比如快速排序、归并排序、二分查找等算法都是基于数组的。

  • 网络协议:在网络协议领域,我们经常使用字节数组进行数据传输,比如TCP、UDP协议中都会使用字节数组来存储数据。

三、相关编程练习

说明:如果有写错的,请留言指正,感谢阅读者!

1、两数之和 (Two Sum)

题目描述:两数之和 (Two Sum) 在一个数组中找到两个数,使得它们的和等于一个给定的目标值。

解题思路

两数之和(Two Sum)的高效求解方法是使用哈希表(Hash Table)。具体思路是:遍历整个数组,对于每个数字,用目标值减去该数字得到一个差值,然后在哈希表中查找这个差值是否存在。如果存在,说明找到了符合条件的两个数字,返回它们的下标;如果不存在,就将当前数字加入哈希表中,继续遍历后面的数字。

这种方法的时间复杂度是O(n),空间复杂度是O(n),其中n是数组中元素的个数。

具体代码展示

package org.zyf.javabasic.letcode.array;

import com.google.common.collect.Maps;

import java.util.Arrays;
import java.util.Map;

/**
 * @author yanfengzhang
 * @description 两数之和 (Two Sum)
 * 题目描述:在一个数组中找到两个数,使得它们的和等于一个给定的目标值。
 * @date 2023/3/30  23:19
 */
public class TwoNumSum {

    /**
     * 两数之和(Two Sum)的高效求解方法是使用哈希表(Hash Table)。
     * 具体思路是:遍历整个数组,对于每个数字,用目标值减去该数字得到一个差值,然后在哈希表中查找这个差值是否存在。
     * 如果存在,说明找到了符合条件的两个数字,返回它们的下标;
     * 如果不存在,就将当前数字加入哈希表中,继续遍历后面的数字。
     * <p>
     * 这种方法的时间复杂度是O(n),空间复杂度是O(n),其中n是数组中元素的个数。
     *
     * @param nums   输入数组
     * @param target 目标值
     * @return 返回对应两个数的下表
     */
    public int[] twoSum(int[] nums, int target) {
        if (null == nums || nums.length == 0) {
            throw new IllegalArgumentException("nums array is illegal!");
        }

        /*创建一个哈希表,用于存储数字及其下标*/
        Map<Integer, Integer> twoSumMap = Maps.newHashMap();
        for (int i = 0; i < nums.length; i++) {
            /*计算目标值与当前数字的差值*/
            int complement = target - nums[i];
            /*如果差值在哈希表中已经存在,则找到符合条件的两个数字,返回它们的下标*/
            if (twoSumMap.containsKey(complement)) {
                return new int[]{twoSumMap.get(complement), i};
            }
            /*将当前数字及其下标存入哈希表中*/
            twoSumMap.put(nums[i], i);
        }
        /*如果没有找到符合条件的两个数字,则抛出异常*/
        throw new IllegalArgumentException("No two sum solution");
    }

    /**
     * 使用哈希表是两数之和问题的最优解,时间复杂度为O(n),其中n是数组中元素的个数。
     * 因为在哈希表中查找一个元素的时间复杂度是O(1),所以对于每个数字,我们只需要一次哈希表查找就能找到它对应的另一个数字。
     * <p>
     * 除了哈希表之外,还有一种时间复杂度为O(nlogn)的解法,即先将数组排序,然后使用双指针进行查找。
     * 具体步骤如下:
     * 1 将数组进行排序。
     * 2 使用双指针i和j分别指向数组的开头和结尾。
     * 3 如果nums[i] + nums[j]等于目标值,就找到了符合条件的两个数字,返回它们的下标。
     * 4 如果nums[i] + nums[j]小于目标值,就将i指针向右移动一位。
     * 5 如果nums[i] + nums[j]大于目标值,就将j指针向左移动一位。
     * 6 重复执行步骤3到5,直到找到符合条件的两个数字或者i和j指针相遇为止。
     * 虽然这种方法的时间复杂度比哈希表略高,但是它并不需要额外的空间,因此如果空间限制很严格,可以考虑使用这种方法。
     *
     * @param nums   输入数组
     * @param target 目标值
     * @return 返回对应两个数的下表
     */
    public int[] twoSumIndex(int[] nums, int target) {
        if (null == nums || nums.length == 0) {
            throw new IllegalArgumentException("nums array is illegal!");
        }

        /*复制数组,排序后的数组将用于双指针查找*/
        int[] sortedNums = Arrays.copyOf(nums, nums.length);
        /*对数组进行排序*/
        Arrays.sort(sortedNums);
        /*初始化指针i和j,分别指向数组的开头和结尾*/
        int i = 0, j = nums.length - 1;
        while (i < j) {
            if (sortedNums[i] + sortedNums[j] == target) {
                /*如果找到符合条件的两个数字,就返回它们的下标*/
                break;
            } else if (sortedNums[i] + sortedNums[j] < target) {
                /*如果两个数字之和小于目标值,就将i指针向右移动一位*/
                i++;
            } else {
                /*如果两个数字之和大于目标值,就将j指针向左移动一位*/
                j--;
            }
        }

        if (nums[i] + nums[j] != target) {
            /*如果没有找到符合条件的两个数字,则抛出异常*/
            throw new IllegalArgumentException("No two sum solution");
        }

        /*初始化两个数字的下标*/
        int index1 = -1, index2 = -1;
        for (int k = 0; k < nums.length; k++) {
            if (nums[k] == sortedNums[i] && index1 == -1) {
                /*如果找到第一个数字的下标,就将其存储在index1中*/
                index1 = k;
            } else if (nums[k] == sortedNums[j] && index2 == -1) {
                /*如果找到第二个数字的下标,就将其存储在index2中*/
                index2 = k;
            }
        }

        /*返回符合条件的两个数字的下标*/
        return new int[]{index1, index2};
    }

    /**
     * 使用双指针技巧对排序数组进行求解。
     * 因为数组是已经排序好的,所以可以将左右两个指针分别指向数组的第一个元素和最后一个元素,计算它们对应的值的和,
     * 然后根据和与目标值的大小关系移动指针,不断缩小搜索范围。
     * 具体而言,每次比较左右指针所指元素的和与目标值的大小关系:
     * 1 如果和等于目标值,说明找到了满足条件的两个数,直接返回它们的下标;
     * 2 如果和小于目标值,说明左指针所指元素过小,需要将左指针右移一位;
     * 3 如果和大于目标值,说明右指针所指元素过大,需要将右指针左移一位。
     * 这样不断地移动指针,直到找到满足条件的两个数,或者搜索范围为空,这时候说明没有满足条件的两个数,抛出异常即可。
     * 这种方法避免了使用哈希表或者暴力枚举的额外空间开销,时间复杂度为 O(n),空间复杂度为 O(1),是一种高效的解法。
     *
     * @param nums   输入数组
     * @param target 目标值
     * @return 返回对应两个数的下表
     */
    public int[] twoSumForSortArray(int[] nums, int target) {
        if (null == nums || nums.length == 0) {
            throw new IllegalArgumentException("nums array is illegal!");
        }

        int left = 0, right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
                return new int[]{left + 1, right + 1};
            } else if (sum < target) {
                left++;
            } else {
                right--;
            }
        }
        throw new IllegalArgumentException("No two sum solution");
    }


    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        int[] expected = {0, 1};

        System.out.println(Arrays.equals(new TwoNumSum().twoSum(nums, target), expected));
        System.out.println(Arrays.equals(new TwoNumSum().twoSumIndex(nums, target), expected));
        System.out.println(Arrays.equals(new TwoNumSum().twoSumForSortArray(nums, target), expected));

        target = 93;
        System.out.println(Arrays.equals(new TwoNumSum().twoSumIndex(nums, target), expected));
        System.out.println(Arrays.equals(new TwoNumSum().twoSumForSortArray(nums, target), expected));
    }
}

2、三数之和 (Three Sum)

题目描述:在一个数组中找到三个数,使得它们的和等于0。

解题思路

目前时间复杂度最优的三数之和解法是基于双指针的方法。

具体思路是先对数组进行排序,然后从小到大枚举第一个数,再用双指针分别指向第一个数后面的数组首尾两端,向中间逼近寻找满足条件的三元组。

具体代码展示

package org.zyf.javabasic.letcode.array;

import com.google.common.collect.Lists;

import java.util.Arrays;
import java.util.List;

/**
 * @author yanfengzhang
 * @description 在一个数组中找到三个数,使得它们的和等于0。
 * @date 2023/3/30  23:29
 */
public class ThreeNumSum {

    /**
     * 目前时间复杂度最优的三数之和解法是基于双指针的方法
     * 具体思路是先对数组进行排序,然后从小到大枚举第一个数,再用双指针分别指向第一个数后面的数组首尾两端,向中间逼近寻找满足条件的三元组。
     * <p>
     * 该算法的时间复杂度为 O(n^2),与暴力枚举法相比,具有更好的性能。
     * 该算法的空间复杂度为 O(1),因为仅使用了常数个额外的变量用于存储一些临时值,不会随着输入规模的增加而增加空间消耗。
     * 因此,在时间复杂度与空间复杂度均较为敏感的场景中,基于双指针的三数之和解法是一种较为优秀的选择。
     *
     * @param nums 输入数组
     * @return
     */
    public List<List<Integer>> threeSum(int[] nums) {
        /*边界条件检查*/
        if (nums == null || nums.length < 3) {
            return Lists.newArrayList();
        }

        /*先排序*/
        Arrays.sort(nums);
        List<List<Integer>> res = Lists.newArrayList();
        for (int i = 0; i < nums.length - 2; i++) {
            if (nums[i] > 0) {
                /*如果当前数已经大于0,后面的数不可能加起来等于0*/
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                /*避免重复计算*/
                continue;
            }

            int left = i + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    left++;
                    right--;
                    /*避免重复计算*/
                    while (left < right && nums[left] == nums[left - 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }

        return res;
    }

    public static void main(String[] args) {
        int[] nums = {-1, 0, 1, 2, -1, -4};
        List<List<Integer>> res = new ThreeNumSum().threeSum(nums);
        System.out.println(res);
    }
}

3、最接近的三数之和 (3Sum Closest) 

题目描述:给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

例如,给定数组 nums = [-1,2,1,-4], 和 target = 1.

与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2)。

解题思路

最优解法是先将数组排序,然后利用双指针扫描数组。具体思路如下:

  1. 将数组排序。
  2. 遍历数组,以当前元素为基准,使用双指针扫描基准元素之后的数组。
  3. 双指针指向的两个元素和基准元素相加,计算它们和target的差值。
  4. 如果差值小于当前最小差值(初始值可以设置为无穷大),则更新当前最小差值和结果。
  5. 根据差值与0的关系移动左右指针。

最后得到的结果即为最接近target的三数之和。

这个算法的时间复杂度为O(n^2),与3Sum相比,只是多了一个计算差值和更新结果的过程。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定一个包括 n 个整数的数组 nums 和 一个目标值 target。
 * 找出 nums 中的三个整数,使得它们的和与 target 最接近。
 * 返回这三个数的和。假定每组输入只存在唯一答案。
 * 例如,给定数组 nums = [-1,2,1,-4], 和 target = 1.
 * 与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2)。
 * @date 2023/4/1  22:19
 */
public class ThreeSumClosest {
    /**
     * 最优解法是先将数组排序,然后利用双指针扫描数组。具体思路如下:
     * 1 将数组排序。
     * 2 遍历数组,以当前元素为基准,使用双指针扫描基准元素之后的数组。
     * 3 双指针指向的两个元素和基准元素相加,计算它们和target的差值。
     * 4 如果差值小于当前最小差值(初始值可以设置为无穷大),则更新当前最小差值和结果。
     * 5 根据差值与0的关系移动左右指针。
     * 最后得到的结果即为最接近target的三数之和。
     */
    public int threeSumClosest(int[] nums, int target) {
        int n = nums.length;
        /*首先将数组排序*/
        Arrays.sort(nums);
        int best = 10000000;

        /*枚举 a*/
        for (int i = 0; i < n; ++i) {
            /*保证和上一次枚举的数不相等*/
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            /*使用双指针枚举 b 和 c*/
            int j = i + 1, k = n - 1;
            while (j < k) {
                int sum = nums[i] + nums[j] + nums[k];
                /*如果和为 target 直接返回答案*/
                if (sum == target) {
                    return target;
                }
                /*根据差值的绝对值来更新答案*/
                if (Math.abs(sum - target) < Math.abs(best - target)) {
                    best = sum;
                }
                if (sum > target) {
                    /*如果和大于 target,移动 c 对应的指针*/
                    int k0 = k - 1;
                    /*移动到下一个不相等的元素*/
                    while (j < k0 && nums[k0] == nums[k]) {
                        --k0;
                    }
                    k = k0;
                } else {
                    /*如果和小于 target,移动 b 对应的指针*/
                    int j0 = j + 1;
                    /*移动到下一个不相等的元素*/
                    while (j0 < k && nums[j0] == nums[j]) {
                        ++j0;
                    }
                    j = j0;
                }
            }
        }
        return best;
    }

    public static void main(String[] args) {
        int[] nums = {-1, 2, 1, -4};
        int target = 1;
        System.out.println(new ThreeSumClosest().threeSumClosest(nums, target));
    }

}

备注:Arrays.sort底层原理

Java中的Arrays.sort()方法实现了对数组的排序。具体实现方式会根据不同的数据类型(如整型、浮点型、自定义对象等)采用不同的排序算法,其中对于基本数据类型的数组,采用的是双轴快速排序(Dual-Pivot Quicksort)算法。

双轴快速排序算法是一种快速排序的变种,其基本思想是将数组分为三部分,其中第一部分是小于第一个枢轴元素的元素,第二部分是介于第一个枢轴元素和第二个枢轴元素之间的元素,第三部分是大于第二个枢轴元素的元素。然后对前两部分和后两部分分别递归执行排序操作,直到最终完成整个数组的排序。

在实现过程中,双轴快速排序算法采用了多个优化策略,如当待排序数组长度小于某个阈值时,会转而采用插入排序或归并排序等算法。此外,它还对递归的深度进行了限制,防止因递归层数过多而导致栈溢出。

除了双轴快速排序,对于其他类型的数组,Arrays.sort()方法还可能采用归并排序、插入排序等算法实现排序操作。在Java 8中,对于大部分情况下的排序操作,Arrays.sort()方法还提供了并行排序的实现方式,可以更好地利用多核处理器的性能优势。

4、移动零 (Move Zeroes)

题目描述:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:输入: [0,1,0,3,12] 输出: [1,3,12,0,0]

解题思路

双指针法:使用两个指针 leftright,其中 left 指向第一个 0 的位置,right 指向第一个不为 0 的位置,然后将 right 指向的数赋值给 left 指向的位置,同时将 right 指针后移,left 指针也后移,如此循环,直到 right 指针超出数组范围。

最后,将 left 指针之后的所有元素置为 0,即可得到移动零后的数组。

这种解法时间复杂度为 O(n),空间复杂度为 O(1),是最优解法。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 移动零 (Move Zeroes) 题目描述:
 * 给定一个数组 nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。
 * <p>
 * 示例:
 * 输入: [0,1,0,3,12]
 * 输出: [1,3,12,0,0]
 * @date 2023/3/30  23:04
 */
public class MoveZeroes {

    /**
     * 双指针法:
     * 使用两个指针 left 和 right,其中 left 指向第一个 0 的位置,right 指向第一个不为 0 的位置,
     * 然后将 right 指向的数赋值给 left 指向的位置,同时将 right 指针后移,left 指针也后移,如此循环,直到 right 指针超出数组范围。
     * 最后,将 left 指针之后的所有元素置为 0,即可得到移动零后的数组。
     * <p>
     * 这种解法时间复杂度为 O(n),空间复杂度为 O(1),是最优解法。
     * 因为它只需要遍历一遍数组,时间复杂度为 O(n),同时不需要额外的空间,空间复杂度为 O(1)。
     * 相比于其他解法,如暴力遍历或是双指针交换,该算法可以更快地完成任务。同时,这个算法也可以保持非零元素的相对顺序,不会影响数组的其他元素。
     *
     * @param nums 输入数组
     */
    public void moveZeroes(int[] nums) {
        /*边界条件检查*/
        if (nums == null || nums.length == 0) {
            return;
        }

        /*定义双指针 left 和 right,分别指向第一个 0 的位置和第一个非 0 的位置*/
        int left = 0, right = 0;
        /*当 right 指针未到达数组末尾时*/
        while (right < nums.length) {
            /*如果 right 指针指向的数不为 0*/
            if (nums[right] != 0) {
                /*将 right 指针指向的数赋值给 left 指针指向的位置*/
                nums[left] = nums[right];
                /*left 指针后移*/
                left++;
            }
            /*right 指针后移*/
            right++;
        }
        /*将 left 指针之后的所有元素置为 0*/
        while (left < nums.length) {
            nums[left] = 0;
            left++;
        }
    }

    public static void main(String[] args) {
        int[] nums = {0, 1, 0, 3, 12};
        new MoveZeroes().moveZeroes(nums);
        System.out.println(Arrays.toString(nums));
    }
}

5、旋转数组 (Rotate Array)

题目描述:将一个含有 n 个元素的数组向右旋转 k 步。

例如,输入: [1,2,3,4,5,6,7] 和 k = 3,输出: [5,6,7,1,2,3,4]

要求使用空间复杂度为 O(1) 的原地算法进行解决。

解题思路

旋转数组的最优解法是“三次翻转算法”,时间复杂度为 O(n),空间复杂度为 O(1)。

具体步骤如下:

  1. 将整个数组翻转,即将数组中的元素首尾颠倒,这样原数组的最后 k 个元素就被移到了数组的前面。比如上面的例子,对 [1,2,3,4,5,6,7] 进行翻转,得到 [7,6,5,4,3,2,1]。

  2. 然后翻转数组的前 k 个元素,这样就能将原数组的最后 k 个元素移到数组的前面。对于上面的例子,翻转前 3 个元素,即 [7,6,5,4,3,2,1] 中的 [7,6,5],得到 [5,6,7,4,3,2,1]。

  3. 最后翻转数组中从第 k+1 个元素到末尾的元素,这样就能得到旋转后的数组。对于上面的例子,翻转从第 4 个元素到末尾的元素,即 [5,6,7,4,3,2,1] 中的 [4,3,2,1],得到 [5,6,7,1,2,3,4]。

通过这种算法,我们只需要对数组进行三次翻转,就能够得到旋转后的数组,而不需要使用额外的数组来存储中间结果,因此空间复杂度为 O(1)。而由于每个元素都只被访问了一次,时间复杂度为 O(n)。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 旋转数组 (Rotate Array)的原题目是:将一个含有 n 个元素的数组向右旋转 k 步。
 * 例如,输入: [1,2,3,4,5,6,7] 和 k = 3,输出: [5,6,7,1,2,3,4]
 * 要求使用空间复杂度为 O(1) 的原地算法进行解决。
 * @date 2023/3/30  23:24
 */
public class RotateArray {

    /**
     * 旋转数组的最优解法是“三次翻转算法”,时间复杂度为 O(n),空间复杂度为 O(1)。
     * <p>
     * 具体步骤如下:
     * 1. 将整个数组翻转,即将数组中的元素首尾颠倒,这样原数组的最后 k 个元素就被移到了数组的前面。
     * 比如上面的例子,对 [1,2,3,4,5,6,7] 进行翻转,得到 [7,6,5,4,3,2,1]。
     * 2. 然后翻转数组的前 k 个元素,这样就能将原数组的最后 k 个元素移到数组的前面。
     * 对于上面的例子,翻转前 3 个元素,即 [7,6,5,4,3,2,1] 中的 [7,6,5],得到 [5,6,7,4,3,2,1]。
     * 3. 最后翻转数组中从第 k+1 个元素到末尾的元素,这样就能得到旋转后的数组。
     * 对于上面的例子,翻转从第 4 个元素到末尾的元素,即 [5,6,7,4,3,2,1] 中的 [4,3,2,1],得到 [5,6,7,1,2,3,4]。
     * <p>
     * 通过这种算法,我们只需要对数组进行三次翻转,就能够得到旋转后的数组,而不需要使用额外的数组来存储中间结果,
     * 因此空间复杂度为 O(1)。而由于每个元素都只被访问了一次,时间复杂度为 O(n)。
     *
     * @param nums 输入数组
     * @param k    旋转 k 步
     */
    public void rotate(int[] nums, int k) {
        /*如果数组为null或长度为0,则直接返回*/
        if (nums == null || nums.length == 0) {
            return;
        }
        int n = nums.length;
        /*计算旋转次数k,若k%n=0,则不需要旋转,直接返回*/
        k %= n;
        if (k == 0) {
            return;
        }
        /*三次翻转数组:第一次翻转整个数组*/
        reverse(nums, 0, n - 1);
        /*三次翻转数组:第二次翻转前k个元素*/
        reverse(nums, 0, k - 1);
        /*三次翻转数组:第三次翻转后n-k个元素*/
        reverse(nums, k, n - 1);
    }

    /**
     * 翻转数组,使用双指针
     */
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start] = nums[end];
            nums[end] = temp;
            start++;
            end--;
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6, 7};
        int k = 3;
        new RotateArray().rotate(nums, k);
        System.out.println(Arrays.toString(nums));
    }
}

6、搜索旋转排序数组 (Search in Rotated Sorted Array)

题目描述:给定一个整数数组 nums ,按升序排列,数组中的元素各不相同。nums 可以在预先未知的某个点上进行旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从 0 开始计数)。例如,数组 [0,1,2,4,5,6,7] 在变化后可能变为 [4,5,6,7,0,1,2]

请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1

示例1:输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4

示例2:输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1

示例3:输入:nums = [1], target = 0 输出:-1

提示:

  • 1 <= nums.length <= 5000
  • -10^4 <= nums[i] <= 10^4
  • nums 中的每个值都 独一无二`
  • 题目数据保证 nums 在预先未知的某个点上进行了旋转
  • -10^4 <= target <= 10^4

来源:力扣(LeetCode)

解题思路

搜索旋转排序数组问题的最优解法是二分查找算法,时间复杂度为 O(log n)。

该算法的关键在于确定哪一部分是有序的。我们可以将数组分为两部分,左半部分 [left, mid] 和右半部分 [mid + 1, right],根据这个分界点,数组中必定有一半是有序的。

我们首先通过比较 nums[mid] 和 nums[left] 的值,可以确定左半部分 [left, mid] 是否有序。如果 nums[mid] >= nums[left],说明左半部分有序,否则右半部分 [mid + 1, right] 有序。

如果左半部分有序,那么我们可以根据 nums[left] <= target < nums[mid],将目标值 target 限制在左半部分。否则,将目标值 target 限制在右半部分。

同样的,如果右半部分有序,那么我们可以根据 nums[mid] < target <= nums[right],将目标值 target 限制在右半部分。否则,将目标值 target 限制在左半部分。

接下来,我们只需要在限制范围内继续执行二分查找即可。如果在查找过程中始终没有找到目标值,则返回 -1。

这里使用二分查找的思想,每次取中间点 mid,并判断左半边是否有序,如果左半边有序,则判断目标值是否在左半边有序区间内,如果在,则向左搜索,否则向右搜索。如果左半边不是有序的,则右半边一定有序,判断目标值是否在右半边有序区间内,如果在,则向右搜索,否则向左搜索。最终如果没找到目标值,返回 -1。

这个算法的时间复杂度是 O(log n),空间复杂度是 O(1)。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个整数数组 nums ,按升序排列,数组中的元素各不相同。
 * nums 可以在预先未知的某个点上进行旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从 0 开始计数)。
 * 例如,数组 [0,1,2,4,5,6,7] 在变化后可能变为 [4,5,6,7,0,1,2] 。
 * <p>
 * 请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1。
 * 示例1:输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4
 * 示例2:输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1
 * 示例3:输入:nums = [1], target = 0 输出:-1
 * <p>
 * 提示:
 * 1 <= nums.length <= 5000
 * -10^4 <= nums[i] <= 10^4
 * nums 中的每个值都 独一无二`
 * 题目数据保证 nums 在预先未知的某个点上进行了旋转
 * -10^4 <= target <= 10^4
 * @date 2023/4/1  22:54
 */
public class SearchRotatedSortedArray {

    /**
     * 搜索旋转排序数组问题的最优解法是二分查找算法,时间复杂度为 O(log n)。
     * 该算法的关键在于确定哪一部分是有序的。我们可以将数组分为两部分,左半部分 [left, mid] 和右半部分 [mid + 1, right],根据这个分界点,数组中必定有一半是有序的。
     * 我们首先通过比较 nums[mid] 和 nums[left] 的值,可以确定左半部分 [left, mid] 是否有序。
     * 如果 nums[mid] >= nums[left],说明左半部分有序,否则右半部分 [mid + 1, right] 有序。
     * 如果左半部分有序,那么我们可以根据 nums[left] <= target < nums[mid],将目标值 target 限制在左半部分。否则,将目标值 target 限制在右半部分。
     * 同样的,如果右半部分有序,那么我们可以根据 nums[mid] < target <= nums[right],将目标值 target 限制在右半部分。否则,将目标值 target 限制在左半部分。
     * 接下来,我们只需要在限制范围内继续执行二分查找即可。如果在查找过程中始终没有找到目标值,则返回 -1。
     * <p>
     * 这里使用二分查找的思想,每次取中间点 mid,并判断左半边是否有序,
     * 如果左半边有序,则判断目标值是否在左半边有序区间内,如果在,则向左搜索,否则向右搜索。
     * 如果左半边不是有序的,则右半边一定有序,判断目标值是否在右半边有序区间内,如果在,则向右搜索,否则向左搜索。
     * 最终如果没找到目标值,返回 -1。
     * <p>
     * 这个算法的时间复杂度是 O(log n),空间复杂度是 O(1)。
     */
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            /*判断左半边是否有序*/
            if (nums[mid] >= nums[left]) {
                /*如果在左半边有序区间内*/
                if (target >= nums[left] && target < nums[mid]) {
                    /*向左搜索*/
                    right = mid - 1;
                } else {
                    //*否则向右搜索*/
                    left = mid + 1;
                }
            }
            /*否则右半边有序*/
            else {
                /*如果在右半边有序区间内*/
                if (target > nums[mid] && target <= nums[right]) {
                    /*向右搜索*/
                    left = mid + 1;
                } else {
                    /*否则向左搜索*/
                    right = mid - 1;
                }
            }
        }
        /*如果没找到,返回 -1*/
        return -1;
    }

    public static void main(String[] args) {
        int[] nums = {4, 5, 6, 7, 0, 1, 2};
        int target = 0;
        int index = new SearchRotatedSortedArray().search(nums, target);
        System.out.println(index);
    }
}

7、寻找旋转排序数组中的最小值 (Find Minimum in Rotated Sorted Array)

题目描述:假设一个按照升序排列的数组在预先未知的某个点上进行了旋转(例如,数组[0,1,2,4,5,6,7] 可能变为[4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

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

示例 2:输入: [4,5,6,7,0,1,2] 输出: 0

提示:

  • n是数组中的元素数量。
  • 没有重复元素。
  • 时间复杂度 O(logn)。

解题思路

旋转排序数组中的最小值可以通过二分查找的方法进行求解。具体步骤如下:

  1. 定义两个指针 leftright,分别指向数组的首尾元素。
  2. 如果数组本身就是非递减的(即未发生旋转),则直接返回数组的首元素。
  3. 如果数组已经发生了旋转,则在数组的左半部分和右半部分之间一定存在一个旋转点,使得左半部分的元素都大于右半部分的元素。我们可以通过二分查找的方式来找到旋转点,具体步骤如下:
    1. mid 为左右指针的中间位置。
    2. 判断 nums[mid]nums[right] 的大小关系:
      • 如果 nums[mid] > nums[right],说明旋转点一定在 [mid+1, right] 之间,令 left=mid+1
      • 否则,说明旋转点一定在 [left, mid] 之间,令 right=mid
    3. left == right 时,返回 nums[left]nums[right] 均可,因为此时的 leftright 就是旋转点。
  4. 最终返回的就是旋转点的值,即数组中的最小值。

该算法的时间复杂度为 O(log n),空间复杂度为 O(1)。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 假设一个按照升序排列的数组在预先未知的某个点上进行了旋转(例如,数组[0,1,2,4,5,6,7] 可能变为[4,5,6,7,0,1,2] )。
 * 请找出其中最小的元素。
 * 你可以假设数组中不存在重复元素。
 * 示例 1:输入: [3,4,5,1,2] 输出: 1
 * 示例 2:输入: [4,5,6,7,0,1,2] 输出: 0
 * <p>
 * 提示:
 * n是数组中的元素数量。
 * 没有重复元素。
 * 时间复杂度O(logn)
 * @date 2023/4/1  23:03
 */
public class MinimumInRotatedSortedArray {

    /**
     * 与搜索旋转排序数组不同的是,本题只需要返回最小值即可,因此没有目标值和边界值的判断。
     * 首先初始化左右指针为数组的首尾位置。在每一次循环中,
     * 先计算出中间位置 mid,然后判断如果 nums[mid] > nums[right],
     * 则最小值肯定在右半边,因此将左指针移到 mid + 1 处。
     * 否则,最小值可能在左半边或就是 mid 本身,因此将右指针移到 mid 处。
     * 当左指针 left 和右指针 right 相遇时,此时的值就是最小值。
     */
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                /*如果中间元素大于右边元素,则最小值在右半边*/
                left = mid + 1;
            } else {
                /*否则最小值在左半边或就是mid本身*/
                right = mid;
            }
        }
        return nums[left];
    }

    public static void main(String[] args) {
        int[] nums1 = {3, 4, 5, 1, 2};
        int[] nums2 = {4, 5, 6, 7, 0, 1, 2};
        int[] nums3 = {11, 13, 15, 17};

        System.out.println(new MinimumInRotatedSortedArray().findMin(nums1));
        System.out.println(new MinimumInRotatedSortedArray().findMin(nums2));
        System.out.println(new MinimumInRotatedSortedArray().findMin(nums3));
    }

}

8、加一 (Plus One)

题目描述:给定一个由非负整数组成的数组,表示一个整数,将这个整数加上一。

你可以假设这个整数不包含任何前导零,除了数字0本身。

最高位数字存放在列表的首位, 数组中每个元素只存储单个数字。

你可以将你的解决方案与标准的库函数进行比较。你可以假设两个数都不包含前导零,除了数字0本身。

解题思路

最优解法是直接模拟加法运算。从数组的最后一位开始,加上1后取余,如果余数不为0,则不会进位,直接返回数组。如果余数为0,则需要进位,进位后继续向前遍历,直到不需要进位为止。

具体步骤如下:

  1. 从数组的最后一位开始,加上1,并计算余数remainder = (digits[i] + 1) % 10,进位carry = (digits[i] + 1) / 10。
  2. 将余数remainder存入当前位digits[i],如果没有进位,直接返回digits数组。
  3. 如果有进位,将carry加入下一位digits[i-1]中,继续向前遍历。
  4. 如果遍历完整个数组digits后,还有进位,就需要在digits数组最前面添加一个1。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定一个由非负整数组成的数组,表示一个整数,将这个整数加上一。
 * 你可以假设这个整数不包含任何前导零,除了数字0本身。
 * 最高位数字存放在列表的首位, 数组中每个元素只存储单个数字。
 * 你可以将你的解决方案与标准的库函数进行比较。
 * 你可以假设两个数都不包含前导零,除了数字0本身。
 * @date 2023/4/2  00:16
 */
public class PlusOne {
    /**
     * 最优解法是直接模拟加法运算。
     * 从数组的最后一位开始,加上1后取余,如果余数不为0,则不会进位,直接返回数组。
     * 如果余数为0,则需要进位,进位后继续向前遍历,直到不需要进位为止。
     * <p>
     * 具体步骤如下:
     * 从数组的最后一位开始,加上1,并计算余数remainder = (digits[i] + 1) % 10,进位carry = (digits[i] + 1) / 10。
     * 将余数remainder存入当前位digits[i],如果没有进位,直接返回digits数组。
     * 如果有进位,将carry加入下一位digits[i-1]中,继续向前遍历。
     * 如果遍历完整个数组digits后,还有进位,就需要在digits数组最前面添加一个1。
     */
    public int[] plusOne(int[] digits) {
        for (int i = digits.length - 1; i >= 0; i--) {
            if (digits[i] < 9) {
                digits[i]++;
                return digits;
            }
            digits[i] = 0;
        }
        int[] res = new int[digits.length + 1];
        res[0] = 1;
        return res;
    }

    public static void main(String[] args) {
        int[] nums1 = {1, 2, 3};
        int[] res1 = new PlusOne().plusOne(nums1);
        System.out.println(Arrays.toString(res1));

        int[] nums2 = {9, 9, 9};
        int[] res2 = new PlusOne().plusOne(nums2);
        System.out.println(Arrays.toString(res2));

        int[] nums3 = {0};
        int[] res3 = new PlusOne().plusOne(nums3);
        System.out.println(Arrays.toString(res3));
    }


}

9、存在重复元素 (Contains Duplicate)

题目描述:给定一个整数数组,判断是否存在重复元素。

如果存在一值在数组中出现至少两次,函数返回 true 。 如果数组中每个元素都不相同,则返回 false 。

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

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

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

解题思路

针对这道题目,有多种解法,以下是其中两种最优解法:

解法一:哈希表

  • 思路:使用哈希表记录每个数字出现的次数,如果出现次数大于等于 2 次,则说明存在重复元素。
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

解法二:排序

  • 思路:先将数组排序,再从前往后遍历,如果相邻元素相等,则说明存在重复元素。
  • 时间复杂度:O(nlog n)
  • 空间复杂度:O(1)

具体代码展示

package org.zyf.javabasic.letcode.array;

import com.google.common.collect.Maps;

import java.util.Arrays;
import java.util.Map;

/**
 * @author yanfengzhang
 * @description 给定一个整数数组,判断是否存在重复元素。
 * 如果存在一值在数组中出现至少两次,函数返回 true 。
 * 如果数组中每个元素都不相同,则返回 false 。
 * <p>
 * 示例 1:输入: [1,2,3,1]  输出: true
 * 示例 2:输入: [1,2,3,4]  输出: false
 * 示例 3:输入: [1,1,1,3,3,4,3,2,4,2] 输出: true
 * <p>
 * 这个题目的最优解是使用哈希表来存储元素出现次数,然后遍历数组,
 * 对于每个元素,在哈希表中查找是否存在对应的元素,
 * 如果存在,则说明有重复元素,返回true,否则将元素加入哈希表中,继续遍历。
 * 时间复杂度为O(n),空间复杂度为O(n)。
 * @date 2023/3/31  23:13
 */
public class ContainsDuplicate {

    /**
     * 解法一:哈希表
     * 思路:使用哈希表记录每个数字出现的次数,如果出现次数大于等于 2 次,则说明存在重复元素。
     * 时间复杂度:O(n)
     * 空间复杂度:O(n)
     *
     * @param nums 输入数组
     * @return 如果数组中每个元素都不相同,则返回 false
     */
    public boolean containsDuplByMap(int[] nums) {
        /*边界条件检查*/
        if (nums == null || nums.length == 0) {
            return false;
        }

        Map<Integer, Integer> map = Maps.newHashMap();
        for (int num : nums) {
            if (map.containsKey(num)) {
                return true;
            } else {
                map.put(num, 1);
            }
        }
        return false;
    }

    /**
     * 解法二:排序
     * 思路:先将数组排序,再从前往后遍历,如果相邻元素相等,则说明存在重复元素。
     * 时间复杂度:O(nlogn)
     * 空间复杂度:O(1)
     *
     * @param nums 输入数组
     * @return 如果数组中每个元素都不相同,则返回 false
     */
    public boolean containsDuplBySort(int[] nums) {
        /*边界条件检查*/
        if (nums == null || nums.length == 0) {
            return false;
        }

        Arrays.sort(nums);
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1]) {
                return true;
            }
        }
        return false;
    }

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

        System.out.println("Test Case 1:   Input:" + Arrays.toString(nums1)
                + ",Output:" + new ContainsDuplicate().containsDuplByMap(nums1));
        System.out.println("Test Case 2:   Input:" + Arrays.toString(nums1)
                + ",Output:" + new ContainsDuplicate().containsDuplByMap(nums2));
    }

}

10、寻找数组的中心索引 (Find Pivot Index)

题目描述:给定一个整数类型的数组 nums,请编写一个能够返回数组“中心索引”的方法。

我们是这样定义数组中心索引的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。

如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。

示例1:输入:nums = [1, 7, 3, 6, 5, 6]       输出:3

解释:索引3(nums[3] = 6)左侧数之和(1 + 7 + 3 = 11),右侧数之和(5 + 6 = 11),二者相等。

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

解释:数组中不存在满足此条件的中心索引。

注意:

1.nums 的长度范围为 [0, 10000]。

2.任何一个 nums[i] 将会是一个范围在 [-1000, 1000]之间的整数。

解题思路

寻找数组的中心索引可以使用前缀和的思想解决,假设数组的总和为 sum,那么只要从左往右遍历数组,依次累加左侧的元素和 leftSum,右侧的元素和 rightSum 就可以找到中心索引。

具体地,假设当前遍历到的索引为 i,如果 i 是中心索引,则满足以下条件:

  1. leftSum = rightSum - nums[i],即左侧元素和等于右侧元素和减去当前元素值。

  2. sum - nums[i] - leftSum = leftSum,即数组总和减去当前元素值减去左侧元素和等于左侧元素和。

根据以上条件,只需要从左往右遍历一次数组,依次计算左侧元素和,再判断是否满足上述条件即可找到中心索引。

如果遍历完整个数组都没有找到中心索引,则说明数组没有中心索引,返回 -1。

该算法的时间复杂度为 O(n),空间复杂度为 O(1)。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个整数类型的数组 nums,请编写一个能够返回数组“中心索引”的方法。
 * 我们是这样定义数组中心索引的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。
 * 如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。
 * 示例1:输入:nums = [1, 7, 3, 6, 5, 6]  输出:3
 * 解释:索引3(nums[3] = 6)左侧数之和(1 + 7 + 3 = 11),右侧数之和(5 + 6 = 11),二者相等。
 * 示例2:输入:nums = [1, 2, 3]   输出:-1
 * 解释:数组中不存在满足此条件的中心索引。
 * <p>
 * 注意:
 * 1.nums 的长度范围为 [0, 10000]。
 * 2.任何一个 nums[i] 将会是一个范围在 [-1000, 1000]之间的整数。
 * @date 2023/4/2  00:29
 */
public class FindPivotIndex {

    /**
     * 寻找数组的中心索引可以使用前缀和的思想解决,
     * 假设数组的总和为 sum,那么只要从左往右遍历数组,依次累加左侧的元素和 leftSum,右侧的元素和 rightSum 就可以找到中心索引。
     * <p>
     * 具体地,假设当前遍历到的索引为 i,如果 i 是中心索引,则满足以下条件:
     * 1 leftSum = rightSum - nums[i],即左侧元素和等于右侧元素和减去当前元素值。
     * 2 sum - nums[i] - leftSum = leftSum,即数组总和减去当前元素值减去左侧元素和等于左侧元素和。
     * 根据以上条件,只需要从左往右遍历一次数组,依次计算左侧元素和,再判断是否满足上述条件即可找到中心索引。
     * 如果遍历完整个数组都没有找到中心索引,则说明数组没有中心索引,返回 -1。
     * 该算法的时间复杂度为 O(n),空间复杂度为 O(1)。
     */
    public int pivotIndex(int[] nums) {
        /*首先计算整个数组的总和*/
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        int leftSum = 0;
        for (int i = 0; i < nums.length; i++) {
            /*判断是否是中心索引,如果是则直接返回*/
            if (leftSum == sum - leftSum - nums[i]) {
                return i;
            }
            leftSum += nums[i];
        }
        /*如果找不到中心索引,则返回 -1*/
        return -1;
    }

    public static void main(String[] args) {
        int[] nums1 = {1, 7, 3, 6, 5, 6};
        System.out.println(new FindPivotIndex().pivotIndex(nums1));

        int[] nums2 = {1, 2, 3};
        System.out.println(new FindPivotIndex().pivotIndex(nums2));

        int[] nums3 = {2, 1, -1};
        System.out.println(new FindPivotIndex().pivotIndex(nums3));
    }
}

11、翻转对 (Reverse Pairs)

题目描述:翻转对指的是对于数组中的两个元素 nums[i] 和 nums[j],如果 i<j 且 nums[i] > 2*nums[j],那么就称其为一个翻转对。

给定一个数组 nums,求出该数组中的翻转对数量。

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

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

注意:

  • 给定数组的长度不超过 50000。
  • 输入数组中的所有数字都在 32 位整数的表示范围内。

解题思路

翻转对问题可以使用归并排序算法来解决,具体方法如下:

  1. 将给定数组分成左右两个部分,分别递归地求出左右两个部分的翻转对数量。
  2. 对于左右两个部分,按照从小到大的顺序合并起来,并统计出左右两个部分之间的翻转对数量。
  3. 将步骤 2 中合并后的数组返回。

整个过程中,每次递归的数组长度都会缩小一半,因此该算法的时间复杂度为 O(nlogn)。

需要注意的是,在合并两个有序数组时,可以利用归并排序的思想,用两个指针分别指向左右两个数组的开头,然后依次比较两个指针指向的元素,将较小的元素加入新的数组,并将其指针向后移动一位。如果右边的指针指向的元素小于左边的指针指向的元素,则说明右边的指针指向的元素和左边所有未加入新数组的元素都构成翻转对,因此可以将其贡献加到结果中。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 翻转对指的是对于数组中的两个元素 nums[i]和 nums[j],
 * 如果 i<j 且 nums[i] > 2*nums[j],那么就称其为一个翻转对。
 * <p>
 * 给定一个数组 nums,求出该数组中的翻转对数量。
 * 示例 1:输入: [1,3,2,3,1]  输出: 2
 * 示例 2:输入: [2,4,3,5,1]  输出: 3
 * <p>
 * 注意:
 * 给定数组的长度不超过 50000。
 * 输入数组中的所有数字都在 32 位整数的表示范围内。
 * @date 2023/4/2  00:41
 */
public class ReversePairs {

    /**
     * 翻转对问题可以使用归并排序算法来解决,具体方法如下:
     * 1 将给定数组分成左右两个部分,分别递归地求出左右两个部分的翻转对数量。
     * 2 对于左右两个部分,按照从小到大的顺序合并起来,并统计出左右两个部分之间的翻转对数量。
     * 3 将步骤 2 中合并后的数组返回。
     * 整个过程中,每次递归的数组长度都会缩小一半,因此该算法的时间复杂度为 O(nlogn)。
     */
    public int reversePairs(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        return mergeSort(nums, 0, nums.length - 1);
    }

    private int mergeSort(int[] nums, int left, int right) {
        if (left >= right) {
            return 0;
        }
        int mid = left + (right - left) / 2;
        /*统计左右两侧的翻转对数目*/
        int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);
        int[] cache = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0, p = mid + 1;
        while (i <= mid) {
            while (p <= right && nums[i] > 2L * nums[p]) {
                /*统计左半部分的数与右半部分的数之间的翻转对数目*/
                p++;
            }
            count += p - (mid + 1);
            while (j <= right && nums[i] >= nums[j]) {
                /*合并左半部分和右半部分的元素*/
                cache[k++] = nums[j++];
            }
            cache[k++] = nums[i++];
        }
        while (j <= right) {
            /*右半部分有剩余元素*/
            cache[k++] = nums[j++];
        }
        /*将排序后的结果复制回原数组*/
        System.arraycopy(cache, 0, nums, left, right - left + 1);
        return count;
    }

    public static void main(String[] args) {
        int[] nums = {5, 2, 6, 1};
        System.out.println(new ReversePairs().reversePairs(nums));
    }

}

12、只出现一次的数字 (Single Number)

题目描述:给定一个非空整数数组,其中每个元素都出现了两次,只有一个元素出现了一次。找出这个只出现一次的元素。

例如,给定数组 nums = [2,2,1],返回 1。

要求算法时间复杂度为 O(n),并且不使用额外空间。

提示:位运算符可能有用。

解题思路

常见的最优解法是使用异或运算。具体思路如下:

  • 将所有数字进行异或运算,即将所有数字的二进制位逐位进行异或运算。由于异或运算满足交换律和结合律,所以异或运算可以随意交换操作数的位置,因此可以将所有相同的数字异或得到0。
  • 最终得到的结果即为只出现一次的数字,因为只有这个数字没有与其他数字配对进行异或运算,所以保留了原值。

这种方法的时间复杂度为 O(n),空间复杂度为 O(1)。

该算法利用了 XOR 的两个性质:

  • 任何数和 0 做异或运算,结果仍然是原来的数。
  • 任何数和其自身做异或运算,结果是 0。

因此,如果我们对所有数字进行异或运算,得到的结果即为那个只出现一次的数字。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 只出现一次的数字是一个经典的问题,给定一个非空整数数组,其中每个元素都出现了两次,只有一个元素出现了一次。找出这个只出现一次的元素。
 * 例如,给定数组 nums = [2,2,1],返回 1。
 * 要求算法时间复杂度为 O(n),并且不使用额外空间。
 * 提示:位运算符可能有用。
 * @date 2023/3/31  23:36
 */
public class SingleNumber {

    /**
     * 常见的最优解法是使用异或运算。具体思路如下:
     * 1.将所有数字进行异或运算,即将所有数字的二进制位逐位进行异或运算。
     * 由于异或运算满足交换律和结合律,所以异或运算可以随意交换操作数的位置,因此可以将所有相同的数字异或得到0。
     * 2.最终得到的结果即为只出现一次的数字,因为只有这个数字没有与其他数字配对进行异或运算,所以保留了原值。
     * 这种方法的时间复杂度为 O(n),空间复杂度为 O(1)。
     * <p>
     * 该算法利用了 XOR 的两个性质:
     * 任何数和 0 做异或运算,结果仍然是原来的数。
     * 任何数和其自身做异或运算,结果是 0。
     * 因此,如果我们对所有数字进行异或运算,得到的结果即为那个只出现一次的数字。
     *
     * @param nums 输入数组
     * @return 只出现一次的元素
     */
    public int singleNumber(int[] nums) {
        /*如果数组为null或长度为0,则直接返回*/
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException("nums array is illegal!");
        }
        int ans = 0;
        for (int num : nums) {
            ans ^= num;
        }
        return ans;
    }

    public static void main(String[] args) {
        int[] nums = {0, 1, 0, 1, 12};
        System.out.println(new SingleNumber().singleNumber(nums));
    }
}

13、合并两个有序数组 (Merge Sorted Array)

题目描述:给你两个有序整数数组 nums1nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1nums2 的元素数量分别为 mn 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

示例 1:输入:nums1 = [1,2,3,0,0,0], m = 3  nums2 = [2,5,6], n = 3  输出:[1,2,2,3,5,6]

解释:通过将 nums2 合并到 nums1 中,我们得到了 [1,2,2,3,5,6] 。

示例 2:输入:nums1 = [1], m = 1 nums2 = [], n = 0  输出:[1]

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -109 <= nums1[i], nums2[i] <= 109

解题思路

最优解法是双指针法,具体思路如下:

  1. 定义两个指针分别指向两个数组的末尾,一个指向 nums1 数组末尾,一个指向 nums2 数组末尾,同时定义一个指向 nums1 数组最后一个元素的指针 p1
  2. 比较指针指向的元素,将大的元素放到 nums1 数组的末尾,并将对应的指针向前移动一位。
  3. 当其中一个数组遍历完成后,将另一个数组中剩余的元素逐一复制到 nums1 数组的前面。

该算法的时间复杂度为 O(m+n),其中 m 和 n 分别为两个数组的长度。空间复杂度为 O(1),因为只需要常数级别的额外空间。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
 * 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
 * 示例 1:输入:nums1 = [1,2,3,0,0,0], m = 3
 * nums2 = [2,5,6],       n = 3
 * 输出:[1,2,2,3,5,6]     =====通过将 nums2 合并到 nums1 中,我们得到了 [1,2,2,3,5,6] 。
 * 示例 2:输入:nums1 = [1], m = 1
 * nums2 = [],  n = 0
 * 输出:[1]
 * 提示:
 * nums1.length == m + n
 * nums2.length == n
 * 0 <= m, n <= 200
 * 1 <= m + n <= 200
 * -109 <= nums1[i], nums2[i] <= 109
 * 来源:力扣(LeetCode)
 * 链接:https://leetcode-cn.com/problems/merge-sorted-array
 * @date 2023/3/31  23:20
 */
public class MergeSortArray {

    /**
     * 最优解法是双指针法,具体思路如下:
     * 1 由于两个数组都是有序的,所以可以使用双指针法,分别从两个数组的头部开始比较大小,将较小的元素插入到新的数组中。
     * 2 然后移动指针,再次比较大小,继续插入。
     * 3 如果某个数组遍历完了,直接将另一个数组剩下的元素全部插入到新的数组中。
     * 4 最后得到的新数组就是合并后的有序数组。
     * <p>
     * 这种方法的时间复杂度是 O(m+n),其中 m 和 n 分别是两个数组的长度。
     * 空间复杂度为 O(1),因为只需要常数级别的额外空间。
     */
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        /*定义指针 p1,指向 nums1 的最后一个元素*/
        int p1 = m - 1;
        /*定义指针 p2,指向 nums2 的最后一个元素*/
        int p2 = n - 1;
        /*定义指针 p,指向 nums1 的最后一个位置*/
        int p = m + n - 1;

        /*从后往前遍历数组,比较 nums1 和 nums2 中的元素,将较大的元素插入到 nums1 数组中*/
        while (p1 >= 0 && p2 >= 0) {
            if (nums1[p1] > nums2[p2]) {
                nums1[p] = nums1[p1];
                p1--;
            } else {
                nums1[p] = nums2[p2];
                p2--;
            }
            p--;
        }

        /*将 nums2 中剩余的元素逐一复制到 nums1 的前面*/
        System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
    }

    public static void main(String[] args) {
        int[] nums1 = {1, 2, 3, 0, 0, 0};
        int[] nums2 = {2, 5, 6};
        int m = 3, n = 3;
        new MergeSortArray().merge(nums1, m, nums2, n);
        System.out.println(Arrays.toString(nums1));

        int[] nums3 = {0, 0, 0};
        int[] nums4 = {1, 2, 3};
        int p = 0, q = 3;
        new MergeSortArray().merge(nums3, p, nums4, q);
        System.out.println(Arrays.toString(nums3));

        int[] nums5 = {2, 0};
        int[] nums6 = {1};
        int r = 1, s = 1;
        new MergeSortArray().merge(nums5, r, nums6, s);
        System.out.println(Arrays.toString(nums5));
    }

}

14、合并区间 (Merge Intervals)

题目描述:给出一个区间的集合,请合并所有重叠的区间。

示例 1:输入: [[1,3],[2,6],[8,10],[15,18]]         输出: [[1,6],[8,10],[15,18]]

           解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

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

           解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。

注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。

解题思路

最优解法:首先,我们将区间按照左端点从小到大进行排序,然后从第二个区间开始,依次判断是否与前一个区间有重合部分,如果有,则将两个区间合并成一个新的区间,并继续判断下一个区间是否能够与新区间合并。如果没有重合部分,则将前一个区间加入结果集中,重复以上过程,直到所有区间都被遍历完毕。

这种解法的时间复杂度为 O(nlogn),其中 n 表示区间的个数。因为排序的时间复杂度为 O(nlogn),而遍历所有区间的时间复杂度也为 O(n),所以总的时间复杂度为 O(nlogn)。

具体代码展示

package org.zyf.javabasic.letcode.array;

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

/**
 * @author yanfengzhang
 * @description 题目描述:给出一个区间的集合,请合并所有重叠的区间。
 * 示例 1:输入: [[1,3],[2,6],[8,10],[15,18]]       输出: [[1,6],[8,10],[15,18]]
 * 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
 * 示例 2:输入: [[1,4],[4,5]]       输出: [[1,5]]
 * 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
 * <p>
 * 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。
 * @date 2023/4/2  01:04
 */
public class MergeIntervals {

    /**
     * 首先,我们将区间按照左端点从小到大进行排序,然后从第二个区间开始,依次判断是否与前一个区间有重合部分,
     * 如果有,则将两个区间合并成一个新的区间,并继续判断下一个区间是否能够与新区间合并。
     * 如果没有重合部分,则将前一个区间加入结果集中,重复以上过程,直到所有区间都被遍历完毕。
     * <p>
     * 这种解法的时间复杂度为 O(nlogn),其中 n 表示区间的个数。
     * 因为排序的时间复杂度为 O(nlogn),而遍历所有区间的时间复杂度也为 O(n),所以总的时间复杂度为 O(nlogn)。
     */
    public int[][] merge(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return new int[0][0];
        }

        /*按照每个区间的左端点排序*/
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);

        /*存放最终合并后的区间*/
        List<int[]> merged = new ArrayList<>();

        /*遍历每个区间,判断是否需要合并*/
        for (int i = 0; i < intervals.length; i++) {
            int[] curr = intervals[i];
            if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < curr[0]) {
                /* 如果 merged 为空或者 merged 中最后一个区间的右端点小于 curr 区间的左端点,
                   则表示当前 curr 区间不能和前面的区间合并,直接将 curr 区间加入 merged 中*/
                merged.add(curr);
            } else {
                /*否则,说明需要将 curr 区间合并到前面的区间中*/
                merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], curr[1]);
            }
        }

        return merged.toArray(new int[merged.size()][]);
    }

    public static void main(String[] args) {
        int[][] intervals = new int[][]{{1, 3}, {2, 6}, {8, 10}, {15, 18}};
        int[][] result = new MergeIntervals().merge(intervals);
        for (int[] interval : result) {
            System.out.println(Arrays.toString(interval));
        }
    }
}

15、最大子序和 (Maximum Subarray)

题目描述:经典的动态规划问题,给定一个整数数组nums,找到具有最大和的连续子数组(至少包含一个元素),返回其最大和。

例如,给定数组 nums = [-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大,为6,因此返回6。

提示:

  • 1 <= nums.length <= 3 * 10^4
  • -10^5 <= nums[i] <= 10^5

解题思路

解决这个问题时,我们需要找到一个子数组,其和最大。这个子数组可以是原始数组的一个连续子数组。因此,我们可以使用动态规划来解决此问题。

假设 dp[i] 表示以第 i 个元素结尾的最大子数组和,则状态转移方程为:

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

其中,nums[i] 表示第 i 个元素的值。也就是说,如果前 i-1 个元素的子数组和加上 nums[i] 的值大于 nums[i],那么 dp[i] 的值就为 dp[i-1] 加上 nums[i] 的值,否则 dp[i] 的值就为 nums[i]。

最终的答案为 dp 数组中的最大值。时间复杂度为 O(n),空间复杂度为 O(1)。

另外,还有一种贪心算法,称为 Kadane 算法。该算法从左到右遍历数组,并在每个位置上计算当前的最大子数组和。时间复杂度同样为 O(n),空间复杂度为 O(1)。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 经典的动态规划问题,给定一个整数数组nums,找到具有最大和的连续子数组(至少包含一个元素),返回其最大和。
 * 例如,给定数组 nums = [-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大,为6,因此返回6。
 * <p>
 * 提示:
 * 1 <= nums.length <= 3 * 10^4
 * -10^5 <= nums[i] <= 10^5
 * @date 2023/4/2  18:16
 */
public class MaximumSubarray {

    /**
     * 解决这个问题时,我们需要找到一个子数组,其和最大。这个子数组可以是原始数组的一个连续子数组。因此,我们可以使用动态规划来解决此问题。
     * 假设 dp[i] 表示以第 i 个元素结尾的最大子数组和,则状态转移方程为:
     * dp[i] = max(dp[i-1] + nums[i], nums[i])
     * 其中,nums[i] 表示第 i 个元素的值。也就是说,如果前 i-1 个元素的子数组和加上 nums[i] 的值大于 nums[i],
     * 那么 dp[i] 的值就为 dp[i-1] 加上 nums[i] 的值,否则 dp[i] 的值就为 nums[i]。
     * <p>
     * 最终的答案为 dp 数组中的最大值。时间复杂度为 O(n),空间复杂度为 O(1)。
     * 另外,还有一种贪心算法,称为 Kadane 算法。该
     * 算法从左到右遍历数组,并在每个位置上计算当前的最大子数组和。
     * 时间复杂度同样为 O(n),空间复杂度为 O(1)。
     */
    public int maxSubArray(int[] nums) {
        /*最大子序和的初始值设为整型最小值*/
        int maxSum = Integer.MIN_VALUE;
        /*当前子序和的初始值为0*/
        int curSum = 0;

        /*遍历数组*/
        for (int num : nums) {
            /*将当前数字加入当前子序和*/
            curSum += num;
            /*更新最大子序和*/
            maxSum = Math.max(maxSum, curSum);
            if (curSum < 0) {
                /*如果当前子序和为负数,重置当前子序和*/
                curSum = 0;
            }
        }
        /*返回最大子序和*/
        return maxSum;
    }

    public static void main(String[] args) {
        int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
        int maxSum = new MaximumSubarray().maxSubArray(nums);
        System.out.println(maxSum);
    }
}

备注1:动态规划算法思想解题步骤

动态规划是一种算法思想,它通常用来解决具有最优子结构性质的问题,即可以通过子问题的最优解来得到原问题的最优解。

具体来说,我们可以通过以下步骤来使用动态规划:

  1. 定义状态:找出问题中子问题的状态,并用变量或数组表示它们。比如,最大子序和问题中,我们可以用 dp[i]表示以第i个元素结尾的最大子序和。

  2. 定义状态转移方程:找出问题中子问题之间的转移关系,即原问题的最优解和子问题的最优解之间的关系。比如,最大子序和问题中,dp[i]的值可以由dp[i-1]和nums[i]计算得到,即 dp[i] = max(dp[i-1] + nums[i], nums[i])。

  3. 定义边界:找出最简单的子问题的最优解。比如,最大子序和问题中,dp[0] = nums[0]。

  4. 求解原问题:根据定义的状态、状态转移方程和边界条件,求出原问题的最优解。比如,最大子序和问题中,原问题的最优解就是以上分析。

16、最长连续递增序列 (Longest Continuous Increasing Subsequence)

题目描述:给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入:[10,9,2,5,3,7,101,18] 输出:4 解释:最长的上升子序列是 [2,3,7,101],它的长度是 4。

输入:[0,1,0,3,2,3] 输出:4 解释:最长的上升子序列是 [0, 1, 2, 3],它的长度是 4。

说明:可能会有多种最长上升子序列的组合,只需要输出对应长度即可。

解题思路

最长递增子序列的最优解法是动态规划。时间复杂度为 O(n^2) 或 O(nlogn),空间复杂度为 O(n)。其中 O(nlogn) 的解法称为 patience sorting,不过它的实现比较复杂,这里我们只介绍 O(n^2) 的解法。

具体思路如下:

  1. 定义状态:用 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。

  2. 定义状态转移方程:枚举 nums[j],如果 nums[j] < nums[i],则 dp[i] = max(dp[i], dp[j] + 1)。

  3. 状态初始化:dp[i] 初始值为 1,因为任意一个元素都可以看做长度为 1 的递增子序列。

  4. 状态输出:最终的结果应该是 dp 数组中的最大值。

具体来说,可以定义一个数组 dp,其中 dp[i] 表示以 nums[i] 为结尾的最长递增子序列的长度。然后我们从左到右遍历数组,对于每个位置 i,我们通过比较 nums[i] 和之前的每个数字 nums[j] 来更新 dp[i],具体来说,如果 nums[i] 大于 nums[j],那么 dp[i] 可以更新为 dp[j]+1,表示在以 nums[j] 为结尾的最长递增子序列后面再加上一个 nums[i]。因此,我们可以得到如下的状态转移方程:

  • dp[i] = max(dp[j] + 1) (0 <= j < i, nums[j] < nums[i])

最终,我们遍历一遍整个数组,找到 dp 中的最大值即为所求。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个无序的整数数组,找到其中最长上升子序列的长度。
 * 示例:
 * 输入:[10,9,2,5,3,7,101,18] 输出:4 解释:最长的上升子序列是 [2,3,7,101],它的长度是 4。
 * 输入:[0,1,0,3,2,3] 输出:4 解释:最长的上升子序列是 [0, 1, 2, 3],它的长度是 4。
 * 说明:可能会有多种最长上升子序列的组合,只需要输出对应长度即可。
 * @date 2023/4/2  18:39
 */
public class LongestContinuousIncreasingSubsequence {

    /**
     * 可以定义一个数组 dp,其中 dp[i] 表示以 nums[i] 为结尾的最长递增子序列的长度。
     * 然后我们从左到右遍历数组,对于每个位置 i,我们通过比较 nums[i] 和之前的每个数字 nums[j] 来更新 dp[i],
     * 具体来说,如果 nums[i] 大于 nums[j],那么 dp[i] 可以更新为 dp[j]+1,
     * 表示在以 nums[j] 为结尾的最长递增子序列后面再加上一个 nums[i]。
     * 因此,我们可以得到如下的状态转移方程:
     * dp[i] = max(dp[j] + 1) (0 <= j < i, nums[j] < nums[i])
     * 最终,我们遍历一遍整个数组,找到 dp 中的最大值即为所求。
     */
    public int lengthOfLIS(int[] nums) {
        /*p数组,表示以当前元素为结尾的最长上升子序列的长度*/
        int[] dp = new int[nums.length];
        /*最长上升子序列的长度*/
        int maxLen = 0;

        for (int i = 0; i < nums.length; i++) {
            /*初始化为1,因为每个元素都是一个上升子序列*/
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    /*如果当前元素大于前面的某个元素,可以构成一个更长的上升子序列*/
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            /*更新最长上升子序列的长度*/
            maxLen = Math.max(maxLen, dp[i]);
        }

        return maxLen;
    }

    public static void main(String[] args) {
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        int res = new LongestContinuousIncreasingSubsequence().lengthOfLIS(nums);
        System.out.println(res);
    }


}

17、最长公共前缀 (Longest Common Prefix)题目描述:

题目描述:编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

示例 1:输入: ["flower","flow","flight"] 输出: "fl"

示例 2:输入: ["dog","racecar","car"] 输出: "" 解释: 输入不存在公共前缀。

说明:

所有输入只包含小写字母 a-z 。

解题思路

可以结合分治法的思想来求解最长公共前缀。

分治法的基本思路是将问题分解为更小的子问题,然后将子问题的结果合并以得到原始问题的解。对于最长公共前缀问题,可以将字符串数组划分为两个子数组,分别求解每个子数组的最长公共前缀,然后将两个子问题的解合并,得到原始问题的解。

这种分治法的时间复杂度也是O(m*n),其中m是字符串数组中字符串的平均长度,n是字符串数组的长度。

需要注意的是,对于最长公共前缀问题,没有明确的最优解法,而是根据具体情况和数据特点来选择适合的算法。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 编写一个函数来查找字符串数组中的最长公共前缀。
 * 如果不存在公共前缀,返回空字符串 ""。
 * 示例 1:输入: ["flower","flow","flight"] 输出: "fl"
 * 示例 2:输入: ["dog","racecar","car"] 输出: "" 解释: 输入不存在公共前缀。
 * 说明:
 * 所有输入只包含小写字母 a-z 。
 * @date 2023/4/2  19:02
 */
public class LongestCommonPrefix {

    /**
     * 可以结合分治法的思想来求解最长公共前缀。
     * 分治法的基本思路是将问题分解为更小的子问题,然后将子问题的结果合并以得到原始问题的解。
     * 对于最长公共前缀问题,可以将字符串数组划分为两个子数组,
     * 分别求解每个子数组的最长公共前缀,
     * 然后将两个子问题的解合并,得到原始问题的解。
     * <p>
     * 这种分治法的时间复杂度也是O(m*n),其中m是字符串数组中字符串的平均长度,n是字符串数组的长度。
     * 需要注意的是,对于最长公共前缀问题,没有明确的最优解法,而是根据具体情况和数据特点来选择适合的算法。
     */
    public String longestCommonPrefix(String[] strs) {
        if (strs == null || strs.length == 0) {
            return "";
        }

        return longestCommonPrefix(strs, 0, strs.length - 1);
    }

    private String longestCommonPrefix(String[] strs, int left, int right) {
        if (left == right) {
            return strs[left];
        }

        int mid = (left + right) / 2;
        String leftPrefix = longestCommonPrefix(strs, left, mid);
        String rightPrefix = longestCommonPrefix(strs, mid + 1, right);
        return commonPrefix(leftPrefix, rightPrefix);
    }

    private String commonPrefix(String str1, String str2) {
        int length = Math.min(str1.length(), str2.length());
        int i = 0;
        while (i < length && str1.charAt(i) == str2.charAt(i)) {
            i++;
        }
        return str1.substring(0, i);
    }


    public static void main(String[] args) {
        String[] strs1 = {"flower", "flow", "flight"};
        System.out.println(new LongestCommonPrefix().longestCommonPrefix(strs1));

        String[] strs2 = {"dog", "racecar", "car"};
        System.out.println(new LongestCommonPrefix().longestCommonPrefix(strs2));

        String[] strs3 = {"dofggxcfd", "dofggxcfdracecar", "dofggxcfdcar"};
        System.out.println(new LongestCommonPrefix().longestCommonPrefix(strs3));
    }

}

18、移除元素 (Remove Element)

题目描述:给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

例如: 给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。

说明: 注意这五个细节:

  • 不要考虑数组超出新长度后面的元素;
  • 数组长度可为 0;
  • 从前往后遍历数组;
  • 当遇到元素值等于 val 时,将当前元素与最后一个元素进行交换,并将数组长度减 1;
  • 交换后继续比较当前元素,直到当前元素不等于 val。

需要注意的是,这个问题和「27. 移除元素」的主要区别是:数据范围从给定数组的长度扩展到了 10^4。

解题思路

移除元素 (Remove Element)的最优解法是双指针法。

“双指针”技巧包括使用两个指针,分别从数组的两端开始遍历,直到它们相遇为止。在这种情况下,我们可以将一个指针用于遍历数组,并将另一个指针用于指向新数组中的最后一个元素。

在本问题中,我们需要移除等于给定值的元素。因此,我们可以使用两个指针i和j,其中i是慢指针,而j是快指针。当nums[j]等于给定值时,我们递增j以跳过此元素。当nums[j]不等于给定值时,我们将nums[j]复制到nums[i],然后递增i以继续遍历。

具体步骤如下:

  1. 初始化两个指针i和j,其中i指向数组的开头,而j指向数组的结尾。

  2. 循环遍历数组nums,直到i和j相遇为止。

  3. 如果nums[i]等于给定值,那么我们就将nums[i]覆盖为nums[j],然后递增j。

  4. 如果nums[i]不等于给定值,那么我们就递增i以继续遍历。

  5. 返回新数组的长度,即为i的值。

通过上述步骤,我们可以实现对数组的原地修改,并返回新数组的长度。

值得注意的是,这种方法不会删除等于给定值的元素,而是将它们移到数组的末尾。如果我们需要完全删除所有等于给定值的元素,我们可以在每次将nums[i]覆盖为nums[j]时,将nums[j]复制到nums[i]。这样做会将数组的顺序改变,但可以将所有等于给定值的元素完全删除。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给你一个数组 nums和一个值 val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。
 * <p>
 * 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
 * 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
 * 例如: 给定 nums = [3,2,2,3], val = 3,
 * 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
 * <p>
 * 说明: 注意这五个细节:
 * 不要考虑数组超出新长度后面的元素;
 * 数组长度可为 0;
 * 从前往后遍历数组;
 * 当遇到元素值等于 val 时,将当前元素与最后一个元素进行交换,并将数组长度减 1;
 * 交换后继续比较当前元素,直到当前元素不等于 val。
 * 需要注意的是,这个问题和「27. 移除元素」的主要区别是:数据范围从给定数组的长度扩展到了 10^4。
 * @date 2023/4/2  19:21
 */
public class RemoveElement {

    /**
     * 移除元素 (Remove Element)的最优解法是双指针法。
     * “双指针”技巧包括使用两个指针,分别从数组的两端开始遍历,直到它们相遇为止。
     * 在这种情况下,我们可以将一个指针用于遍历数组,并将另一个指针用于指向新数组中的最后一个元素。
     * <p>
     * 在本问题中,我们需要移除等于给定值的元素。
     * 因此,我们可以使用两个指针i和j,其中i是慢指针,而j是快指针。
     * 当nums[j]等于给定值时,我们递增j以跳过此元素。
     * 当nums[j]不等于给定值时,我们将nums[j]复制到nums[i],然后递增i以继续遍历。
     * <p>
     * 具体步骤如下:
     * 初始化两个指针i和j,其中i指向数组的开头,而j指向数组的结尾。
     * 循环遍历数组nums,直到i和j相遇为止。
     * 如果nums[i]等于给定值,那么我们就将nums[i]覆盖为nums[j],然后递增j。
     * 如果nums[i]不等于给定值,那么我们就递增i以继续遍历。
     * 返回新数组的长度,即为i的值。
     * 通过上述步骤,我们可以实现对数组的原地修改,并返回新数组的长度。
     */
    public static int removeElement(int[] nums, int val) {
        /*慢指针 i 指向当前数组中需要保留的元素的下标*/
        int i = 0;
        /*快指针 j 遍历整个数组*/
        for (int j = 0; j < nums.length; j++) {
            /*如果 nums[j] 不等于 val,说明该元素需要保留*/
            if (nums[j] != val) {
                /*将该元素移到 nums[i] 的位置上*/
                nums[i] = nums[j];
                /*i 向右移动一位*/
                i++;
            }
        }
        /*返回保留的元素的数量,即为新数组的长度*/
        return i;
    }

    public static void main(String[] args) {
        int[] nums = {3, 2, 2, 3};
        int val = 3;
        int len = removeElement(nums, val);
        System.out.println("New length: " + len);
        System.out.print("New array: ");
        for (int i = 0; i < len; i++) {
            System.out.print(nums[i] + " ");
        }
    }

}

19、除自身以外数组的乘积 (Product of Array Except Self)

题目描述:给定长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

例如,输入数组为 [1,2,3,4],输出数组应该为 [24,12,8,6]

要求时间复杂度为 $O(n)$,并且不能使用除法。

提示:本题解法较多,请至少尝试完成两种不同的解法。

解题思路

最优解法是使用前缀积和后缀积的方法,时间复杂度为 O(n),空间复杂度为 O(1)。

具体步骤如下:

  1. 遍历一次数组,计算出每个元素左侧所有元素的乘积,存入结果数组中。
  2. 遍历一次数组,计算出每个元素右侧所有元素的乘积,和步骤 1 结果数组相乘,得到最终结果数组。

这里使用两个变量 left 和 right,分别表示每个元素左侧和右侧所有元素的乘积。初始值都为 1。在遍历时,先更新结果数组中的左侧乘积,然后再从右向左遍历更新右侧乘积,并将结果数组对应位置的值乘以右侧乘积即可。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description
 * @date 2023/4/2  19:32
 */
public class XXArrayExceptSelf {

    /**
     * 最优解法是使用前缀积和后缀积的方法,时间复杂度为 O(n),空间复杂度为 O(1)。
     * <p>
     * 具体步骤如下:
     * 1 遍历一次数组,计算出每个元素左侧所有元素的乘积,存入结果数组中。
     * 2 遍历一次数组,计算出每个元素右侧所有元素的乘积,和步骤 1 结果数组相乘,得到最终结果数组。
     * 这里使用两个变量 left 和 right,分别表示每个元素左侧和右侧所有元素的乘积。
     * 初始值都为 1。在遍历时,先更新结果数组中的左侧乘积,
     * 然后再从右向左遍历更新右侧乘积,并将结果数组对应位置的值乘以右侧乘积即可。
     */
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];

        /*计算前缀积*/
        ans[0] = 1;
        for (int i = 1; i < n; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }

        /*计算后缀积并乘到前缀积中*/
        int suffixProduct = 1;
        for (int i = n - 1; i >= 0; i--) {
            ans[i] *= suffixProduct;
            suffixProduct *= nums[i];
        }

        return ans;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4};
        int[] result = new XXArrayExceptSelf().productExceptSelf(nums);
        System.out.println(Arrays.toString(result));
    }

}

20、颜色分类 (Sort Colors)

题目描述:给定一个包含红、白、蓝且长度为 n 的数组,将数组元素进行分类使得相同颜色的元素相邻,并按照红、白、蓝的顺序进行排序。我们可以使用整数 0,1 和 2 分别代表红,白,蓝。

注意: 不能使用代码库中的排序函数来解决这个问题。

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

进阶:

  1. 一个直观的解决方案是使用计数排序的两趟扫描算法。 首先,迭代计算出 0、1 和 2 元素的个数,然后按照 0、1、2 的排序,重写当前数组。
  2. 你能想出一个仅使用常数空间的一趟扫描算法吗?

解题思路

当我们遇到一个红色(0)时,我们将其交换到数组的左侧,遇到一个蓝色(2)时,我们将其交换到数组的右侧。这样,白色(1)将被自动排序到数组的中间。

我们用三个指针来维护这个过程,分别是 leftrightcurrent,其中 left 指向当前已经排序好的最右侧的红色(0)的下一个位置,right 指向当前已经排序好的最左侧的蓝色(2)的前一个位置,current 从左往右扫描整个数组,如果扫描到的值是 0,则交换到最左侧,如果是 2,则交换到最右侧,否则不做任何操作,leftcurrent 向右移动一位,right 向左移动一位。当 currentright 相遇时,排序完成。

这种方法可以在只扫描一次数组的情况下完成排序,时间复杂度为O(n),空间复杂度为O(1),是一种非常高效的排序方法。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定一个包含红、白、蓝且长度为 n 的数组,
 * 将数组元素进行分类使得相同颜色的元素相邻,并按照红、白、蓝的顺序进行排序。
 * 我们可以使用整数 0,1 和 2 分别代表红,白,蓝。
 * 注意: 不能使用代码库中的排序函数来解决这个问题。
 * 示例: 输入: [2,0,2,1,1,0] 输出: [0,0,1,1,2,2]
 * <p>
 * 进阶:
 * 一个直观的解决方案是使用计数排序的两趟扫描算法。 首先,迭代计算出 0、1 和 2 元素的个数,然后按照 0、1、2 的排序,重写当前数组。
 * 你能想出一个仅使用常数空间的一趟扫描算法吗?
 * @date 2023/4/2  19:50
 */
public class SortColors {

    /**
     * 我们定义三个指针,分别为p0、p2和当前遍历的元素指针i。p0和p2分别指向数组的左右两端,而i则是当前遍历到的元素。
     * <p>
     * 当遍历到的元素为0时,我们交换它和p0位置上的元素,并将p0向右移动一位。
     * 当遍历到的元素为2时,我们交换它和p2位置上的元素,并将p2向左移动一位。
     * 由于当前遍历到的元素是通过交换到当前位置的,我们需要再次检查一下该位置,因此将i减1。
     * 如果遍历到的元素为1,则不需要任何操作,继续向右遍历。
     * 当i指针遍历到p2指针的位置时,遍历完成。经过上述操作后,数组中的元素按照0、1、2的顺序排列。
     */
    public void sortColors(int[] nums) {
        int n = nums.length;
        /*初始化左右指针*/
        int p0 = 0, p2 = n - 1;
        /*遍历数组*/
        for (int i = 0; i <= p2; i++) {
            /*当前元素为0,移动到左侧区域*/
            if (nums[i] == 0) {
                swap(nums, i, p0);
                p0++;
            } else if (nums[i] == 2) {
                /*当前元素为2,移动到右侧区域*/
                swap(nums, i, p2);
                p2--;
                /*由于i位置上的元素是经过交换到当前位置的,需要再次检查一下该位置*/
                i--;
            }
        }
    }

    /**
     * 数组元素交换方法
     */
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    public static void main(String[] args) {
        int[] nums1 = {2, 0, 2, 1, 1, 0};
        new SortColors().sortColors(nums1);
        System.out.println(Arrays.toString(nums1));

        int[] nums2 = {2, 0, 1};
        new SortColors().sortColors(nums2);
        System.out.println(Arrays.toString(nums2));

        int[] nums3 = {0};
        new SortColors().sortColors(nums3);
        System.out.println(Arrays.toString(nums3));
    }

}

21、数组区间段加和

题目描述:给定一个整数数组 nums 和两个整数 start、end,计算数组 nums 在下标范围 [start, end] 内的元素之和。

要求:预处理的时间复杂度为 O(n),其中 n 是数组 nums 的长度。计算区间段的和的时间复杂度为 O(1)。

解题思路

为了尽量保证时间复杂度低,可以使用预处理的方式来快速计算区间段的和。具体步骤如下:

  1. 创建一个辅助数组 prefixSum,用于保存数组 nums 的前缀和。prefixSum[i] 表示从数组开始位置到下标 i 的元素之和。
  2. 预处理数组 prefixSum,计算每个位置的前缀和。遍历数组 nums,并累加每个元素到当前位置的前缀和。
  3. 计算区间段的和。根据前缀和数组 prefixSum,区间 [start, end] 的元素之和可以通过 prefixSum[end] - prefixSum[start-1] 来计算。需要注意的是,如果 start 为 0,则前缀和数组中的元素 prefixSum[start-1] 不存在,此时可以忽略该项。

通过上述预处理的方式,可以在常数时间复杂度内计算区间段的和。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个整数数组 nums 和两个整数 start、end,
 * 计算数组 nums 在下标范围 [start, end] 内的元素之和。
 * @date 2023/6/12  23:23
 */
public class IntervalSum {
    private int[] prefixSum;

    public IntervalSum(int[] nums) {
        // 预处理数组的前缀和
        prefixSum = new int[nums.length];
        prefixSum[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            prefixSum[i] = prefixSum[i - 1] + nums[i];
        }
    }

    /**
     * 为了尽量保证时间复杂度低,可以使用预处理的方式来快速计算区间段的和。具体步骤如下:
     *
     * 	1.	创建一个辅助数组 prefixSum,用于保存数组 nums 的前缀和。prefixSum[i] 表示从数组开始位置到下标 i 的元素之和。
     * 	2.	预处理数组 prefixSum,计算每个位置的前缀和。遍历数组 nums,并累加每个元素到当前位置的前缀和。
     * 	3.	计算区间段的和。根据前缀和数组 prefixSum,区间 [start, end] 的元素之和可以通过 prefixSum[end] - prefixSum[start-1] 来计算。
     * 	需要注意的是,如果 start 为 0,则前缀和数组中的元素 prefixSum[start-1] 不存在,此时可以忽略该项。
     *
     * 通过上述预处理的方式,可以在常数时间复杂度内计算区间段的和。
     * 预处理的时间复杂度为 O(n),其中 n 是数组 nums 的长度。计算区间段的和的时间复杂度为 O(1)。
     */
    public int getSum(int start, int end) {
        // 计算区间段的和
        if (start == 0) {
            return prefixSum[end];
        } else {
            return prefixSum[end] - prefixSum[start - 1];
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6};
        IntervalSum intervalSum = new IntervalSum(nums);
        int sum = intervalSum.getSum(1, 4);
        // 输出 Sum: 14
        System.out.println("Sum: " + sum);
    }
}

22、二维矩阵的第K大数

题目描述:给定一个二维矩阵 matrix,其中每一行和每一列都按非递减顺序排列,同时给定一个整数 k,求矩阵中第 k 大的数。

解题思路

一种常见的解法是使用最小堆(Min Heap)来解决该问题。具体步骤如下:

  1. 创建一个大小为 k 的最小堆 minHeap。
  2. 遍历二维矩阵 matrix 的每个元素,将元素加入最小堆 minHeap 中。
  3. 如果最小堆 minHeap 的大小超过了 k,则将堆顶元素(即当前最小的元素)移除,保持堆的大小为 k。
  4. 遍历完所有元素后,最小堆 minHeap 中的堆顶元素即为第 k 大的数。

通过上述步骤,我们可以找到二维矩阵中第 k 大的数。该算法的时间复杂度为 O(mnlog(k)),其中 m 和 n 分别是矩阵的行数和列数。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.PriorityQueue;

/**
 * @author yanfengzhang
 * @description 给定一个二维矩阵 matrix,
 * 其中每一行和每一列都按非递减顺序排列,
 * 同时给定一个整数 k,求矩阵中第 k 大的数。
 * @date 2023/6/14  23:43
 */
public class KthLargestElement {
    /**
     * 一种常见的解法是使用最小堆(Min Heap)来解决该问题。具体步骤如下:
     *
     * 	1.	创建一个大小为 k 的最小堆 minHeap。
     * 	2.	遍历二维矩阵 matrix 的每个元素,将元素加入最小堆 minHeap 中。
     * 	3.	如果最小堆 minHeap 的大小超过了 k,则将堆顶元素(即当前最小的元素)移除,保持堆的大小为 k。
     * 	4.	遍历完所有元素后,最小堆 minHeap 中的堆顶元素即为第 k 大的数。
     *
     * 通过上述步骤,我们可以找到二维矩阵中第 k 大的数。
     * 该算法的时间复杂度为 O(mnlog(k)),其中 m 和 n 分别是矩阵的行数和列数。
     * @param matrix
     * @param k
     * @return
     */
    public int findKthLargest(int[][] matrix, int k) {
        int m = matrix.length;
        int n = matrix[0].length;

        // 创建最小堆
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();

        // 遍历二维矩阵
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                minHeap.offer(matrix[i][j]);

                // 如果堆的大小超过了 k,移除堆顶元素
                if (minHeap.size() > k) {
                    minHeap.poll();
                }
            }
        }

        // 返回堆顶元素
        return minHeap.peek();
    }

    public static void main(String[] args) {
        int[][] matrix = {
                {1, 3, 5},
                {2, 4, 7},
                {6, 8, 9}
        };
        int k = 5;

        KthLargestElement kthLargest = new KthLargestElement();
        int kthLargestElement = kthLargest.findKthLargest(matrix, k);
        // 输出 Kth Largest Element: 5
        System.out.println("Kth Largest Element: " + kthLargestElement);
    }
}

23、升序数组的两数之和

题目描述:给定一个升序数组 arr 和一个整数 sum,判断该数组中是否存在两个数,它们的和等于 sum。

解题思路

由于数组是升序排列的,我们可以使用双指针的方法来解决该问题。具体步骤如下:

1. 初始化两个指针 left 和 right,分别指向数组的起始位置和结束位置。

2. 循环遍历数组,直到 left 大于等于 right:

• 计算当前指针所指元素的和 currSum = arr[left] + arr[right]。

• 如果 currSum 等于 sum,则说明找到了满足条件的两个数,返回 true。

• 如果 currSum 小于 sum,说明当前和太小,我们需要增大和,将 left 指针右移一位。

• 如果 currSum 大于 sum,说明当前和太大,我们需要减小和,将 right 指针左移一位。

3. 如果循环结束后仍未找到满足条件的两个数,则说明数组中不存在这样的两个数,返回 false。

通过上述步骤,我们可以判断数组中是否存在两个数的和等于给定的 sum。该算法的时间复杂度为 O(n),其中 n 是数组的长度。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个升序数组 arr 和一个整数 sum,
 * 判断该数组中是否存在两个数,它们的和等于 sum。
 * @date 2023/6/14  23:58
 */
public class FindTwoSum {
    /**
     * 由于数组是升序排列的,我们可以使用双指针的方法来解决该问题。具体步骤如下:
     *
     * 	1.	初始化两个指针 left 和 right,分别指向数组的起始位置和结束位置。
     * 	2.	循环遍历数组,直到 left 大于等于 right:
     * 	•	计算当前指针所指元素的和 currSum = arr[left] + arr[right]。
     * 	•	如果 currSum 等于 sum,则说明找到了满足条件的两个数,返回 true。
     * 	•	如果 currSum 小于 sum,说明当前和太小,我们需要增大和,将 left 指针右移一位。
     * 	•	如果 currSum 大于 sum,说明当前和太大,我们需要减小和,将 right 指针左移一位。
     * 	3.	如果循环结束后仍未找到满足条件的两个数,则说明数组中不存在这样的两个数,返回 false。
     *
     * 通过上述步骤,我们可以判断数组中是否存在两个数的和等于给定的 sum。该算法的时间复杂度为 O(n),其中 n 是数组的长度。
     * @param arr
     * @param sum
     * @return
     */
    public boolean findTwoSum(int[] arr, int sum) {
        int left = 0;
        int right = arr.length - 1;

        while (left < right) {
            int currSum = arr[left] + arr[right];

            if (currSum == sum) {
                return true;
            } else if (currSum < sum) {
                left++;
            } else {
                right--;
            }
        }

        return false;
    }

    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 7, 9};
        int sum = 10;

        FindTwoSum solution = new FindTwoSum();
        boolean hasTwoSum = solution.findTwoSum(arr, sum);
        // 输出 Has Two Sum: true
        System.out.println("Has Two Sum: " + hasTwoSum);
    }
}

24、二维数组找到某个数

题目描述:给定一个行递增、列递增的二维数组(矩阵),判断是否可以找到某个数。

解题思路

由于二维数组的行和列都是递增的,我们可以利用这个特性进行查找。可以从矩阵的右上角开始,逐行递减或逐列递增地查找,直到找到目标数或者越界。

具体步骤如下:

  1. 初始化指针,将指针定位到矩阵的右上角(或者左下角)。
  2. 如果指针所指向的元素等于目标数,返回 true。
  3. 如果指针所指向的元素大于目标数,则说明目标数在当前元素的左边,将指针左移一列。
  4. 如果指针所指向的元素小于目标数,则说明目标数在当前元素的下方,将指针下移一行。
  5. 重复步骤 2~4,直到找到目标数或者越界。
  6. 如果遍历完整个矩阵仍未找到目标数,返回 false。

通过按行递增和按列递增的特性,每次移动指针都可以排除一行或一列的元素,从而快速缩小搜索范围,使得时间复杂度较低。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个行递增、列递增的二维数组(矩阵),判断是否可以找到某个数。
 * @date 2023/7/11  23:28
 */
public class FindNumberInMatrix {
    /**
     * 判断在行递增、列递增的二维数组中是否可以找到某个数
     *
     * @param matrix 二维数组
     * @param target 目标数
     * @return 是否找到目标数
     */
    public static boolean findNumber(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }

        int rows = matrix.length;
        int cols = matrix[0].length;
        // 从第一行开始
        int row = 0;
        // 从最后一列开始
        int col = cols - 1;

        while (row < rows && col >= 0) {
            if (matrix[row][col] == target) {
                // 找到目标数,返回true
                return true;
            } else if (matrix[row][col] > target) {
                // 当前元素大于目标数,向左移动一列
                col--;
            } else {
                // 当前元素小于目标数,向下移动一行
                row++;
            }
        }

        // 遍历完整个矩阵仍未找到目标数,返回false
        return false;
    }

    public static void main(String[] args) {
        int[][] matrix = {
                {1, 2, 3},
                {4, 5, 6},
                {7, 8, 9}
        };

        int target = 5;

        boolean found = findNumber(matrix, target);
        System.out.println("Number " + target + " found: " + found);
    }

}

25、给定数组中寻找和大于等于给定值的最短子数组长度

题目描述:在给定数组中寻找和大于等于给定值的最短子数组的长度

解题思路

这个问题可以通过使用双指针的方法来解决。我们可以使用两个指针,分别表示子数组的起始位置和结束位置。

  • 首先,将起始指针和结束指针都初始化为数组的第一个元素的索引,然后将它们都向右移动,直到子数组的和大于等于目标值,或者结束指针达到数组的末尾。
  • 一旦子数组的和大于等于目标值,我们记录当前子数组的长度,并尝试将起始指针向右移动,以看是否可以找到更短的子数组,同时更新最短子数组的长度。
  • 然后,我们继续将结束指针向右移动,再次检查子数组的和是否大于等于目标值。如果是,我们再次尝试将起始指针向右移动,并更新最短子数组的长度。
  • 我们重复以上步骤,直到结束指针达到数组的末尾。
  • 最后,我们得到的最短子数组的长度就是我们要找的结果。

这种方法的时间复杂度是 O(n),其中 n 是数组的长度。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 在给定数组中寻找和大于等于给定值的最短子数组的长度
 * @date 2023/7/14  23:42
 */
public class ShortestSubarray {

    /**
     * 这个问题可以通过使用双指针的方法来解决。我们可以使用两个指针,分别表示子数组的起始位置和结束位置。
     * 首先,将起始指针和结束指针都初始化为数组的第一个元素的索引,然后将它们都向右移动,直到子数组的和大于等于目标值,或者结束指针达到数组的末尾。
     * 一旦子数组的和大于等于目标值,我们记录当前子数组的长度,并尝试将起始指针向右移动,以看是否可以找到更短的子数组,同时更新最短子数组的长度。
     * 然后,我们继续将结束指针向右移动,再次检查子数组的和是否大于等于目标值。如果是,我们再次尝试将起始指针向右移动,并更新最短子数组的长度。
     * 我们重复以上步骤,直到结束指针达到数组的末尾。
     * 最后,我们得到的最短子数组的长度就是我们要找的结果。
     * 这种方法的时间复杂度是 O(n),其中 n 是数组的长度。
     */
    public static int findShortestSubarray(int[] nums, int target) {
        // 子数组的起始位置
        int left = 0;
        // 最短子数组的长度
        int minLen = Integer.MAX_VALUE;
        // 子数组的和
        int sum = 0;

        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];

            while (sum >= target) {
                minLen = Math.min(minLen, right - left + 1);
                sum -= nums[left];
                left++;
            }
        }

        // 如果没有找到满足条件的子数组,则返回0
        return minLen != Integer.MAX_VALUE ? minLen : 0;
    }

    public static void main(String[] args) {
        int[] nums = {2, 3, 1, 2, 4, 3};
        int target = 7;

        int result = findShortestSubarray(nums, target);
        System.out.println("最短子数组的长度是:" + result);
    }

}

26、数组的全排列

题目描述:给定一个数组,要求返回这个数组的所有可能的全排列。

全排列是一种将数组中的元素重新排列的方式,使得每个元素在排列结果中都出现一次,并且顺序不同即可。换句话说,对于长度为 n 的数组,全排列共有 n! 种可能。

例如,对于数组 [1, 2, 3],它的全排列包括 [1, 2, 3]、[1, 3, 2]、[2, 1, 3]、[2, 3, 1]、[3, 1, 2] 和 [3, 2, 1]

解题思路

该问题的最优解题思路是使用回溯算法来生成全排列。

回溯算法是一种通过尝试不同的选择来解决问题的算法。在生成全排列的过程中,我们可以通过交换数组中的元素来产生不同的排列。

具体步骤如下:

  1. 定义一个递归函数 backtrack,该函数接收当前要生成排列的位置 index、原始数组 nums、当前已生成的排列 tempList,以及最终的结果列表 result。
  2. 如果当前位置 index 等于数组的长度,表示已经生成了一个全排列,将 tempList 添加到 result 中。
  3. 否则,从当前位置 index 开始,遍历数组 nums。将当前位置的元素与 index 位置的元素交换,相当于固定了一个元素,然后递归调用 backtrack,继续生成下一个位置的排列。
  4. 递归调用结束后,要恢复交换前的状态,以便进行下一轮的遍历。将当前位置的元素与 index 位置的元素交换回来。
  5. 回溯过程结束后,返回最终的结果列表 result。

这种方法的时间复杂度是 O(n!),其中 n 是数组的长度。因为全排列的数量是 n!,而生成每个全排列的时间复杂度是 O(n)。因此,整体的时间复杂度是 O(n!)。

具体代码展示

package org.zyf.javabasic.letcode.array;

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

/**
 * @author yanfengzhang
 * @description 给定一个数组,要求返回这个数组的所有可能的全排列。
 * 全排列是一种将数组中的元素重新排列的方式,使得每个元素在排列结果中都出现一次,并且顺序不同即可。
 * 换句话说,对于长度为 n 的数组,全排列共有 n! 种可能。
 * 例如,对于数组 [1, 2, 3],它的全排列包括 [1, 2, 3]、[1, 3, 2]、[2, 1, 3]、[2, 3, 1]、[3, 1, 2] 和 [3, 2, 1]。
 * 请问如何解决这个问题呢?
 * @date 2023/7/14  23:43
 */
public class Permutations {

    public static List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, 0, result);
        return result;
    }

    /**
     * 该问题的最优解题思路是使用回溯算法来生成全排列。
     * 回溯算法是一种通过尝试不同的选择来解决问题的算法。在生成全排列的过程中,我们可以通过交换数组中的元素来产生不同的排列。
     * 具体步骤如下:
     * 1. 定义一个递归函数 backtrack,该函数接收当前要生成排列的位置 index、原始数组 nums、当前已生成的排列 tempList,以及最终的结果列表 result。
     * 2. 如果当前位置 index 等于数组的长度,表示已经生成了一个全排列,将 tempList 添加到 result 中。
     * 3. 否则,从当前位置 index 开始,遍历数组 nums。将当前位置的元素与 index 位置的元素交换,相当于固定了一个元素,然后递归调用 backtrack,继续生成下一个位置的排列。
     * 4. 递归调用结束后,要恢复交换前的状态,以便进行下一轮的遍历。将当前位置的元素与 index 位置的元素交换回来。
     * 5. 回溯过程结束后,返回最终的结果列表 result。
     * 这种方法的时间复杂度是 O(n!),其中 n 是数组的长度。因为全排列的数量是 n!,而生成每个全排列的时间复杂度是 O(n)。因此,整体的时间复杂度是 O(n!)。
     */
    private static void backtrack(int[] nums, int index, List<List<Integer>> result) {
        // 如果当前位置 index 等于数组的长度,表示已经生成了一个全排列,将 tempList 添加到 result 中
        if (index == nums.length) {
            List<Integer> tempList = new ArrayList<>();
            for (int num : nums) {
                tempList.add(num);
            }
            result.add(tempList);
        } else {
            for (int i = index; i < nums.length; i++) {
                // 交换当前位置的元素和 index 位置的元素,固定一个元素
                swap(nums, i, index);
                // 递归调用,生成下一个位置的排列
                backtrack(nums, index + 1, result);
                // 恢复交换前的状态,以便进行下一轮的遍历
                swap(nums, i, index);
            }
        }
    }

    private static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 6};
        List<List<Integer>> permutations = permute(nums);
        for (List<Integer> permutation : permutations) {
            System.out.println(permutation);
        }
    }

}

27、多数元素(Majority Element)

题目描述:给定一个大小为 n 的数组,找到其中的多数元素。多数元素指在数组中出现次数大于 ⌊n/2⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

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

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

解题思路

摩尔投票法:该算法基于一个事实,即在一个数组中,若某个元素出现次数大于数组长度的一半,则该元素必定存在。

算法步骤如下:

  1. 设置一个候选众数 candidate 和一个计数器 count,初始值分别为任意值和 0。
  2. 遍历数组 nums 中的每个元素,如果计数器 count 为 0,则将当前元素设置为候选众数 candidate,并将计数器 count 设置为 1;否则,如果当前元素等于候选众数 candidate,则将计数器 count 加 1,否则将计数器 count 减 1。
  3. 遍历完数组后,candidate 即为众数。

算法分析:

  • 时间复杂度:O(n),其中 n 为数组的长度。遍历一遍数组即可找到众数。
  • 空间复杂度:O(1),使用常数空间。

具体代码展示

package org.zyf.javabasic.letcode.hash;

/**
 * @author yanfengzhang
 * @description 给定一个大小为 n 的数组,找到其中的多数元素。
 * 多数元素指在数组中出现次数大于 ⌊n/2⌋ 的元素。
 * @date 2023/4/9  19:21
 */
public class MajorityElement {

    /**
     * 摩尔投票法:该算法基于一个事实,即在一个数组中,若某个元素出现次数大于数组长度的一半,则该元素必定存在。
     * <p>
     * 算法步骤如下:
     * 1 设置一个候选众数 candidate 和一个计数器 count,初始值分别为任意值和 0。
     * 2 遍历数组 nums 中的每个元素,如果计数器 count 为 0,则将当前元素设置为候选众数 candidate,并将计数器 count 设置为 1;
     * 否则,如果当前元素等于候选众数 candidate,则将计数器 count 加 1,否则将计数器 count 减 1。
     * 3 遍历完数组后,candidate 即为众数。
     */
    public static int findMajorityElement(int[] nums) {
        // 候选元素
        int candidate = 0;
        // 候选元素的出现次数
        int count = 0; 

        for (int num : nums) {
            if (count == 0) {
                // 如果当前候选元素的出现次数为0,则将当前元素设为候选元素,并将出现次数设为1
                candidate = num;
                count = 1;
            } else if (num == candidate) {
                // 如果当前元素与候选元素相同,则候选元素的出现次数加1
                count++;
            } else {
                // 如果当前元素与候选元素不同,则候选元素的出现次数减1
                count--;
            }
        }

        // 最终候选元素可能是多数元素,需要再次验证
        int countCandidate = 0;
        for (int num : nums) {
            if (num == candidate) {
                countCandidate++;
            }
        }

        // -1 表示没有多数元素
        return countCandidate > nums.length / 2 ? candidate : -1;
    }


    public static void main(String[] args) {
        int[] nums = {3, 3, 3, 2, 5};
        int result = new MajorityElement().findMajorityElement(nums);
        /*输出 3*/
        System.out.println(result);
    }

}

28、从1开始顺时针螺旋填充矩阵

题目描述:给定一个正整数n,要求构造一个n x n的矩阵,并按照从1开始顺时针螺旋填充矩阵。

例如,当n=3时,构造的螺旋矩阵如下:

1 2 3

8 9 4

7 6 5

解题思路

为了解决这个问题,可以使用模拟方法。从矩阵的左上角开始,按照顺时针方向依次填充数字,同时注意矩阵边界的处理。通常可以使用四个变量来表示矩阵的上下左右边界,然后按照螺旋的顺序不断填充数字。

螺旋填充矩阵问题可以使用模拟的方法来解决。最优解法的时间复杂度为O(n^2),其中n是矩阵的边长。

具体的最优解法步骤如下:

  1. 初始化一个n x n的矩阵,并用一个变量num来表示当前要填充的数字,初始值为1。
  2. 设置四个变量top、bottom、left和right来表示当前填充的边界范围。初始时,top=0,bottom=n-1,left=0,right=n-1。
  3. 进行循环,从左到右、从上到下、从右到左、从下到上四个方向依次填充数字,直到所有的位置都填充完毕。在每个方向上,每填充一个数字,num增加1。
  4. 每填充完一个方向后,需要更新边界范围:对于从左到右和从右到左的方向,更新top和bottom;对于从上到下和从下到上的方向,更新left和right。
  5. 继续下一个方向的填充,直到所有位置都填充完毕。

最优解法的关键点在于通过四个边界变量来控制填充的方向和范围,这样可以在一个循环内完成所有位置的填充。因为每个位置只被填充一次,所以时间复杂度为O(n^2)。

具体代码展示

package org.zyf.javabasic.letcode.array;

/**
 * @author yanfengzhang
 * @description 给定一个正整数n,要求构造一个n x n的矩阵,并按照从1开始顺时针螺旋填充矩阵。
 * 例如,当n=3时,构造的螺旋矩阵如下:
 * 1 2 3
 * 8 9 4
 * 7 6 5
 * @date 2023/7/18  23:12
 */
public class SpiralMatrix {

    /**
     * 为了解决这个问题,可以使用模拟方法。
     * 从矩阵的左上角开始,按照顺时针方向依次填充数字,同时注意矩阵边界的处理。
     * 通常可以使用四个变量来表示矩阵的上下左右边界,然后按照螺旋的顺序不断填充数字。
     * <p>
     * 螺旋填充矩阵问题可以使用模拟的方法来解决。最优解法的时间复杂度为O(n^2),其中n是矩阵的边长。
     * 具体的最优解法步骤如下:
     * 1.	初始化一个n x n的矩阵,并用一个变量num来表示当前要填充的数字,初始值为1。
     * 2.	设置四个变量top、bottom、left和right来表示当前填充的边界范围。初始时,top=0,bottom=n-1,left=0,right=n-1。
     * 3.	进行循环,从左到右、从上到下、从右到左、从下到上四个方向依次填充数字,直到所有的位置都填充完毕。在每个方向上,每填充一个数字,num增加1。
     * 4.	每填充完一个方向后,需要更新边界范围:对于从左到右和从右到左的方向,更新top和bottom;对于从上到下和从下到上的方向,更新left和right。
     * 5.	继续下一个方向的填充,直到所有位置都填充完毕。
     * 最优解法的关键点在于通过四个边界变量来控制填充的方向和范围,这样可以在一个循环内完成所有位置的填充。
     * 因为每个位置只被填充一次,所以时间复杂度为O(n^2)。
     */
    public static int[][] generateMatrix(int n) {
        // 创建一个 n x n 的矩阵
        int[][] matrix = new int[n][n];
        // 用于填充的数字,初始值为 1
        int num = 1;
        // 初始化边界
        int top = 0, bottom = n - 1, left = 0, right = n - 1;

        while (num <= n * n) {
            // 从左到右
            for (int i = left; i <= right; i++) {
                matrix[top][i] = num++;
            }
            // 更新上边界
            top++;

            // 从上到下
            for (int i = top; i <= bottom; i++) {
                matrix[i][right] = num++;
            }
            // 更新右边界
            right--;

            // 从右到左
            for (int i = right; i >= left; i--) {
                matrix[bottom][i] = num++;
            }
            // 更新下边界
            bottom--;

            // 从下到上
            for (int i = bottom; i >= top; i--) {
                matrix[i][left] = num++;
            }
            // 更新左边界
            left++;
        }

        return matrix;
    }

    public static void main(String[] args) {
        // 验证的矩阵边长为 4
        int n = 4;
        int[][] result = generateMatrix(n);

        // 打印生成的螺旋填充矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.print(result[i][j] + " ");
            }
            System.out.println();
        }
    }
}

29、26个字母按照顺时针螺旋填充矩阵

题目描述:要求将26个字母按照顺时针螺旋填充到一个5x5的矩阵中。请见下方的矩阵:

A  B  C  D  E

V  W  X  Y  F

U  T  S  R  G

T  R  Q  P  H

S  Q  P  O  N

其中,字母按照字母表的顺序从A开始,依次填充到矩阵中,直到Z。

解题思路

对于将26个字母按照顺时针螺旋填充到一个5x5的矩阵中,最优解法是按照以下步骤进行:

  1. 初始化一个5x5的空矩阵,并设定一个指针,初始位置在矩阵的左上角(0,0)。
  2. 遍历字母表中的每个字母,从A到Z。
  3. 将当前字母填充到指针所指向的位置,并将指针向前移动一步。
  4. 检查指针移动后的位置是否已经填充过字母或者是否越界。如果是,则按照顺时针方向旋转指针,并重新移动。
  5. 重复步骤3和步骤4,直到所有字母都被填充到矩阵中。

这样,按照最优解法,可以得到一个符合题目要求的5x5矩阵,其中字母按照字母表的顺序从A开始,依次填充到Z。由于5x5的矩阵有限,只有25个位置,所以最后一个字母Z会填充在矩阵的最后一个可用位置。

需要注意的是,最优解法需要仔细处理边界条件和指针的移动,确保每个字母都被正确地填充到矩阵中,而且不会出现死循环或者遗漏字母的情况。

具体代码展示

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 要求将26个字母按照顺时针螺旋填充到一个5x5的矩阵中。请见下方的矩阵:
 * A  B  C  D  E
 * V  W  X  Y  F
 * U  T  S  R  G
 * T  R  Q  P  H
 * S  Q  P  O  N
 * 其中,字母按照字母表的顺序从A开始,依次填充到矩阵中,直到Z。
 * @date 2023/7/18  23:38
 */
public class SpiralFillAlphabet {

    /**
     * 对于将26个字母按照顺时针螺旋填充到一个5x5的矩阵中,最优解法是按照以下步骤进行:
     * 	1.	初始化一个5x5的空矩阵,并设定一个指针,初始位置在矩阵的左上角(0,0)。
     * 	2.	遍历字母表中的每个字母,从A到Z。
     * 	3.	将当前字母填充到指针所指向的位置,并将指针向前移动一步。
     * 	4.	检查指针移动后的位置是否已经填充过字母或者是否越界。如果是,则按照顺时针方向旋转指针,并重新移动。
     * 	5.	重复步骤3和步骤4,直到所有字母都被填充到矩阵中。
     * 这样,按照最优解法,可以得到一个符合题目要求的5x5矩阵,其中字母按照字母表的顺序从A开始,依次填充到Z。
     * 由于5x5的矩阵有限,只有25个位置,所以最后一个字母Z会填充在矩阵的最后一个可用位置。
     * 需要注意的是,最优解法需要仔细处理边界条件和指针的移动,确保每个字母都被正确地填充到矩阵中,而且不会出现死循环或者遗漏字母的情况。
     */
    public static char[][] fillSpiralMatrix() {
        char[][] matrix = new char[5][5];
        char currentLetter = 'A';
        int row = 0;
        int col = 0;
        int rowDirection = 0;
        int colDirection = 1;

        for (int i = 0; i < 26; i++) {
            // 步骤1:在当前位置填充当前字母到矩阵中。
            matrix[row][col] = currentLetter;
            // 步骤2:移动到下一个字母。
            currentLetter++;

            // 步骤3:检查下一个位置是否有效(未填充且在边界内)。
            int nextRow = row + rowDirection;
            int nextCol = col + colDirection;

            if (nextRow < 0 || nextRow >= 5 || nextCol < 0 || nextCol >= 5 || matrix[nextRow][nextCol] != 0) {
                // 如果下一个位置无效,则顺时针改变方向。
                if (rowDirection == 0 && colDirection == 1) {
                    rowDirection = 1;
                    colDirection = 0;
                } else if (rowDirection == 1 && colDirection == 0) {
                    rowDirection = 0;
                    colDirection = -1;
                } else if (rowDirection == 0 && colDirection == -1) {
                    rowDirection = -1;
                    colDirection = 0;
                } else if (rowDirection == -1 && colDirection == 0) {
                    rowDirection = 0;
                    colDirection = 1;
                }
            }

            // 步骤4:移动到下一个有效位置。
            row += rowDirection;
            col += colDirection;
        }

        return matrix;
    }

    public static void main(String[] args) {
        char[][] matrix = fillSpiralMatrix();
        printMatrix(matrix);
    }

    public static void printMatrix(char[][] matrix) {
        for (int i = 0; i < matrix.length; i++) {
            System.out.println(Arrays.toString(matrix[i]));
        }
    }

}

30、找到数组 A 元素组成的小于 n 的最大整数

题目描述:给定一个正整数 n 和一个由正整数组成的数组 A,您需要找到由数组 A 中的元素(可以重复使用数组 A 中的元素)组成的最大整数,该整数小于 n。要求编写一个函数来实现这个逻辑。

例如,如果 n 为 23121,数组 A 为 [2, 4, 9],您需要找到一个由数组 A 中的元素组成的最大整数,该整数小于 23121。在这种情况下,可能的结果是 22999。

解题思路

题目要求从给定的正整数数组中组合出一个小于等于n的最大整数。因为数组中的元素可以重复使用,所以可以使用回溯算法来解决。

具体思路如下:

  1. 对数组进行排序,方便后续的遍历。

  2. 使用深度优先遍历,从数组的第一个元素开始,依次选取每个元素,将其添加到遍历路径中。

  3. 当遍历路径的长度等于目标值n的长度时,判断当前路径是否小于等于n,如果是则更新结果集。

  4. 如果当前路径已经大于n,则回溯到上一个节点,继续遍历。

  5. 回溯时,需要将最后添加的元素从遍历路径中删除。

  6. 如果回溯结束后仍然没有找到小于等于n的最大整数,则取数组中的最大数,组成一个长度等于len(n) - 1的值返回。

时间复杂度:O(n!),因为需要遍历所有可能的组合。

空间复杂度:O(n),因为需要使用一个StringBuilder来记录遍历路径。

具体代码展示验证

package org.zyf.javabasic.letcode.array;

import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;

/**
 * @author yanfengzhang
 * @description 给定一个正整数 n 和一个由正整数组成的数组 A,
 * 您需要找到由数组 A 中的元素(可以重复使用数组 A 中的元素)组成的最大整数,该整数小于 n。
 * 要求编写一个函数来实现这个逻辑。
 * 例如,如果 n 为 23121,数组 A 为 [2, 4, 9],您需要找到一个由数组 A 中的元素组成的最大整数,该整数小于 23121。
 * 在这种情况下,可能的结果是 22999。
 * @date 2023/8/7  23:00
 */
public class MaxNumberWithArray {

    private int res = 0;

    /**
     * 从数组nums中组合出来一个小于等于n的最大数字并返回.
     * 例如:n = 23131, nums=[2,4,9]
     * 应该返回:22999
     *
     * @param n    目标值
     * @param nums 数组
     * @return 组合出来的小于等于n的最大数字
     */
    private int getCombineMax(int n, int[] nums) {
        // 元素无重复可复选,回溯
        // dfs路径
        StringBuilder path = new StringBuilder();
        // 对数组进行排序,方便后续的遍历
        Arrays.sort(nums);
        // 调用回溯函数
        backtrace(n, nums, 0, path);

        // 回溯没有找到大于等于n的数,取数组中的最大数,组成一个长度等于len(n) - 1的值返回
        if (res == 0) {
            StringBuilder sb = new StringBuilder();
            int count = String.valueOf(n).length();
            while (count > 1) {
                // 取数组中的最大数
                sb.append(nums[nums.length - 1]);
                count--;
            }
            // 将组成的数字转换为整数
            res = Integer.parseInt(sb.toString());
        }
        System.out.printf("当你为某个数n=%d,对应数组是%s,此时输出的最大值为%d\n",
                n, Arrays.toString(nums), res);
        return res;
    }

    /**
     * 深度优先遍历.
     *
     * @param n     目标值
     * @param nums  数组
     * @param start 开始下标
     * @param path  遍历路径
     */
    private void backtrace(int n, int[] nums, int start, StringBuilder path) {
        // 当路径长度和目标值一样长时,是一个结果集,并判断当年结果集是否为小于等于n的最大值,是更新结果集,不是继续
        if (path.length() == String.valueOf(n).length()) {
            // 判断当前路径是否小于等于n
            if (Integer.parseInt(path.toString()) <= n) {
                // 更新结果集
                res = Math.max(res, Integer.parseInt(path.toString()));
            }
            return;
        }

        if (StringUtils.isNotEmpty(path.toString()) && Integer.parseInt(path.toString()) > n) {
            return;
        }

        for (int i = start; i < nums.length; i++) {
            // 选中当前元素,将当前元素添加到路径中
            path.append(nums[i]);
            // 递递归调用回溯函数
            backtrace(n, nums, i, path);
            // 回溯,删除最后添加的字符
            path.deleteCharAt(path.length() - 1);
        }
    }

    public static void main(String[] args) {
        int n1 = 23131;
        int[] A1 = {2, 4, 9};
        new MaxNumberWithArray().getCombineMax(n1, A1);

        int n2 = 12121;
        int[] A2 = {2, 4, 9};
        new MaxNumberWithArray().getCombineMax(n2, A2);

        int n3 = 93456;
        int[] A3 = {0, 1, 2, 3, 4, 5};
        new MaxNumberWithArray().getCombineMax(n3, A3);

        int n4 = 12345;
        int[] A4 = {0, 1, 2, 3, 4};
        new MaxNumberWithArray().getCombineMax(n4, A4);

        int n5 = 2345646;
        int[] A5 = {0, 1, 2, 3};
        new MaxNumberWithArray().getCombineMax(n5, A5);

        int n6 = 1122334;
        int[] A6 = {0, 1, 2};
        new MaxNumberWithArray().getCombineMax(n6, A6);

        int n7 = 12345;
        int[] A7 = {0, 1};
        new MaxNumberWithArray().getCombineMax(n7, A7);

        int n8 = 12345;
        int[] A8 = {0};
        new MaxNumberWithArray().getCombineMax(n8, A8);
    }


}

31.合并N个有序数组

题目描述:给定 N 个已经排好序的整数数组,请将这 N 个数组合并为一个新的有序数组,并返回该新数组。

  • 你需要设计一个高效的算法来完成合并操作,确保新数组中的元素仍然按非降序排列。
  • 要求算法的时间复杂度尽可能低。

输入格式:

  • 输入为一个包含 N 个有序整数数组的二维数组 arrays,其中每个子数组代表一个排好序的整数数组。
  • 每个数组的长度可能不同。

输出格式:

  • 返回一个新的 List<Integer>,该列表包含所有输入数组中的元素,并且按照非降序排列。

解题思路

要有效地合并 N 个有序数组,可以使用最小堆(也称为优先队列)来实现:

  1. 最小堆的使用:最小堆的特点是堆顶元素始终是当前堆中的最小元素。利用这一特性,我们可以将每个有序数组的第一个元素放入最小堆中,然后通过不断取出堆顶元素并将其下一个元素加入堆中的方式,实现对 N 个有序数组的合并。

  2. 步骤

    初始化最小堆:将 N 个有序数组的第一个元素连同该元素所属的数组和在数组中的位置一起放入最小堆中。逐步合并:如果该元素所属的数组中还有后续元素,则将后续元素加入堆中。取出最小堆的堆顶元素(当前最小的元素),将其添加到结果数组中。重复上述步骤,直到最小堆为空为止,此时所有元素已经合并到结果数组中。

时间复杂度分析

  • 初始时,将 N 个元素加入堆的时间复杂度是 O(N)
  • 接下来,我们需要对每个元素进行 log(N) 次的堆调整操作,总共有 K 个元素需要合并,K 是所有数组中元素的总数,因此总体时间复杂度为 O(K * log(N))
  • 这个时间复杂度相比于直接合并和排序的 O(K * log(K)) 要更优,尤其是在 N 远小于 K 的情况下。

具体代码展示验证

package org.zyf.javabasic.letcode.hot100;

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

/**
 * @program: zyfboot-javabasic
 * @description: MergeKSortedArrays
 * @author: zhangyanfeng
 * @create: 2024-08-30 14:50
 **/
public class MergeKSortedArrays {
    // 定义一个内部类来存储数组的值及其来源
    static class ArrayEntry implements Comparable<ArrayEntry> {
        // 当前值
        int value;
        // 来源数组的索引
        int arrayIndex;
        // 当前元素在数组中的索引
        int elementIndex;

        public ArrayEntry(int value, int arrayIndex, int elementIndex) {
            this.value = value;
            this.arrayIndex = arrayIndex;
            this.elementIndex = elementIndex;
        }

        @Override
        public int compareTo(ArrayEntry other) {
            return Integer.compare(this.value, other.value); // 按值升序排列
        }
    }

    public static List<Integer> mergeKSortedArrays(int[][] arrays) {
        // 使用最小堆(优先队列)来合并n个有序数组
        PriorityQueue<ArrayEntry> minHeap = new PriorityQueue<>(); // 最小堆
        List<Integer> result = new ArrayList<>(); // 用于存储最终合并后的List

        // 初始化最小堆,将每个数组的第一个元素放入堆中
        for (int i = 0; i < arrays.length; i++) {
            if (arrays[i].length > 0) {
                // 将数组的第一个元素放入堆中
                minHeap.offer(new ArrayEntry(arrays[i][0], i, 0));
            }
        }

        // 开始合并数组
        while (!minHeap.isEmpty()) {
            // 取出堆顶元素,即当前最小值
            ArrayEntry current = minHeap.poll();
            // 将最小值加入结果List中
            result.add(current.value);

            // 如果当前数组还有剩余元素,则将下一个元素加入堆中
            if (current.elementIndex + 1 < arrays[current.arrayIndex].length) {
                minHeap.offer(new ArrayEntry(
                        arrays[current.arrayIndex][current.elementIndex + 1],
                        current.arrayIndex,
                        current.elementIndex + 1
                ));
            }
        }

        // 返回合并后的List
        return result;
    }

    public static void main(String[] args) {
        int[][] arrays = {
                {1, 4, 7},
                {2, 5, 8},
                {3, 6, 9}
        };

        // 调用mergeKSortedArrays方法合并数组
        List<Integer> result = mergeKSortedArrays(arrays);

        // 输出合并后的结果List
        for (int num : result) {
            System.out.print(num + " ");
        }
    }
}

32、数组差值最小值

题目描述:给定两个整形数组,各自取出一个元素,求这两个元素的差值绝对值的最小值。

解题思路

为了找出两个数组中差值最小的元素对,最直接的方式是通过暴力枚举,即尝试每个数组中每一个元素的组合,计算出所有可能组合的差值绝对值,然后返回最小值。然而,暴力枚举的时间复杂度为 O(n * m)(n 和 m 分别是两个数组的长度),在数组较大的情况下效率不高。

为了提高效率,可以利用排序 + 双指针的方式来解决问题。通过排序,两个数组中的相邻元素差值通常较小,这样就可以更快地找到差值最小的组合。

详细步骤:

  1. 数组排序:首先对两个数组进行排序。排序后,两个数组中相邻的元素(最小差值的元素对)将更容易被找到,因为排序后的数组是单调递增的。
  2. 双指针法:定义两个指针分别指向两个排序后的数组的第一个元素,然后:比较当前两个指针对应元素的差值,更新最小差值;移动较小元素对应的指针(因为较小元素距离更大的元素有更大的潜力减小差值);不断重复此过程,直到其中一个数组被遍历完。
  3. 提前返回:如果找到的差值是 0,则直接返回,因为这是最小的可能值。

复杂度分析:

  • 时间复杂度
    • 排序两个数组的时间复杂度为 O(nlogn + mlogm),其中 nm 分别是两个数组的长度。
    • 双指针遍历两个数组的时间复杂度为 O(n + m),因为每次指针移动都会处理一个元素。
    • 因此,总的时间复杂度为 O(nlogn + mlogm)
  • 空间复杂度
    • 只需要常数级别的额外空间(用于指针和变量),因此空间复杂度为 O(1)

具体代码展示验证

package org.zyf.javabasic.letcode.array;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 从两个整形数组中分别取出一个元素,求出这两个元素差值的绝对值,最终返回两个数组中元素差值绝对值的最小值。
 * @author: zhangyanfeng
 * @create: 2024-09-14 15:23
 **/
public class MinAbsDifference {
    public static int findMinAbsDifference(int[] arr1, int[] arr2) {
        // 边界情况处理,确保输入数组不为空
        if (arr1 == null || arr2 == null || arr1.length == 0 || arr2.length == 0) {
            throw new IllegalArgumentException("输入数组不能为空");
        }

        // 对两个数组排序
        Arrays.sort(arr1);
        Arrays.sort(arr2);

        int i = 0, j = 0;
        int minDiff = Integer.MAX_VALUE;  // 初始化最小差值为最大值

        // 使用双指针法遍历两个数组
        while (i < arr1.length && j < arr2.length) {
            // 计算当前两个指针指向元素的差值
            int diff = Math.abs(arr1[i] - arr2[j]);

            // 更新最小差值
            minDiff = Math.min(minDiff, diff);

            // 如果发现最小差值为0,直接返回
            if (minDiff == 0) {
                return 0;
            }

            // 移动指针:较小的元素对应的指针前移
            if (arr1[i] < arr2[j]) {
                i++;
            } else {
                j++;
            }
        }

        return minDiff;  // 返回最小差值
    }

    public static void main(String[] args) {
        // 示例输入
        int[] arr1 = {1, 3, 15, 11, 2};
        int[] arr2 = {23, 127, 235, 19, 8};
        // 输出最小绝对差值
        System.out.println("最小绝对差值: " + findMinAbsDifference(arr1, arr2));
    }
}

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张彦峰ZYF

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

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

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

打赏作者

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

抵扣说明:

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

余额充值