LeetCode 热题 100 回顾

目录

一、哈希部分

1.两数之和 (简单)

2.字母异位词分组 (中等)

3.最长连续序列 (中等)

二、双指针部分

4.移动零 (简单)

5.盛最多水的容器 (中等)

6. 三数之和 (中等)

7.接雨水 (困难)

三、滑动窗口

8.无重复字符的最长子串 (中等)

9.找到字符串中所有字母异位词 (中等)

四、子串

10.和为 K 的子数组 (中等)

11.滑动窗口最大值 (困难)

12.最小覆盖子串 (困难)

五、普通数组

13.最大子数组和 (中等)

14.合并区间 (中等)

15.轮转数组 (中等)

16.除自身以外数组的乘积 (中等)

17.缺失的第一个正数 (困难)

六、矩阵

18.矩阵置零 (中等)

19.螺旋矩阵(中等)

20.旋转图像(中等)

21.搜索二维矩阵 II(中等)

七、链表

22.相交链表(简单)

23.反转链表(简单)

24.回文链表(简单)

25.环形链表(简单)

26. 环形链表 II(中等)

27.合并两个有序链表(简单)

28.两数相加(中等)

29.删除链表的倒数第 N 个结点(中等)

30.两两交换链表中的节点(中等)

31.K 个一组翻转链表 (困难)

32.随机链表的复制(中等)

33.排序链表(中等)

34.合并 K 个升序链表 (困难)

35.LRU 缓存(中等)

八、二叉树

36.二叉树的中序遍历(简单)

37.二叉树的最大深度(简单)

38.翻转二叉树(简单)

39.对称二叉树(简单)

40.二叉树的直径(简单)

41.二叉树的层序遍历(中等)

42.将有序数组转换为二叉搜索树(简单)

43.验证二叉搜索树(中等)

44.二叉搜索树中第 K 小的元素(中等)

45.二叉树的右视图(中等)

46.二叉树展开为链表(中等)

47.从前序与中序遍历序列构造二叉树(中等)

48.路径总和 III(中等)

49.二叉树的最近公共祖先(中等)

50.二叉树中的最大路径和 (困难)

九、图论

51.岛屿数量(中等)

52.腐烂的橘子(中等)

53.课程表(中等)

54.实现 Trie (前缀树)(中等)

十、回溯

55.全排列(中等)

56.子集(中等)

57.电话号码的字母组合(中等)

58.组合总和(中等)

59.括号生成(中等)

60.单词搜索(中等)

61.分割回文串(中等)

62.N 皇后 (困难)

十一、二分查找

63.搜索插入位置(简单)

64.搜索二维矩阵(中等)

65.在排序数组中查找元素的第一个和最后一个位置(中等)

66.搜索旋转排序数组(中等)

67.寻找旋转排序数组中的最小值(中等)

68.寻找两个正序数组的中位数 (困难)

十二、栈

69.有效的括号(简单)

70.最小栈(中等)

71.字符串解码(中等)

72.每日温度(中等)

73.柱状图中最大的矩形(困难)

十三、堆

74.数组中的第K个最大元素(中等)

75.前 K 个高频元素(中等)

76.数据流的中位数(困难)

十四、贪心算法

77.买卖股票的最佳时机(简单)

78.跳跃游戏(中等)

79.跳跃游戏 II(中等)

80.划分字母区间(中等)

十五、动态规划

81.爬楼梯(简单)

82.杨辉三角(简单)

83.打家劫舍(中等)

84.完全平方数(中等)

85.零钱兑换(中等)

86.单词拆分 (中等)

87.最长递增子序列  (中等)

88.乘积最大子数组 (中等)

89.分割等和子集 (中等)

90.最长有效括号(困难)

十六、多维动态规划

91.不同路径 (中等)

92.最小路径和(中等)

93.最长回文子串(中等)

94.最长公共子序列 (中等)

95.编辑距离(中等)

十七、技巧

96.只出现一次的数字(简单)

97.多数元素(简单)

98.颜色分类(中等)

99.下一个排列(中等)

100.寻找重复数(中等)


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

一、哈希部分

1.两数之和 (简单)

题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

示例 1:输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

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

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

提示:

  • 2 <= nums.length <= 10^{4}
  • -10^{9} <= nums[i] <= 10^{9}
  • -10^{9} <= target <= 10^{9}
  • 只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

解题思路

这个问题可以使用哈希表(HashMap)来实现。我们可以通过一次遍历数组的方式解决该问题。具体步骤如下:

  1. 创建一个哈希表:用来存储数组中已经访问过的元素及其对应的下标。
  2. 遍历数组:对数组中的每一个元素,计算出它与目标值之间的差值 complement
  3. 查找补数:检查哈希表中是否存在这个差值。如果存在,说明我们已经找到了两个数,它们的和等于目标值,返回它们的下标。
  4. 更新哈希表:如果当前元素的补数不在哈希表中,将当前元素和它的下标加入哈希表中。
  5. 返回结果:当找到符合条件的两个数时,直接返回它们的下标。

复杂度分析

  • 时间复杂度:O(n)。我们只需遍历数组一次,对于数组中的每个元素,哈希表的查找和插入操作的时间复杂度都是 O(1),因此总的时间复杂度为 O(n)。

  • 空间复杂度:O(n)。在最坏的情况下(没有两个元素的和为目标值),我们需要在哈希表中存储数组中所有的元素及其下标,因此空间复杂度为 O(n)。

代码实现

package org.zyf.javabasic.letcode.hot100.hash;

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

/**
 * @program: zyfboot-javabasic
 * @description: 两数之和
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:16
 **/
public class TwoSumSolution {
    public int[] twoSum(int[] nums, int target) {
        // 创建一个哈希表来存储已经访问过的元素及其下标
        Map<Integer, Integer> numMap = new HashMap<>();

        // 遍历数组中的每一个元素
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];

            // 检查补数是否在哈希表中
            if (numMap.containsKey(complement)) {
                // 如果补数在哈希表中,返回补数的下标和当前元素的下标
                return new int[] { numMap.get(complement), i };
            }

            // 如果补数不在哈希表中,将当前元素和下标加入哈希表
            numMap.put(nums[i], i);
        }

        // 按题意,只会存在一个有效答案,所以不需要额外的返回值
        throw new IllegalArgumentException("No two sum solution");
    }

    public static void main(String[] args) {
        TwoSumSolution solution = new TwoSumSolution();

        int[] nums1 = {2, 7, 11, 15};
        int target1 = 9;
        int[] result1 = solution.twoSum(nums1, target1);
        System.out.println("Result 1: [" + result1[0] + ", " + result1[1] + "]"); // 输出: [0, 1]

        int[] nums2 = {3, 2, 4};
        int target2 = 6;
        int[] result2 = solution.twoSum(nums2, target2);
        System.out.println("Result 2: [" + result2[0] + ", " + result2[1] + "]"); // 输出: [1, 2]

        int[] nums3 = {3, 3};
        int target3 = 6;
        int[] result3 = solution.twoSum(nums3, target3);
        System.out.println("Result 3: [" + result3[0] + ", " + result3[1] + "]"); // 输出: [0, 1]
    }
}

2.字母异位词分组 (中等)

题目描述

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:输入: strs = [""] 输出: [[""]]

示例 3:输入: strs = ["a"] 输出: [["a"]]

提示:

  • 1 <= strs.length <= 10^{4}
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母

解题思路

要将字母异位词组合在一起,我们可以利用哈希表(HashMap)的特性。字母异位词在重新排列后,它们的字符顺序会相同,因此我们可以通过以下步骤实现这一目标:

  1. 创建哈希表:用来存储排序后的字符串(作为键)和原始字符串的列表(作为值)。
  2. 遍历字符串数组:对于每一个字符串,将它的字符排序后作为键,如果这个键已经存在于哈希表中,那么将当前字符串添加到这个键对应的列表中;如果这个键不存在,则创建一个新的列表并将当前字符串加入其中。
  3. 返回结果:遍历完成后,哈希表中存储的所有值即为字母异位词分组的结果。

复杂度分析

  • 时间复杂度:O(n * k * log k)。其中 n 是字符串数组的长度,k 是字符串的平均长度。

    对于每个字符串,排序的时间复杂度为 O(k * log k),总的时间复杂度为 O(n * k * log k)。
  • 空间复杂度:O(n * k)。需要使用额外的空间来存储排序后的字符串,以及存储结果的哈希表。哈希表最多需要 O(n * k) 的空间,其中 n 是字符串数组的长度,k 是字符串的平均长度。

代码实现

package org.zyf.javabasic.letcode.hot100.hash;

import java.util.*;

/**
 * @program: zyfboot-javabasic
 * @description: 字母异位词
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:26
 **/
public class GroupAnagramsSolution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 创建一个哈希表,键为排序后的字符串,值为包含该异位词的列表
        Map<String, List<String>> anagramMap = new HashMap<>();

        // 遍历字符串数组
        for (String str : strs) {
            // 将字符串的字符排序
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            String sortedStr = new String(chars);

            // 将排序后的字符串作为键
            if (!anagramMap.containsKey(sortedStr)) {
                // 如果哈希表中不存在这个键,创建一个新的列表
                anagramMap.put(sortedStr, new ArrayList<>());
            }
            // 将原始字符串加入该键对应的列表中
            anagramMap.get(sortedStr).add(str);
        }

        // 返回哈希表中所有的值,即为字母异位词的分组
        return new ArrayList<>(anagramMap.values());
    }

    public static void main(String[] args) {
        GroupAnagramsSolution solution = new GroupAnagramsSolution();

        String[] strs1 = {"eat", "tea", "tan", "ate", "nat", "bat"};
        System.out.println(solution.groupAnagrams(strs1));
        // 输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

        String[] strs2 = {""};
        System.out.println(solution.groupAnagrams(strs2));
        // 输出: [[""]]

        String[] strs3 = {"a"};
        System.out.println(solution.groupAnagrams(strs3));
        // 输出: [["a"]]
    }
}

3.最长连续序列 (中等)

题目描述

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:输入:nums = [100,4,200,1,3,2]  输出:4   解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

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

提示:

  • 0 <= nums.length <= 10^{5}
  • -10^{9} <= nums[i] <= 10^{9}

解题思路

要在未排序的整数数组中找出最长的连续序列,并且时间复杂度要求为 O(n),可以采用哈希集(HashSet)来进行优化。具体步骤如下:

  1. 使用哈希集:首先将所有元素放入一个哈希集中,这样可以在 O(1) 时间内判断某个元素是否存在。
  2. 遍历数组:然后遍历数组中的每个元素,对每个元素 x,如果 x - 1 不存在于哈希集中,说明这个元素 x 可能是某个连续序列的起点。
  3. 查找最长序列:从这个起点开始,逐一检查 x+1, x+2, ... 是否存在于哈希集中,统计这个序列的长度。
  4. 记录最长长度:在遍历过程中记录并更新最长的序列长度。

复杂度分析

  • 时间复杂度:O(n)。每个数字最多只会被访问一次,因此时间复杂度为 O(n),其中 n 是数组的长度。在哈希集中插入和查找的操作时间复杂度都是 O(1)。

  • 空间复杂度:O(n)。需要一个哈希集来存储数组中的所有元素,最坏情况下需要 O(n) 的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.hash;

import java.util.HashSet;
import java.util.Set;

/**
 * @program: zyfboot-javabasic
 * @description: 最长序列(不要求序列元素在原数组中连续)的长度
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:34
 **/
public class LongestConsecutiveSolution {
    public int longestConsecutive(int[] nums) {
        // 将所有数字放入哈希集
        Set<Integer> numSet = new HashSet<>();
        for (int num : nums) {
            numSet.add(num);
        }

        int longestStreak = 0;

        // 遍历数组中的每一个数字
        for (int num : nums) {
            // 只有当 num-1 不在哈希集中时,才认为 num 是一个序列的起点
            if (!numSet.contains(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;

                // 从起点开始寻找连续的序列
                while (numSet.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                // 更新最长序列的长度
                longestStreak = Math.max(longestStreak, currentStreak);
            }
        }

        return longestStreak;
    }

    public static void main(String[] args) {
        LongestConsecutiveSolution solution = new LongestConsecutiveSolution();

        int[] nums1 = {100, 4, 200, 1, 3, 2};
        System.out.println(solution.longestConsecutive(nums1)); // 输出: 4

        int[] nums2 = {0, 3, 7, 2, 5, 8, 4, 6, 0, 1};
        System.out.println(solution.longestConsecutive(nums2)); // 输出: 9
    }
}

二、双指针部分

4.移动零 (简单)

题目描述

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

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

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

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

提示:

  • 1 <= nums.length <= 10^{4}
  • -2^{31} <= nums[i] <= 2^{31} - 1

进阶:你能尽量减少完成的操作次数吗?

解题思路

为了在不复制数组的情况下原地移动所有 0 到数组末尾,同时保持非零元素的相对顺序,我们可以使用双指针技术来实现。具体步骤如下:

  1. 使用双指针:我们使用两个指针,ij。其中,i 用于遍历整个数组,j 用于记录下一个非零元素应该放置的位置。
  2. 遍历数组:遍历数组,当遇到非零元素时,将其移动到 j 指针的位置,然后将 j 向前移动一位。
  3. 填充零:当所有非零元素都按顺序放置好之后,j 之后的位置都应该填充为 0,直到数组结束。

复杂度分析

  • 时间复杂度:O(n)。数组中的每个元素最多被遍历两次(一次在第一次遍历时移动非零元素,另一次在填充零时),因此时间复杂度为 O(n)。

  • 空间复杂度:O(1)。只使用了常数级别的额外空间,即指针 j,因此空间复杂度为 O(1)。

代码实现

package org.zyf.javabasic.letcode.hot100.twopoint;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 移动零
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:44
 **/
public class MoveZeroesSolution {
    public void moveZeroes(int[] nums) {
        int j = 0; // j指针用于记录下一个非零元素的位置

        // 遍历数组,将所有非零元素按顺序移动到前面
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 0) {
                nums[j] = nums[i];
                j++;
            }
        }

        // 将剩下的位置全部填充为0
        for (int i = j; i < nums.length; i++) {
            nums[i] = 0;
        }
    }

    public static void main(String[] args) {
        MoveZeroesSolution solution = new MoveZeroesSolution();

        int[] nums1 = {0, 1, 0, 3, 12};
        solution.moveZeroes(nums1);
        System.out.println(Arrays.toString(nums1)); // 输出: [1, 3, 12, 0, 0]

        int[] nums2 = {0};
        solution.moveZeroes(nums2);
        System.out.println(Arrays.toString(nums2)); // 输出: [0]
    }
}

5.盛最多水的容器 (中等)

题目描述

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

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

提示:

  • n == height.length
  • 2 <= n <= 10^{5}
  • 0 <= height[i] <= 10^{4}

解题思路

这个问题可以通过使用双指针的方式来解决。因为我们想要找到两个垂线,使得它们能形成的容器容纳最多的水,所以可以通过以下步骤实现:

  1. 初始化双指针:一个指针 left 指向数组的起始位置,另一个指针 right 指向数组的末尾位置。
  2. 计算容积:在每一步中,计算由 leftright 指针指向的垂线所形成的容器的容积,公式为 min(height[left], height[right]) * (right - left)
  3. 移动指针:为了找到更大的容积,比较 height[left]height[right],将较小的那个指针向中间移动一位(如果左侧较小,则左指针右移,否则右指针左移)。
  4. 更新最大值:在每次计算中,记录最大容积的值。
  5. 终止条件:当两个指针相遇时,遍历结束,最大容积即为结果。

复杂度分析

  • 时间复杂度:O(n)。在双指针法中,每一步只移动一个指针,一共需要遍历整个数组一次,因此时间复杂度为 O(n)。

  • 空间复杂度:O(1)。只使用了固定的额外空间来存储指针和最大面积,因此空间复杂度为 O(1)。

代码实现

package org.zyf.javabasic.letcode.hot100.twopoint;

/**
 * @program: zyfboot-javabasic
 * @description: 盛最多水的容器  ​
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:54
 **/
public class MaxAreaSolution {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1;
        int maxArea = 0;

        // 使用双指针法计算最大面积
        while (left < right) {
            // 计算当前指针指向的垂线形成的容器的面积
            int currentArea = Math.min(height[left], height[right]) * (right - left);
            // 更新最大面积
            maxArea = Math.max(maxArea, currentArea);

            // 移动较小的一端的指针
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }

        return maxArea;
    }

    public static void main(String[] args) {
        MaxAreaSolution solution = new MaxAreaSolution();

        int[] height1 = {1,8,6,2,5,4,8,3,7};
        System.out.println(solution.maxArea(height1)); // 输出: 49

        int[] height2 = {1,1};
        System.out.println(solution.maxArea(height2)); // 输出: 1
    }
}

6. 三数之和 (中等)

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:输入:nums = [-1,0,1,2,-1,-4]   输出:[[-1,-1,2],[-1,0,1]]   解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。

示例 2:输入:nums = [0,1,1]   输出:[]   解释:唯一可能的三元组和不为 0 。

示例 3:输入:nums = [0,0,0]   输出:[[0,0,0]]   解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -10^{5} <= nums[i] <= 10^{5}

解题思路

要在数组中找出所有和为 0 且不重复的三元组,可以采用排序+双指针的方法。具体步骤如下:

  1. 排序:首先对数组进行排序,这样可以方便地使用双指针来寻找三元组,并且可以避免重复。
  2. 遍历数组:从第一个元素开始,固定一个元素,接着使用双指针的方法寻找其后面的元素组成的三元组。
  3. 双指针查找:对于固定的元素 nums[i],使用左指针 left 指向 i+1,右指针 right 指向数组的末尾。当 nums[i] + nums[left] + nums[right] == 0 时,说明找到了一个三元组,将其加入结果集。然后为了避免重复,需要跳过重复的元素。
  4. 移动指针:如果三者之和大于 0,说明右指针指向的元素过大,需要将右指针左移;如果三者之和小于 0,说明左指针指向的元素过小,需要将左指针右移。
  5. 跳过重复元素:为了避免重复的三元组,对于 nums[i]nums[left]nums[right] 都需要跳过重复的元素。

复杂度分析

  • 时间复杂度:O(n^2)。排序的时间复杂度为 O(n log n)。双指针查找所有三元组的时间复杂度为 O(n^2),因为对于每个元素,内层循环遍历一次其后的元素。

  • 空间复杂度:O(1)。只使用了常数级别的额外空间(不包括存储结果所用的空间),因此空间复杂度为 O(1)。

代码实现

package org.zyf.javabasic.letcode.hot100.twopoint;

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

/**
 * @program: zyfboot-javabasic
 * @description: 三数之和
 * @author: zhangyanfeng
 * @create: 2024-08-21 20:59
 **/
public class ThreeSumSolution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums); // 排序数组

        for (int i = 0; i < nums.length - 2; i++) {
            // 跳过重复的元素
            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) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    // 跳过重复的 left 元素
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    // 跳过重复的 right 元素
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }

                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }

        return result;
    }

    public static void main(String[] args) {
        ThreeSumSolution solution = new ThreeSumSolution();

        int[] nums1 = {-1,0,1,2,-1,-4};
        System.out.println(solution.threeSum(nums1)); // 输出: [[-1,-1,2],[-1,0,1]]

        int[] nums2 = {0,1,1};
        System.out.println(solution.threeSum(nums2)); // 输出: []

        int[] nums3 = {0,0,0};
        System.out.println(solution.threeSum(nums3)); // 输出: [[0,0,0]]
    }
}

7.接雨水 (困难)

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:输入:height = [4,2,0,3,2,5]   输出:9

提示:

  • n == height.length
  • 1 <= n <= 2 * 10^{4}
  • 0 <= height[i] <= 10^{5}

解题思路

为了计算在柱子之间能够接住多少雨水,可以使用双指针的方法。具体步骤如下:

  1. 初始化指针和变量:使用两个指针 leftright 分别指向数组的两端。leftMaxrightMax 分别记录从左侧到 left 位置的最大高度和从右侧到 right 位置的最大高度。water 用于记录接住的总雨水量。

  2. 双指针遍历:如果 height[left] 小于 height[right],则说明 left 位置的柱子可能会接住雨水,接住的水量取决于 leftMax 和当前 height[left] 的差值。如果 leftMax 大于 height[left],则能接住雨水,并将 left 指针右移。否则,移动 right 指针并以同样的方式计算 right 位置能接住的雨水量。

  3. 更新最大高度:每次移动指针时,更新 leftMaxrightMax,以便在接下来的计算中使用。

  4. 终止条件:当 leftright 指针相遇时,遍历结束,所有的雨水量已经计算完毕。

复杂度分析

  • 时间复杂度:O(n)。双指针遍历整个数组一次,因此时间复杂度为 O(n),其中 n 是数组的长度。

  • 空间复杂度:O(1)。只使用了固定的额外空间来存储指针和变量,因此空间复杂度为 O(1)。

代码实现

package org.zyf.javabasic.letcode.hot100.twopoint;

/**
 * @program: zyfboot-javabasic
 * @description: 接雨水  ​
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:09
 **/
public class TrapSolution {
    public int trap(int[] height) {
        // 初始化双指针,分别指向数组的两端
        int left = 0, right = height.length - 1;

        // 初始化左边和右边的最大高度
        int leftMax = 0, rightMax = 0;

        // 初始化接住的雨水量
        int water = 0;

        // 当左指针小于右指针时,继续遍历
        while (left < right) {
            // 如果左边柱子低于右边柱子,处理左边
            if (height[left] < height[right]) {
                // 如果当前左边的高度大于等于 leftMax,更新 leftMax
                if (height[left] >= leftMax) {
                    leftMax = height[left];
                } else {
                    // 否则,leftMax 大于当前高度,计算能接住的水量
                    water += leftMax - height[left];
                }
                // 将左指针右移
                left++;
            } else {
                // 如果右边柱子低于或等于左边柱子,处理右边
                if (height[right] >= rightMax) {
                    rightMax = height[right];
                } else {
                    // 否则,rightMax 大于当前高度,计算能接住的水量
                    water += rightMax - height[right];
                }
                // 将右指针左移
                right--;
            }
        }

        // 返回总的接住的雨水量
        return water;
    }

    public static void main(String[] args) {
        TrapSolution solution = new TrapSolution();

        // 测试用例 1
        int[] height1 = {0,1,0,2,1,0,1,3,2,1,2,1};
        System.out.println(solution.trap(height1)); // 输出: 6

        // 测试用例 2
        int[] height2 = {4,2,0,3,2,5};
        System.out.println(solution.trap(height2)); // 输出: 9
    }
}

三、滑动窗口

8.无重复字符的最长子串 (中等)

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:输入: s = "abcabcbb"   输出: 3   解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:输入: s = "bbbbb"   输出: 1   解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:输入: s = "pwwkew"   输出: 3   解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。   请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 10^{4}
  • s 由英文字母、数字、符号和空格组成

解题思路

为了解决这个问题,可以使用滑动窗口的技术。滑动窗口可以动态地维护一个子串,并且当发现子串中有重复字符时,可以调整窗口的起始位置,从而找到不含重复字符的最长子串。

具体步骤如下:

  1. 初始化:使用一个哈希集 set 来存储当前窗口内的字符。使用两个指针 leftright 表示滑动窗口的左右边界。初始时,leftright 都指向字符串的起始位置。

  2. 移动窗口:当 right 指针所指向的字符未出现在 set 中时,将其加入 set,并将 right 右移,以扩大窗口;当 right 指针所指向的字符已经在 set 中时,说明出现了重复字符,这时需要将 left 指针右移,缩小窗口,直到窗口内没有重复字符为止。

  3. 记录最大长度:每次更新窗口后,记录当前窗口的长度,并与已知的最大长度进行比较,保留较大的值。

  4. 终止条件:当 right 指针遍历到字符串的末尾时,遍历结束。

复杂度分析

  • 时间复杂度:O(n)。每个字符在最坏情况下会被访问两次:一次通过 right 指针,一次通过 left 指针。因此,总的时间复杂度为 O(n),其中 n 是字符串的长度。

  • 空间复杂度:O(min(m, n))。使用了一个哈希集来存储当前窗口内的字符,最坏情况下需要存储所有字符,因此空间复杂度与字符集大小 m 和字符串长度 n 有关,取其中的较小值。

代码实现

package org.zyf.javabasic.letcode.hot100.slidingwindow;

import java.util.HashSet;
import java.util.Set;

/**
 * @program: zyfboot-javabasic
 * @description: 无重复字符的最长子串   ​
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:18
 **/
public class LengthOfLongestSubstringSolution {
    public int lengthOfLongestSubstring(String s) {
        // 使用哈希集来存储当前窗口内的字符
        Set<Character> set = new HashSet<>();

        // 初始化左右指针和最大长度
        int left = 0, right = 0;
        int maxLength = 0;

        // 开始滑动窗口遍历字符串
        while (right < s.length()) {
            // 如果当前字符不在哈希集中,说明没有重复,加入哈希集并移动右指针
            if (!set.contains(s.charAt(right))) {
                set.add(s.charAt(right));
                right++;
                // 更新最大长度
                maxLength = Math.max(maxLength, right - left);
            } else {
                // 如果当前字符已经在哈希集中,说明有重复,移除左指针的字符并移动左指针
                set.remove(s.charAt(left));
                left++;
            }
        }

        // 返回记录的最大长度
        return maxLength;
    }

    public static void main(String[] args) {
        LengthOfLongestSubstringSolution solution = new LengthOfLongestSubstringSolution();

        // 测试用例 1
        String s1 = "abcabcbb";
        System.out.println(solution.lengthOfLongestSubstring(s1)); // 输出: 3

        // 测试用例 2
        String s2 = "bbbbb";
        System.out.println(solution.lengthOfLongestSubstring(s2)); // 输出: 1

        // 测试用例 3
        String s3 = "pwwkew";
        System.out.println(solution.lengthOfLongestSubstring(s3)); // 输出: 3
    }
}

9.找到字符串中所有字母异位词 (中等)

题目描述

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:输入: s = "cbaebabacd", p = "abc"   输出: [0,6]   解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。

 示例 2:输入: s = "abab", p = "ab"   输出: [0,1,2]   解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。

提示:

  • 1 <= s.length, p.length <= 3 * 104
  • s 和 p 仅包含小写字母

解题思路

为了解决这个问题,我们可以使用滑动窗口和哈希表的方法。滑动窗口可以动态地维护一个长度为 p.length() 的子串,同时我们可以通过比较该子串和 p 是否为异位词来判断是否记录其起始索引。具体步骤如下:

  1. 构建目标频率表:我们首先统计字符串 p 中每个字符的出现频率,保存到一个数组 pFreq 中。

  2. 初始化滑动窗口:使用两个指针 leftright 表示滑动窗口的左右边界。初始时,leftright 都指向字符串 s 的起始位置。另外,用一个数组 sFreq 来统计当前窗口中字符的频率。

  3. 滑动窗口遍历:每次将 right 指向的字符加入 sFreq,并移动 right 指针。当窗口大小等于 p.length() 时,比较 sFreqpFreq 是否相等,如果相等则说明该窗口是 p 的一个异位词,将 left 的位置加入结果列表。然后,移动 left 指针,并减少 left 指向字符的频率,继续下一轮滑动。

  4. 终止条件:当 right 指针遍历到字符串 s 的末尾时,遍历结束。

复杂度分析

  • 时间复杂度:O(n)。其中 n 是字符串 s 的长度。滑动窗口每次移动都需要比较两个频率表,这一步是 O(1) 的操作,因此整个算法的时间复杂度为 O(n)。

  • 空间复杂度:O(1)。虽然我们使用了两个频率表 pFreqsFreq,但它们的大小是固定的(26个字母),所以空间复杂度为 O(1)。

代码实现

package org.zyf.javabasic.letcode.hot100.slidingwindow;

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

/**
 * @program: zyfboot-javabasic
 * @description: 找到字符串中所有字母异位词   ​
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:26
 **/
public class FindAnagramsSolution {
    public List<Integer> findAnagrams(String s, String p) {
        // 结果列表
        List<Integer> result = new ArrayList<>();

        // 特殊情况处理
        if (s.length() < p.length()) {
            return result;
        }

        // 统计字符串 p 中每个字符的频率
        int[] pFreq = new int[26];
        for (char c : p.toCharArray()) {
            pFreq[c - 'a']++;
        }

        // 滑动窗口的字符频率
        int[] sFreq = new int[26];

        // 初始化滑动窗口
        int left = 0, right = 0;

        while (right < s.length()) {
            // 将当前字符加入窗口的频率统计
            sFreq[s.charAt(right) - 'a']++;

            // 当窗口大小达到 p 的长度时,开始检查
            if (right - left + 1 == p.length()) {
                // 检查当前窗口是否为异位词
                if (matches(sFreq, pFreq)) {
                    result.add(left);
                }

                // 移动左指针,缩小窗口,更新频率表
                sFreq[s.charAt(left) - 'a']--;
                left++;
            }

            // 右指针继续向右扩展窗口
            right++;
        }

        return result;
    }

    // 辅助函数:检查两个频率数组是否相同
    private boolean matches(int[] sFreq, int[] pFreq) {
        for (int i = 0; i < 26; i++) {
            if (sFreq[i] != pFreq[i]) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        FindAnagramsSolution solution = new FindAnagramsSolution();

        // 测试用例 1
        String s1 = "cbaebabacd";
        String p1 = "abc";
        System.out.println(solution.findAnagrams(s1, p1)); // 输出: [0, 6]

        // 测试用例 2
        String s2 = "abab";
        String p2 = "ab";
        System.out.println(solution.findAnagrams(s2, p2)); // 输出: [0, 1, 2]
    }
}

四、子串

10.和为 K 的子数组 (中等)

题目描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 

子数组是数组中元素的连续非空序列。

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

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

提示:

  • 1 <= nums.length <= 2 * 10^{4}
  • -1000 <= nums[i] <= 1000
  • -10^{7} <= k <= 10^{7}

解题思路

要解决这个问题,我们可以使用前缀和(Prefix Sum)以及哈希表来优化查找和为 k 的子数组的个数。前缀和的基本思想是通过累积数组元素的和,可以快速计算出任意子数组的和。

具体步骤如下:

  1. 前缀和的定义:我们定义 prefixSum[i] 为数组 nums 从第一个元素到第 i 个元素的累积和。即 prefixSum[i] = nums[0] + nums[1] + ... + nums[i]

    目标是找到一个下标对 (i, j),使得 prefixSum[j] - prefixSum[i-1] = k,这里的 i-1prefixSumi 的前一个位置。转换为查找问题就是:prefixSum[j] - k = prefixSum[i-1]
  2. 使用哈希表记录前缀和的出现次数

    我们用一个哈希表 prefixSumCount 来记录每个前缀和出现的次数,键为前缀和,值为该前缀和的出现次数;在遍历数组的过程中,计算当前的前缀和 currentSum,然后检查哈希表中是否存在 currentSum - k。如果存在,则说明在此之前有一个子数组的和为 k,将结果计数加上对应的次数。
  3. 初始化和遍历

    初始化 currentSum 为 0,同时在哈希表中加入 prefixSumCount[0] = 1,表示在开始前有一个空子数组的和为 0;遍历数组,更新 currentSum,并检查 currentSum - k 是否在哈希表中,更新结果计数。最后,更新哈希表 prefixSumCountcurrentSum 的次数。

复杂度分析

  • 时间复杂度:O(n)。其中 n 是数组 nums 的长度。我们只遍历一次数组,每次操作的时间复杂度是 O(1),因此总的时间复杂度是 O(n)。

  • 空间复杂度:O(n)。在最坏的情况下,哈希表 prefixSumCount 需要存储 n 个不同的前缀和,因此空间复杂度是 O(n)。

代码实现

package org.zyf.javabasic.letcode.hot100.substring;

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

/**
 * @program: zyfboot-javabasic
 * @description: 和为 K 的子数组
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:36
 **/
public class SubarraySumSolution {
    public int subarraySum(int[] nums, int k) {
        // 创建一个哈希表记录前缀和出现的次数
        Map<Integer, Integer> prefixSumCount = new HashMap<>();
        // 初始化前缀和为0的情况
        prefixSumCount.put(0, 1);

        int currentSum = 0; // 当前前缀和
        int count = 0; // 和为k的子数组的数量

        // 遍历数组
        for (int num : nums) {
            // 计算当前前缀和
            currentSum += num;

            // 检查是否存在一个前缀和,使得currentSum - k存在于哈希表中
            if (prefixSumCount.containsKey(currentSum - k)) {
                count += prefixSumCount.get(currentSum - k);
            }

            // 更新哈希表中当前前缀和的出现次数
            prefixSumCount.put(currentSum, prefixSumCount.getOrDefault(currentSum, 0) + 1);
        }

        return count; // 返回和为k的子数组的个数
    }

    public static void main(String[] args) {
        SubarraySumSolution solution = new SubarraySumSolution();

        // 测试用例 1
        int[] nums1 = {1, 1, 1};
        int k1 = 2;
        System.out.println(solution.subarraySum(nums1, k1)); // 输出: 2

        // 测试用例 2
        int[] nums2 = {1, 2, 3};
        int k2 = 3;
        System.out.println(solution.subarraySum(nums2, k2)); // 输出: 2
    }
}

11.滑动窗口最大值 (困难)

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

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

提示:

  • 1 <= nums.length <= 10^{5}
  • -10^{4} <= nums[i] <= 10^{4}
  • 1 <= k <= nums.length

解题思路

要解决这个问题,我们可以使用**双端队列(Deque)**来高效地找到滑动窗口内的最大值。双端队列允许我们在 O(1) 的时间复杂度下在队列的两端进行插入和删除操作。

具体步骤如下:

  1. 双端队列的定义与维护

    我们使用一个双端队列 deque 来存储数组 nums 中元素的索引。这个队列中的索引按元素大小降序排列,意味着队列的头部总是当前窗口的最大值;每次移动窗口时,我们会维护这个队列,确保队列中的元素始终属于当前窗口,并且在队列头部保存的是当前窗口的最大值。
  2. 窗口的移动与更新

    随着窗口向右移动,我们会依次从数组中添加新的元素到窗口中,并将其索引添加到 deque 中;如果 deque 中的第一个元素已经不在当前窗口范围内,我们将其从 deque 中移除;在将新元素加入 deque 时,如果 deque 尾部的元素小于新元素,则将尾部的元素移除,因为这些元素不会再成为最大值。
  3. 记录结果

    当窗口移动超过大小 k 后,每次我们都会将 deque 中的第一个元素(当前窗口的最大值)记录到结果列表中。

复杂度分析

  • 时间复杂度:O(n)。其中 n 是数组 nums 的长度。每个元素最多被插入和删除一次,因此总的时间复杂度是 O(n)。

  • 空间复杂度:O(k)。双端队列中最多会保存 k 个元素的索引,因此空间复杂度是 O(k)。

代码实现

package org.zyf.javabasic.letcode.hot100.substring;

import java.util.Deque;
import java.util.LinkedList;

/**
 * @program: zyfboot-javabasic
 * @description: 滑动窗口最大值
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:43
 **/
public class MaxSlidingWindowSolution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new int[0];
        }

        // 结果数组
        int[] result = new int[nums.length - k + 1];
        // 使用双端队列存储索引
        Deque<Integer> deque = new LinkedList<>();

        for (int i = 0; i < nums.length; i++) {
            // 移除队列中不在当前窗口范围内的元素
            if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }

            // 移除队列中所有小于当前元素的索引
            // 因为这些元素不会再成为最大值
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }

            // 将当前元素的索引添加到队列中
            deque.offerLast(i);

            // 当窗口大小达到k时,将当前窗口的最大值添加到结果数组中
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }

        return result;
    }

    public static void main(String[] args) {
        MaxSlidingWindowSolution solution = new MaxSlidingWindowSolution();

        // 测试用例 1
        int[] nums1 = {1, 3, -1, -3, 5, 3, 6, 7};
        int k1 = 3;
        int[] result1 = solution.maxSlidingWindow(nums1, k1);
        for (int num : result1) {
            System.out.print(num + " ");
        }
        // 输出: [3, 3, 5, 5, 6, 7]

        System.out.println();

        // 测试用例 2
        int[] nums2 = {1};
        int k2 = 1;
        int[] result2 = solution.maxSlidingWindow(nums2, k2);
        for (int num : result2) {
            System.out.print(num + " ");
        }
        // 输出: [1]
    }
}

12.最小覆盖子串 (困难)

题目描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

示例 2:输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。

示例 3:输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 10^{5}
  • s 和 t 由英文字母组成

进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?

解题思路

这个问题要求我们在字符串 s 中找到包含字符串 t 所有字符的最小子串。问题的核心是要用滑动窗口的技巧来找到最小的满足条件的子串。

滑动窗口算法步骤:

  1. 初始化两个计数器:一个用于记录当前窗口中各字符的出现次数 windowCount;另一个用于记录字符串 t 中每个字符所需的出现次数 targetCount

  2. 使用两个指针表示滑动窗口leftright 都初始化为 0,表示滑动窗口的左右边界;我们从 right 开始遍历字符串 s,并将字符加入窗口中。

  3. 移动 right 指针扩大窗口:每次移动 right 指针,将字符加入 windowCount 中;检查当前窗口是否包含字符串 t 中所有字符,即检查 windowCount 是否满足 targetCount

  4. 移动 left 指针缩小窗口:当窗口包含 t 中所有字符时,开始移动 left 指针以缩小窗口,尽量找到最小的符合条件的子串;在缩小窗口的过程中,不断更新最小子串的长度和起始位置。

  5. 返回结果:当遍历完成后,返回找到的最小子串,如果没有符合条件的子串,则返回空字符串 ""

复杂度分析

  • 时间复杂度:O(m + n)。其中 m 是字符串 s 的长度,n 是字符串 t 的长度。我们只需遍历字符串 s 一次,并且每个字符在 windowCount 中的增减操作是 O(1) 的,因此总时间复杂度为 O(m + n)。

  • 空间复杂度:O(∣S∣ + ∣T∣)。需要 O(∣T∣) 的空间存储字符串 t 中每个字符的计数,并且滑动窗口可能需要 O(∣S∣) 的空间来存储窗口中的字符计数。因此总空间复杂度为 O(∣S∣ + ∣T∣)。

代码实现

package org.zyf.javabasic.letcode.hot100.substring;

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

/**
 * @program: zyfboot-javabasic
 * @description: 最小覆盖子串
 * @author: zhangyanfeng
 * @create: 2024-08-21 21:55
 **/
public class MinWindowSolution {
    public String minWindow(String s, String t) {
        if (s == null || t == null || s.length() < t.length()) {
            return "";
        }

        // 记录t中每个字符的出现次数
        Map<Character, Integer> targetCount = new HashMap<>();
        for (char c : t.toCharArray()) {
            targetCount.put(c, targetCount.getOrDefault(c, 0) + 1);
        }

        // 定义窗口计数器
        Map<Character, Integer> windowCount = new HashMap<>();
        int left = 0, right = 0, matchCount = 0;
        int minLen = Integer.MAX_VALUE;
        int minStart = 0;

        while (right < s.length()) {
            char cRight = s.charAt(right);
            if (targetCount.containsKey(cRight)) {
                windowCount.put(cRight, windowCount.getOrDefault(cRight, 0) + 1);
                if (windowCount.get(cRight).intValue() == targetCount.get(cRight).intValue()) {
                    matchCount++;
                }
            }
            right++;

            // 当窗口包含所有t中的字符后,开始收缩窗口
            while (matchCount == targetCount.size()) {
                // 更新最小窗口
                if (right - left < minLen) {
                    minLen = right - left;
                    minStart = left;
                }

                char cLeft = s.charAt(left);
                if (targetCount.containsKey(cLeft)) {
                    windowCount.put(cLeft, windowCount.get(cLeft) - 1);
                    if (windowCount.get(cLeft) < targetCount.get(cLeft)) {
                        matchCount--;
                    }
                }
                left++;
            }
        }

        return minLen == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
    }

    public static void main(String[] args) {
        MinWindowSolution solution = new MinWindowSolution();
        String s1 = "ADOBECODEBANC";
        String t1 = "ABC";
        System.out.println(solution.minWindow(s1, t1));  // 输出: "BANC"

        String s2 = "a";
        String t2 = "a";
        System.out.println(solution.minWindow(s2, t2));  // 输出: "a"

        String s3 = "a";
        String t3 = "aa";
        System.out.println(solution.minWindow(s3, t3));  // 输出: ""
    }
}

五、普通数组

13.最大子数组和 (中等)

题目描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:输入:nums = [-2,1,-3,4,-1,2,1,-5,4]   输出:6   解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

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

示例 3:输入:nums = [5,4,-1,7,8]   输出:23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

解题思路

这个问题可以通过动态规划分治法两种方法解决。

动态规划法:动态规划法的核心思想是使用一个变量 currentMax 来记录当前子数组的最大和,然后更新全局最大和 globalMax

具体步骤

  1. 初始化 globalMaxcurrentMax 为数组的第一个元素。
  2. 从数组的第二个元素开始,遍历每个元素:更新 currentMaxMath.max(currentElement, currentMax + currentElement)。即决定是继续扩展当前子数组,还是从当前元素重新开始子数组;更新 globalMaxMath.max(globalMax, currentMax)
  3. 返回 globalMax 作为最终结果。

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

分治法:分治法利用递归将数组分为左右两部分,并结合三个部分来找到最大和:

  1. 左半部分的最大子数组和。
  2. 右半部分的最大子数组和。
  3. 跨越中间的最大子数组和。

具体步骤

  1. 将数组分成左右两部分。
  2. 递归地计算左半部分和右半部分的最大子数组和。
  3. 计算跨越中间的最大子数组和。首先从中间向左扩展,找到左侧最大和,然后从中间向右扩展,找到右侧最大和,最后求和得到跨越中间的最大子数组和。
  4. 返回三个值中的最大值。

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

复杂度分析

动态规划法

  • 时间复杂度:O(n)。我们遍历一次数组,每个元素进行常数时间的操作。
  • 空间复杂度:O(1)。我们只使用了常数空间来存储变量。

分治法

  • 时间复杂度:O(n log n)。每次分割数组都需要 O(n) 时间来计算跨越中间的子数组和,并且总共有 O(log n) 层递归。

  • 空间复杂度:O(log n)。递归调用栈的深度为 O(log n)。

代码实现

package org.zyf.javabasic.letcode.hot100.ordinaryarray;

/**
 * @program: zyfboot-javabasic
 * @description: 最大子数组和
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:04
 **/
public class MaxSubArraySolution {
    public int maxSubArray1(int[] nums) {
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException("Array cannot be empty");
        }

        // 初始化动态规划变量
        int globalMax = nums[0];
        int currentMax = nums[0];

        for (int i = 1; i < nums.length; i++) {
            currentMax = Math.max(nums[i], currentMax + nums[i]);
            globalMax = Math.max(globalMax, currentMax);
        }

        return globalMax;
    }

    public int maxSubArray2(int[] nums) {
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException("Array cannot be empty");
        }

        return maxSubArray(nums, 0, nums.length - 1);
    }

    private int maxSubArray(int[] nums, int left, int right) {
        if (left == right) {
            return nums[left];
        }

        int mid = (left + right) / 2;

        // 计算左半部分、右半部分以及跨越中间的最大子数组和
        int leftMax = maxSubArray(nums, left, mid);
        int rightMax = maxSubArray(nums, mid + 1, right);
        int crossMax = maxCrossingSubArray(nums, left, mid, right);

        // 返回三个部分中的最大值
        return Math.max(Math.max(leftMax, rightMax), crossMax);
    }

    private int maxCrossingSubArray(int[] nums, int left, int mid, int right) {
        int leftSum = Integer.MIN_VALUE;
        int sum = 0;

        // 计算跨越中间的最大子数组和(从中间向左)
        for (int i = mid; i >= left; i--) {
            sum += nums[i];
            if (sum > leftSum) {
                leftSum = sum;
            }
        }

        int rightSum = Integer.MIN_VALUE;
        sum = 0;

        // 计算跨越中间的最大子数组和(从中间向右)
        for (int i = mid + 1; i <= right; i++) {
            sum += nums[i];
            if (sum > rightSum) {
                rightSum = sum;
            }
        }

        // 返回跨越中间的最大子数组和
        return leftSum + rightSum;
    }

    public static void main(String[] args) {
        MaxSubArraySolution solution = new MaxSubArraySolution();

        // 测试用例 1
        int[] nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
        System.out.println(solution.maxSubArray1(nums1));  // 输出: 6

        // 测试用例 2
        int[] nums2 = {1};
        System.out.println(solution.maxSubArray1(nums2));  // 输出: 1

        // 测试用例 3
        int[] nums3 = {5, 4, -1, 7, 8};
        System.out.println(solution.maxSubArray1(nums3));  // 输出: 23
    }
}

14.合并区间 (中等)

题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start_{i}, end_{i}] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 1:输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • 1 <= intervals.length <= 10^{4}
  • intervals[i].length == 2
  • 0 <= start_{i} <= end_{i}end_{i} <= 10^{4}

解题思路

要解决这个问题,我们需要合并所有重叠的区间。以下是详细的解题步骤和思路:

  1. 排序:首先,我们需要按区间的起始位置对区间进行排序。这样可以确保我们处理区间时,所有重叠区间都在一起,便于合并。

  2. 合并区间:初始化一个结果列表 merged,用于存储合并后的区间。

    遍历排序后的区间列表。对于每个区间:如果当前区间与结果列表中的最后一个区间重叠,则合并这两个区间。具体地,更新结果列表中的最后一个区间的结束位置为当前区间的结束位置与最后一个区间结束位置中的较大者;如果结果列表为空,或当前区间与结果列表中的最后一个区间不重叠,则将当前区间添加到结果列表。
  3. 返回结果:遍历完成后,结果列表 merged 中的区间即为合并后的不重叠区间。

复杂度分析

时间复杂度排序:O(n log n),其中 n 是区间的数量;合并:O(n),因为我们仅遍历一遍区间列表。整体时间复杂度为 O(n log n)。

空间复杂度:O(n),用于存储结果列表 merged

代码实现

package org.zyf.javabasic.letcode.hot100.ordinaryarray;

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

/**
 * @program: zyfboot-javabasic
 * @description: 合并区间
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:13
 **/
public class MergeSolution {
    public int[][] merge(int[][] intervals) {
        if (intervals == null || intervals.length == 0) {
            return new int[0][0];
        }

        // 对区间按起始位置进行排序
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        List<int[]> merged = new ArrayList<>();

        // 遍历排序后的区间列表
        for (int i = 0; i < intervals.length; i++) {
            int[] currentInterval = intervals[i];

            // 如果结果列表为空,或者当前区间与结果列表中最后一个区间不重叠,直接添加当前区间
            if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < currentInterval[0]) {
                merged.add(currentInterval);
            } else {
                // 合并区间,更新结果列表中最后一个区间的结束位置
                int[] lastMerged = merged.get(merged.size() - 1);
                lastMerged[1] = Math.max(lastMerged[1], currentInterval[1]);
            }
        }

        // 将结果列表转换为二维数组
        return merged.toArray(new int[merged.size()][]);
    }

    public static void main(String[] args) {
        MergeSolution solution = new MergeSolution();

        // 测试用例 1
        int[][] intervals1 = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
        int[][] result1 = solution.merge(intervals1);
        System.out.println(Arrays.deepToString(result1));  // 输出: [[1, 6], [8, 10], [15, 18]]

        // 测试用例 2
        int[][] intervals2 = {{1, 4}, {4, 5}};
        int[][] result2 = solution.merge(intervals2);
        System.out.println(Arrays.deepToString(result2));  // 输出: [[1, 5]]
    }
}

15.轮转数组 (中等)

题目描述

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:输入:nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100]

提示:

  • 1 <= nums.length <= 10^{5}
  • -2^{31} <= nums[i] <= 2^{31} - 1
  • 0 <= k <= 10^{5}

进阶:

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

解题思路

要将数组中的元素向右轮转 k 个位置,可以使用几种不同的方法:

  1. 使用额外空间(数组复制)

    复制原数组的部分到一个新的数组中,并将它们合并以得到结果。这种方法简单,但使用了额外的空间。
  2. 使用反转数组

    这种方法不使用额外的空间(O(1) 空间复杂度),通过数组反转实现轮转。具体步骤是:反转后 n-k 个元素;反转前 k 个元素;反转整个数组。
  3. 使用环状替换(原地算法)

    这是一种在 O(n) 时间复杂度和 O(1) 空间复杂度下的原地算法。通过循环移动元素,将每个元素放到它最终的位置。

复杂度分析

  • 方法 1:通过创建新数组并将元素放置到新位置,然后将结果复制回原数组。这种方法简单但需要额外的空间。

  • 方法 2:通过反转整个数组,然后分别反转前 k 个元素和后 n-k 个元素来实现轮转。这种方法不使用额外的空间(O(1) 空间复杂度),且时间复杂度为 O(n)。

  • 方法 3:使用环状替换算法,通过逐个移动元素到其最终位置来实现轮转。这种方法也是 O(n) 时间复杂度,并且 O(1) 空间复杂度。

代码实现

package org.zyf.javabasic.letcode.hot100.ordinaryarray;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 轮转数组​
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:22
 **/
public class RotateSolution {
    public void rotate1(int[] nums, int k) {
        int n = nums.length;
        k = k % n;  // 处理 k 大于数组长度的情况
        int[] result = new int[n];

        for (int i = 0; i < n; i++) {
            result[(i + k) % n] = nums[i];
        }

        // 复制结果数组到原数组
        System.arraycopy(result, 0, nums, 0, n);
    }

    public void rotate2(int[] nums, int k) {
        int n = nums.length;
        k = k % n;  // 处理 k 大于数组长度的情况
        reverse(nums, 0, n - 1);  // 反转整个数组
        reverse(nums, 0, k - 1);  // 反转前 k 个元素
        reverse(nums, k, n - 1);  // 反转后 n - k 个元素
    }

    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 void rotate3(int[] nums, int k) {
        int n = nums.length;
        k = k % n;  // 处理 k 大于数组长度的情况
        int count = 0;  // 记录移动的元素数量

        for (int start = 0; count < n; start++) {
            int current = start;
            int prevValue = nums[start];

            do {
                int nextIndex = (current + k) % n;
                int temp = nums[nextIndex];
                nums[nextIndex] = prevValue;
                prevValue = temp;
                current = nextIndex;
                count++;
            } while (start != current);
        }
    }

    public static void main(String[] args) {
        RotateSolution solution = new RotateSolution();

        // 测试用例 1
        int[] nums1 = {1, 2, 3, 4, 5, 6, 7};
        solution.rotate3(nums1, 3);
        System.out.println(Arrays.toString(nums1));  // 输出: [5, 6, 7, 1, 2, 3, 4]

        // 测试用例 2
        int[] nums2 = {-1, -100, 3, 99};
        solution.rotate3(nums2, 2);
        System.out.println(Arrays.toString(nums2));  // 输出: [3, 99, -1, -100]
    }
}

16.除自身以外数组的乘积 (中等)

题目描述

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

示例 1:输入: nums = [1,2,3,4] 输出: [24,12,8,6]

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

提示:

  • 2 <= nums.length <= 10^{5}
  • -30 <= nums[i] <= 30
  • 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内

进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

解题思路

要解决这个问题并且避免使用除法,我们可以利用前缀乘积和后缀乘积来计算每个位置的结果。我们可以按照以下步骤进行:

  1. 前缀乘积

    创建一个数组 prefix,其中 prefix[i] 存储的是 nums 中从 0i-1 的元素乘积;初始化 prefix[0]1,表示没有元素前缀的乘积;遍历 nums 数组,从左到右填充 prefix 数组。
  2. 后缀乘积

    创建一个变量 suffix,用于计算从 i+1 到数组末尾的乘积;遍历 nums 数组,从右到左更新结果数组 answer;对于每个 ianswer[i]prefix[i]suffix 的乘积。

这种方法能够在 O(n) 时间复杂度内完成计算,且空间复杂度为 O(1),不考虑输出数组的额外空间。

复杂度分析

  • 时间复杂度:O(n),因为我们分别进行两次线性遍历,一次计算前缀乘积,一次计算后缀乘积。
  • 空间复杂度:O(1),不考虑输出数组的空间,使用了常量空间来存储中间结果。

代码实现

package org.zyf.javabasic.letcode.hot100.ordinaryarray;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 除自身以外数组的乘积
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:31
 **/
public class ProductExceptSelfSolution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] answer = new int[n];

        // 步骤 1:计算前缀乘积
        // 初始化答案数组的第一个元素
        answer[0] = 1;

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

        // 步骤 2:计算后缀乘积并最终更新答案数组
        int suffix = 1; // 从 1 开始,表示当前元素右侧的乘积
        for (int i = n - 1; i >= 0; i--) {
            // 更新答案数组的当前元素,乘以前缀乘积和后缀乘积
            answer[i] = answer[i] * suffix;
            // 更新后缀乘积为当前元素
            suffix *= nums[i];
        }

        return answer;
    }

    public static void main(String[] args) {
        ProductExceptSelfSolution solution = new ProductExceptSelfSolution();

        // 测试用例 1
        int[] nums1 = {1, 2, 3, 4};
        int[] result1 = solution.productExceptSelf(nums1);
        System.out.println(Arrays.toString(result1));  // 输出: [24, 12, 8, 6]

        // 测试用例 2
        int[] nums2 = {-1, 1, 0, -3, 3};
        int[] result2 = solution.productExceptSelf(nums2);
        System.out.println(Arrays.toString(result2));  // 输出: [0, 0, 9, 0, 0]
    }
}

17.缺失的第一个正数 (困难)

题目描述

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

示例 1:输入:nums = [1,2,0] 输出:3 解释:范围 [1,2] 中的数字都在数组中。

示例 2:输入:nums = [3,4,-1,1] 输出:2 解释:1 在数组中,但 2 没有。

示例 3:输入:nums = [7,8,9,11,12] 输出:1 解释:最小的正数 1 没有出现。

提示:

  • 1 <= nums.length <= 10^{5}
  • -2^{31} <= nums[i] <= 2^{31} - 1

解题思路

  • 处理不合法的元素:首先,遍历数组,将所有小于等于0或大于数组长度 n 的元素标记为 n + 1,因为这些元素不影响我们寻找缺失的最小正整数。

  • 标记出现的正整数

    遍历数组,对于每个元素的绝对值 num,如果 num 在范围 [1, n] 内,说明这个数是合法的,可以作为索引来标记;使用负号标记 nums[num - 1],表示数字 num 出现在数组中。
  • 查找未出现的正整数

    再次遍历数组,找到第一个值为正的索引 i,返回 i + 1,这就是缺失的最小正整数;如果所有位置的值都是负的,说明数组中包含了 [1, n] 范围内的所有整数,因此返回 n + 1

复杂度分析

  • 时间复杂度:O(n),遍历数组三次,每次遍历的时间复杂度都是 O(n)。
  • 空间复杂度:O(1),只使用了常数级别的额外空间,修改了原数组。

代码实现

package org.zyf.javabasic.letcode.hot100.ordinaryarray;

/**
 * @program: zyfboot-javabasic
 * @description: 缺失的第一个正数
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:38
 **/
public class FirstMissingPositiveSolution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;

        // 步骤 1:处理不合法的元素
        // 将不在范围 [1, n] 内的元素标记为 n+1(超出范围的数)
        for (int i = 0; i < n; i++) {
            if (nums[i] <= 0 || nums[i] > n) {
                nums[i] = n + 1;
            }
        }

        // 步骤 2:使用原地哈希方法标记出现的正整数
        for (int i = 0; i < n; i++) {
            int num = Math.abs(nums[i]);
            if (num <= n) {
                // 标记 num 位置的值为负数,表示 num 出现过
                if (nums[num - 1] > 0) {
                    nums[num - 1] = -nums[num - 1];
                }
            }
        }

        // 步骤 3:查找第一个未出现的正整数
        for (int i = 0; i < n; i++) {
            if (nums[i] > 0) {
                return i + 1;
            }
        }

        // 如果所有位置都被标记,则返回 n+1
        return n + 1;
    }

    public static void main(String[] args) {
        FirstMissingPositiveSolution solution = new FirstMissingPositiveSolution();

        // 测试用例 1
        int[] nums1 = {1, 2, 0};
        System.out.println(solution.firstMissingPositive(nums1));  // 输出: 3

        // 测试用例 2
        int[] nums2 = {3, 4, -1, 1};
        System.out.println(solution.firstMissingPositive(nums2));  // 输出: 2

        // 测试用例 3
        int[] nums3 = {7, 8, 9, 11, 12};
        System.out.println(solution.firstMissingPositive(nums3));  // 输出: 1
    }
}

六、矩阵

18.矩阵置零 (中等)

题目描述

给定一个 m x n 的矩阵,如果一个元素为 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法

示例 1:

输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]

示例 2:

输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]

提示:

  • m == matrix.length
  • n == matrix[0].length
  • 1 <= m, n <= 200
  • -2^{31} <= matrix[i][j] <= 2^{31} - 1

进阶:

  • 一个直观的解决方案是使用  O(mn) 的额外空间,但这并不是一个好的解决方案。
  • 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
  • 你能想出一个仅使用常量空间的解决方案吗?

解题思路

  • 检查第一行和第一列:首先检查矩阵的第一行和第一列是否包含 0。这两部分特殊处理,因为它们将用作标记其他行和列的状态。

  • 标记其他行和列:遍历矩阵中除了第一行和第一列之外的所有元素。如果某个元素为 0,则将其所在的行和列的首位置为 0。即,matrix[i][0]matrix[0][j]

  • 将标记的行和列置为 0:遍历矩阵,将那些被标记为 0 的行和列中的所有元素设置为 0

  • 处理第一行和第一列:根据步骤 1 中记录的标志,处理第一行和第一列。如果第一行或第一列需要被置为 0,则逐一设置。

复杂度分析

  • 时间复杂度:O(m * n),因为每个元素最多被访问和修改两次。
  • 空间复杂度:O(1),除了输入矩阵外,不使用额外的空间。

代码实现

package org.zyf.javabasic.letcode.hot100.matrix;

/**
 * @program: zyfboot-javabasic
 * @description: 矩阵置零
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:46
 **/
public class SetZeroesSolution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;     // 获取矩阵的行数
        int n = matrix[0].length;  // 获取矩阵的列数

        // 步骤 1: 判断第一行和第一列是否需要被置为 0
        boolean firstRowZero = false;
        boolean firstColZero = false;

        // 检查第一行是否有 0
        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) {
                firstRowZero = true;
                break;
            }
        }

        // 检查第一列是否有 0
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                firstColZero = true;
                break;
            }
        }

        // 步骤 2: 使用第一行和第一列作为标记,记录其他行和列的 0
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] == 0) {
                    matrix[i][0] = 0; // 标记行
                    matrix[0][j] = 0; // 标记列
                }
            }
        }

        // 步骤 3: 根据标记将相应的行和列置为 0
        for (int i = 1; i < m; i++) {
            if (matrix[i][0] == 0) {
                for (int j = 1; j < n; j++) {
                    matrix[i][j] = 0;
                }
            }
        }

        for (int j = 1; j < n; j++) {
            if (matrix[0][j] == 0) {
                for (int i = 1; i < m; i++) {
                    matrix[i][j] = 0;
                }
            }
        }

        // 步骤 4: 根据之前的标记将第一行和第一列置为 0
        if (firstRowZero) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }

        if (firstColZero) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
    }

    public static void main(String[] args) {
        SetZeroesSolution solution = new SetZeroesSolution();

        // 测试用例 1
        int[][] matrix1 = {
                {1, 1, 1},
                {1, 0, 1},
                {1, 1, 1}
        };
        solution.setZeroes(matrix1);
        printMatrix(matrix1);  // 输出: [[1, 0, 1], [0, 0, 0], [1, 0, 1]]

        // 测试用例 2
        int[][] matrix2 = {
                {0, 1, 2, 0},
                {3, 4, 5, 2},
                {1, 3, 1, 5}
        };
        solution.setZeroes(matrix2);
        printMatrix(matrix2);  // 输出: [[0, 0, 0, 0], [0, 4, 5, 0], [0, 3, 1, 0]]
    }

    // 辅助函数:打印矩阵
    private static void printMatrix(int[][] matrix) {
        for (int[] row : matrix) {
            for (int value : row) {
                System.out.print(value + " ");
            }
            System.out.println();
        }
    }
}

19.螺旋矩阵(中等)

题目描述

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100

解题思路

  • 初始化边界:定义四个边界 topbottomleftright,它们分别代表当前矩阵的上下左右边界。初始值分别为矩阵的四个边界。

  • 遍历矩阵

    从左到右:遍历当前 top 行,将元素添加到结果列表中,然后 top 向下移动;从上到下:遍历当前 right 列,将元素添加到结果列表中,然后 right 向左移动;从右到左:如果 top 行和 bottom 行还有未处理的部分,遍历当前 bottom 行,将元素添加到结果列表中,然后 bottom 向上移动;从下到上:如果 left 列和 right 列还有未处理的部分,遍历当前 left 列,将元素添加到结果列表中,然后 left 向右移动。
  • 更新边界:每完成一个方向的遍历后,更新相应的边界值,缩小螺旋矩阵的范围。

复杂度分析

  • 时间复杂度:O(m * n),因为每个元素被访问一次。
  • 空间复杂度:O(m * n),用于存储结果列表。

代码实现

package org.zyf.javabasic.letcode.hot100.matrix;

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

/**
 * @program: zyfboot-javabasic
 * @description: 螺旋矩阵​
 * @author: zhangyanfeng
 * @create: 2024-08-21 22:53
 **/
public class SpiralOrderSolution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> result = new ArrayList<>();

        // 获取矩阵的行数和列数
        int m = matrix.length;
        int n = matrix[0].length;

        // 定义四个边界
        int top = 0, bottom = m - 1;
        int left = 0, right = n - 1;

        // 只要还有未处理的区域
        while (top <= bottom && left <= right) {
            // 从左到右遍历当前上边界
            for (int j = left; j <= right; j++) {
                result.add(matrix[top][j]);
            }
            top++; // 上边界向下移动

            // 从上到下遍历当前右边界
            for (int i = top; i <= bottom; i++) {
                result.add(matrix[i][right]);
            }
            right--; // 右边界向左移动

            // 确保当前行还有未处理的部分
            if (top <= bottom) {
                // 从右到左遍历当前下边界
                for (int j = right; j >= left; j--) {
                    result.add(matrix[bottom][j]);
                }
                bottom--; // 下边界向上移动
            }

            // 确保当前列还有未处理的部分
            if (left <= right) {
                // 从下到上遍历当前左边界
                for (int i = bottom; i >= top; i--) {
                    result.add(matrix[i][left]);
                }
                left++; // 左边界向右移动
            }
        }

        return result;
    }

    public static void main(String[] args) {
        SpiralOrderSolution solution = new SpiralOrderSolution();

        // 测试用例 1
        int[][] matrix1 = {
                {1, 2, 3},
                {4, 5, 6},
                {7, 8, 9}
        };
        System.out.println(solution.spiralOrder(matrix1)); // 输出: [1,2,3,6,9,8,7,4,5]

        // 测试用例 2
        int[][] matrix2 = {
                {1, 2, 3, 4},
                {5, 6, 7, 8},
                {9, 10, 11, 12}
        };
        System.out.println(solution.spiralOrder(matrix2)); // 输出: [1,2,3,4,8,12,11,10,9,5,6,7]
    }
}

20.旋转图像(中等)

题目描述

给定一个 × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

提示:

  • n == matrix.length == matrix[i].length
  • 1 <= n <= 20
  • -1000 <= matrix[i][j] <= 1000

解题思路

链接:https://leetcode.cn/problems/rotate-image/solutions/526980/xuan-zhuan-tu-xiang-by-leetcode-solution-vu3m/

题目中要求我们尝试在不使用额外内存空间的情况下进行矩阵的旋转,也就是说,我们需要「原地旋转」这个矩阵。那么我们如何在方法一的基础上完成原地旋转呢?

我们观察方法一中的关键等式:

它阻止了我们进行原地旋转,这是因为如果我们直接将 matrix[row][col] 放到原矩阵中的目标位置 matrix[col][n−row−1]:

原矩阵中的 matrix[col][n−row−1] 就被覆盖了!这并不是我们想要的结果。因此我们可以考虑用一个临时变量 temp 暂存 matrix[col][n−row−1] 的值,这样虽然 matrix[col][n−row−1] 被覆盖了,我们还是可以通过 temp 获取它原来的值:

那么 matrix[col][n−row−1] 经过旋转操作之后会到哪个位置呢?我们还是使用方法一中的关键等式,不过这次,我们需要将

带入关键等式,就可以得到:

同样地,直接赋值会覆盖掉 matrix[n−row−1][n−col−1] 原来的值,因此我们还是需要使用一个临时变量进行存储,不过这次,我们可以直接使用之前的临时变量 temp:

我们再重复一次之前的操作,matrix[n−row−1][n−col−1] 经过旋转操作之后会到哪个位置呢?

带入关键等式,就可以得到:

写进去:

不要灰心,再来一次!matrix[n−col−1][row] 经过旋转操作之后回到哪个位置呢?

带入关键等式,就可以得到:

我们回到了最初的起点 matrix[row][col],也就是说:

这四项处于一个循环中,并且每一项旋转后的位置就是下一项所在的位置!因此我们可以使用一个临时变量 temp 完成这四项的原地交换:

当我们知道了如何原地旋转矩阵之后,还有一个重要的问题在于:我们应该枚举哪些位置 (row,col) 进行上述的原地交换操作呢?由于每一次原地交换四个位置,因此:

当 n 为偶数时,我们需要枚举 n2/4=(n/2)×(n/2) 个位置,可以将该图形分为四块,以 4×4 的矩阵为例:

保证了不重复、不遗漏;

当 n 为奇数时,由于中心的位置经过旋转后位置不变,我们需要枚举个位置,需要换一种划分的方式,以 5×5 的矩阵为例:

同样保证了不重复、不遗漏,矩阵正中央的点无需旋转。

复杂度分析

  • 时间复杂度:O(n^2)。每一层的处理需要遍历 n 个元素,因此总体时间复杂度是 O(n^2)。
  • 空间复杂度:O(1)。只使用了常数级别的额外空间,所有操作都在原矩阵上完成。

代码实现

package org.zyf.javabasic.letcode.hot100.matrix;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 旋转图像
 * @author: zhangyanfeng
 * @create: 2024-08-21 23:21
 **/
public class RotateSolution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;

        // 遍历每一层
        for (int i = 0; i < n / 2; ++i) {
            // 每一层的元素交换
            for (int j = 0; j < (n + 1) / 2; ++j) {
                // 交换四个位置上的元素
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }
        }
    }

    public static void main(String[] args) {
        RotateSolution solution = new RotateSolution();

        // Test Case 1
        int[][] matrix1 = {
                {1, 2, 3},
                {4, 5, 6},
                {7, 8, 9}
        };
        solution.rotate(matrix1);
        printMatrix(matrix1);
        // Expected output:
        // 7 4 1
        // 8 5 2
        // 9 6 3

        // Test Case 2
        int[][] matrix2 = {
                {5, 1, 9, 11},
                {2, 4, 8, 10},
                {13, 3, 6, 7},
                {15, 14, 12, 16}
        };
        solution.rotate(matrix2);
        printMatrix(matrix2);
        // Expected output:
        // 15 13 2 5
        // 14 3 4 1
        // 12 6 8 9
        // 16 7 10 11

        // Test Case 3
        int[][] matrix3 = {
                {1, 2},
                {3, 4}
        };
        solution.rotate(matrix3);
        printMatrix(matrix3);
        // Expected output:
        // 3 1
        // 4 2
    }

    public static void printMatrix(int[][] matrix) {
        for (int[] row : matrix) {
            System.out.println(Arrays.toString(row));
        }
        System.out.println();
    }
}

21.搜索二维矩阵 II(中等)

题目描述

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

示例 1:

输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true

示例 2:

输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= n, m <= 300
  • -10^{9} <= matrix[i][j] <= 10^{9}
  • 每行的所有元素从左到右升序排列
  • 每列的所有元素从上到下升序排列
  • -10^{9} <= target <= 10^{9}

解题思路

为了高效地搜索一个具有特定性质的矩阵中的目标值,我们可以利用矩阵的排序特性来设计一个时间复杂度为 O(m+n)O(m + n)O(m+n) 的算法:从矩阵的右上角或左下角开始搜索,并根据当前元素与目标值的比较结果决定搜索的方向。算法思路:

  1. 初始化:从矩阵的右上角开始。初始化 row 为 0(矩阵的行数 - 1),col 为 0(矩阵的列数 - 1)。

  2. 搜索目标值小于当前元素:由于每列的元素是升序的,目标值在当前列的上方,因此我们可以向左移动;目标值大于当前元素:由于每行的元素是升序的,目标值在当前行的下方,因此我们可以向下移动;目标值等于当前元素:找到目标值,返回 true

  3. 终止条件:当 rowcol 超出矩阵的边界时,说明目标值不在矩阵中,返回 false

复杂度分析

  • 时间复杂度: O(m+n),其中 m 是矩阵的行数,n 是矩阵的列数。最坏情况下,每个方向(向左或向下)都只遍历一次。
  • 空间复杂度: O(1),仅使用常量级别的空间来存储变量。

代码实现

package org.zyf.javabasic.letcode.hot100.matrix;

/**
 * @program: zyfboot-javabasic
 * @description: 搜索二维矩阵 II
 * @author: zhangyanfeng
 * @create: 2024-08-21 23:31
 **/
public class SearchMatrixSolution {
    public boolean searchMatrix(int[][] matrix, int target) {
        // 获取矩阵的行数和列数
        int m = matrix.length;
        int n = matrix[0].length;

        // 从矩阵的右上角开始
        int row = 0;
        int col = n - 1;

        // 搜索矩阵
        while (row < m && col >= 0) {
            if (matrix[row][col] == target) {
                return true; // 找到目标值
            } else if (matrix[row][col] > target) {
                col--; // 向左移动
            } else {
                row++; // 向下移动
            }
        }

        // 目标值不在矩阵中
        return false;
    }

    public static void main(String[] args) {
        SearchMatrixSolution solution = new SearchMatrixSolution();

        // Test Case 1
        int[][] matrix1 = {
                {1, 4, 7, 11, 15},
                {2, 5, 8, 12, 19},
                {3, 6, 9, 16, 22},
                {10, 13, 14, 17, 24},
                {18, 21, 23, 26, 30}
        };
        int target1 = 5;
        System.out.println(solution.searchMatrix(matrix1, target1)); // Expected: true

        // Test Case 2
        int[][] matrix2 = {
                {1, 4, 7, 11, 15},
                {2, 5, 8, 12, 19},
                {3, 6, 9, 16, 22},
                {10, 13, 14, 17, 24},
                {18, 21, 23, 26, 30}
        };
        int target2 = 20;
        System.out.println(solution.searchMatrix(matrix2, target2)); // Expected: false

        // Test Case 3
        int[][] matrix3 = {
                {1, 2},
                {3, 4}
        };
        int target3 = 3;
        System.out.println(solution.searchMatrix(matrix3, target3)); // Expected: true

        // Test Case 4
        int[][] matrix4 = {
                {1}
        };
        int target4 = 0;
        System.out.println(solution.searchMatrix(matrix4, target4)); // Expected: false
    }
}

七、链表

22.相交链表(简单)

题目描述

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。

 

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。

示例 2:

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 10^{4}
  • 1 <= Node.val <= 10^{5}
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listA 和 listB 没有交点,intersectVal 为 0
  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]

进阶:你能否设计一个时间复杂度 O(m + n) 、仅用 O(1) 内存的解决方案?

解题思路

要解决两个单链表相交问题,并找到它们的第一个交点,我们可以使用双指针法

  • 使用两个指针 pApB 分别指向链表 headAheadB 的头节点。
  • 遍历两个链表,逐步移动指针。具体而言,如果指针 pA 到达链表 headA 的末尾,则将其重新指向 headB;同理,指针 pB 到达链表 headB 的末尾,则将其重新指向 headA
  • 这样,当 pApB 再次遍历到相交的节点时,它们将同步到相同的节点。
  • 如果两个链表有交点,最终这两个指针将相遇于交点;如果没有交点,它们将最终都变为 null,并结束循环。

复杂度分析

  • 时间复杂度: O(m+n),每个指针 pApB 都遍历每个链表一次。
  • 空间复杂度: O(1),只使用了常量级别的额外空间来存储指针。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 相交链表​
 * @author: zhangyanfeng
 * @create: 2024-08-21 23:43
 **/
public class GetIntersectionNodeSolution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 如果任何一个链表为空,直接返回 null
        if (headA == null || headB == null) {
            return null;
        }

        // 初始化两个指针
        ListNode pA = headA;
        ListNode pB = headB;

        // 当两个指针不相等时继续遍历
        while (pA != pB) {
            // 遇到链表末尾时,切换到另一链表
            pA = (pA == null) ? headB : pA.next;
            pB = (pB == null) ? headA : pB.next;
        }

        // 返回相交的节点或者 null
        return pA;
    }

    public static void main(String[] args) {
        GetIntersectionNodeSolution solution = new GetIntersectionNodeSolution();

        // 创建链表节点
        ListNode common = new ListNode(8);
        common.next = new ListNode(4);
        common.next.next = new ListNode(5);

        ListNode headA = new ListNode(4);
        headA.next = new ListNode(1);
        headA.next.next = common;

        ListNode headB = new ListNode(5);
        headB.next = new ListNode(6);
        headB.next.next = new ListNode(1);
        headB.next.next.next = common;

        // Test Case 1: 链表有交点
        ListNode result1 = solution.getIntersectionNode(headA, headB);
        System.out.println(result1 != null ? result1.val : "null"); // Expected: 8

        // Test Case 2: 链表没有交点
        ListNode headC = new ListNode(2);
        headC.next = new ListNode(6);
        headC.next.next = new ListNode(4);

        ListNode headD = new ListNode(1);
        headD.next = new ListNode(5);

        ListNode result2 = solution.getIntersectionNode(headC, headD);
        System.out.println(result2 != null ? result2.val : "null"); // Expected: null
    }
}

23.反转链表(简单)

题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

输入:head = [1,2]
输出:[2,1]

示例 3:输入:head = [] 输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

解题思路

反转单链表是一道经典的链表操作题目。可以使用两种主要的方法来实现:迭代和递归。

1. 迭代方法思路

  • 使用三个指针来反转链表:prev(前一个节点),curr(当前节点),和 next(下一个节点)。
  • 遍历链表,将当前节点的 next 指针指向前一个节点,更新 prevcurr 指针,直到遍历完成。

2. 递归方法思路

  • 递归处理链表的尾部,并将每个节点的 next 指针指向当前节点,从而实现反转。
  • 基本的递归策略是:反转链表的其余部分,然后将当前节点追加到反转链表的尾部。

复杂度分析

1. 迭代方法复杂度

  • 时间复杂度: O(n)O(n)O(n),其中 nnn 是链表的节点数,每个节点仅遍历一次。
  • 空间复杂度: O(1)O(1)O(1),仅使用常量级别的额外空间。

2. 递归方法思路复杂度

  • 时间复杂度: O(n)O(n)O(n),其中 nnn 是链表的节点数,每个节点仅处理一次。
  • 空间复杂度: O(n)O(n)O(n),递归调用栈的空间复杂度为 O(n)O(n)O(n)。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 反转链表​
 * @author: zhangyanfeng
 * @create: 2024-08-21 23:54
 **/
public class ReverseListSolution {
    public ListNode reverseList1(ListNode head) {
        ListNode prev = null; // 前一个节点
        ListNode curr = head; // 当前节点

        while (curr != null) {
            ListNode next = curr.next; // 保存下一个节点
            curr.next = prev; // 反转当前节点的指针
            prev = curr; // 更新前一个节点
            curr = next; // 移动到下一个节点
        }

        return prev; // 返回新头节点
    }

    public ListNode reverseList2(ListNode head) {
        // 递归基准条件:链表为空或只有一个节点
        if (head == null || head.next == null) {
            return head;
        }

        // 递归反转链表的剩余部分
        ListNode newHead = reverseList2(head.next);

        // 反转当前节点和下一个节点
        head.next.next = head;
        head.next = null;

        return newHead; // 返回新的头节点
    }

    public static void main(String[] args) {
        ReverseListSolution solution = new ReverseListSolution();

        // 测试用例 1: 普通链表
        System.out.println("Test Case 1: [1, 2, 3, 4, 5]");
        ListNode head1 = new ListNode(1);
        head1.next = new ListNode(2);
        head1.next.next = new ListNode(3);
        head1.next.next.next = new ListNode(4);
        head1.next.next.next.next = new ListNode(5);
        ListNode result1 = solution.reverseList1(head1);
        printList(result1);

        // 测试用例 2: 两个节点的链表
        System.out.println("Test Case 2: [1, 2]");
        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);
        ListNode result2 = solution.reverseList1(head2);
        printList(result2);

        // 测试用例 3: 空链表
        System.out.println("Test Case 3: []");
        ListNode head3 = null;
        ListNode result3 = solution.reverseList2(head3);
        printList(result3);

        // 测试用例 4: 单节点链表
        System.out.println("Test Case 4: [1]");
        ListNode head4 = new ListNode(1);
        ListNode result4 = solution.reverseList2(head4);
        printList(result4);
    }

    // 打印链表的方法
    public static void printList(ListNode head) {
        if (head == null) {
            System.out.println("null");
            return;
        }
        ListNode curr = head;
        while (curr != null) {
            System.out.print(curr.val + " ");
            curr = curr.next;
        }
        System.out.println();
    }
}

24.回文链表(简单)

题目描述

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。

示例 1:

输入:head = [1,2,2,1]
输出:true

示例 2:

输入:head = [1,2]
输出:false

提示:

  • 链表中节点数目在范围[1, 105] 内
  • 0 <= Node.val <= 9

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

解题思路

  • 快慢指针找到中间节点: 使用快慢指针方法,快指针每次移动两个节点,慢指针每次移动一个节点。当快指针到达链表末尾时,慢指针正好处于链表的中间节点。

  • 反转链表的后半部分: 从中间节点开始,反转链表的后半部分。这一步可以用来比较链表的前半部分和反转后的后半部分。

  • 比较两个半部分: 比较链表的前半部分和反转后的后半部分。如果它们相同,那么链表是回文的。

  • 恢复链表的原始状态: 为了保持链表的原始结构,可以在比较完成后再次反转链表的后半部分,恢复链表的结构。

复杂度分析

  • 时间复杂度: O(n)。需要遍历链表几次,每次遍历的时间复杂度是 O(n)。
  • 空间复杂度: O(1)。只使用了常数级的额外空间(指针变量),没有使用额外的存储结构。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 回文链表
 * @author: zhangyanfeng
 * @create: 2024-08-22 00:03
 **/
public class PalindromeSolution {
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true; // 空链表或只有一个节点的链表是回文的
        }

        // 快慢指针找到链表的中间
        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        // 反转链表的后半部分
        ListNode secondHalf = reverseList(slow);
        ListNode firstHalf = head;

        // 比较前半部分和后半部分
        while (secondHalf != null) {
            if (firstHalf.val != secondHalf.val) {
                return false;
            }
            firstHalf = firstHalf.next;
            secondHalf = secondHalf.next;
        }

        // 恢复链表的原始状态
        reverseList(slow);

        return true;
    }

    // 反转链表的函数
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    public static void main(String[] args) {
        PalindromeSolution solution = new PalindromeSolution();

        // 测试用例 1: 回文链表
        System.out.println("Test Case 1: [1, 2, 2, 1]");
        ListNode head1 = new ListNode(1);
        head1.next = new ListNode(2);
        head1.next.next = new ListNode(2);
        head1.next.next.next = new ListNode(1);
        System.out.println("Is palindrome: " + solution.isPalindrome(head1));

        // 测试用例 2: 非回文链表
        System.out.println("Test Case 2: [1, 2]");
        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);
        System.out.println("Is palindrome: " + solution.isPalindrome(head2));

        // 测试用例 3: 单节点链表
        System.out.println("Test Case 3: [1]");
        ListNode head3 = new ListNode(1);
        System.out.println("Is palindrome: " + solution.isPalindrome(head3));

        // 测试用例 4: 空链表
        System.out.println("Test Case 4: []");
        ListNode head4 = null;
        System.out.println("Is palindrome: " + solution.isPalindrome(head4));
    }
}

25.环形链表(简单)

题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos 为 -1 或者链表中的一个 有效索引 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

解题思路

判断链表中是否有环,可以使用一种高效的算法,即 Floyd 的兔子和乌龟算法(也叫做 快慢指针算法),可以在 O(n) 时间复杂度和 O(1) 空间复杂度下解决问题:

  1. 使用快慢指针:

    初始化两个指针,slowfastslow 每次移动一个节点,fast 每次移动两个节点;如果链表中存在环,那么 slowfast 会在环中相遇;如果链表中没有环,fast 指针会到达链表末尾(即 fastfast.next 变为 null)。
  2. 过程:

    初始化 slowfast 指针都指向链表的头节点;遍历链表,移动指针:fast 移动到下两个节点。slow 移动到下一个节点。如果 slowfast 指针在某一时刻相遇,链表中存在环。如果 fast 指针到达链表的末尾,链表中没有环。

复杂度分析

  • 时间复杂度: O(n)。每个节点最多访问两次,分别是快指针和慢指针的访问。
  • 空间复杂度: O(1)。只使用了常量级别的额外空间(两个指针)。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 环形链表​
 * @author: zhangyanfeng
 * @create: 2024-08-22 00:12
 **/
public class HasCycleSolution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false; // 空链表或只有一个节点的链表不可能有环
        }

        ListNode slow = head;
        ListNode fast = head;

        while (fast != null && fast.next != null) {
            slow = slow.next; // 慢指针每次移动一个节点
            fast = fast.next.next; // 快指针每次移动两个节点

            if (slow == fast) { // 快慢指针相遇,说明链表有环
                return true;
            }
        }

        return false; // 快指针到达链表末尾,说明链表没有环
    }

    public static void main(String[] args) {
        HasCycleSolution solution = new HasCycleSolution();

        // 测试用例 1: 有环的链表
        ListNode head1 = new ListNode(3);
        head1.next = new ListNode(2);
        head1.next.next = new ListNode(0);
        head1.next.next.next = new ListNode(-4);
        head1.next.next.next.next = head1.next; // 尾部连接到第二个节点
        System.out.println("Test Case 1: " + solution.hasCycle(head1)); // 应该输出 true

        // 测试用例 2: 有环的链表
        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);
        head2.next.next = head2; // 尾部连接到第一个节点
        System.out.println("Test Case 2: " + solution.hasCycle(head2)); // 应该输出 true

        // 测试用例 3: 无环的链表
        ListNode head3 = new ListNode(1);
        System.out.println("Test Case 3: " + solution.hasCycle(head3)); // 应该输出 false
    }
}

26. 环形链表 II(中等)

题目描述

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104] 内
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶:你是否可以使用 O(1) 空间解决此题?

解题思路

要找出链表中环的起始节点,可以使用 Floyd 的循环检测算法(也称为 快慢指针算法),并在此基础上进一步寻找环的起始节点。解题思路

  1. 检测环的存在:使用快慢指针法(兔子和乌龟算法)来检测链表是否存在环。快指针每次移动两个节点,慢指针每次移动一个节点。如果链表中存在环,那么快慢指针会在环中相遇。

  2. 找到环的起始节点:当快慢指针相遇时,将慢指针移回链表的头部,同时保持快指针在相遇点,二者都以相同的速度(每次移动一个节点)继续移动。它们会在环的起始节点相遇。

复杂度分析

  • 时间复杂度O(n): 快指针和慢指针各自遍历链表的时间复杂度为 O(n)。
  • 空间复杂度O(1): 只使用了常量级别的额外空间(两个指针)。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 环形链表 II​
 * @author: zhangyanfeng
 * @create: 2024-08-22 00:17
 **/
public class DetectCycleSolution {
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null; // 空链表或只有一个节点的链表没有环
        }

        ListNode slow = head;
        ListNode fast = head;

        // 阶段 1: 检测是否有环
        while (fast != null && fast.next != null) {
            slow = slow.next; // 慢指针每次移动一个节点
            fast = fast.next.next; // 快指针每次移动两个节点

            if (slow == fast) { // 快慢指针相遇,说明链表有环
                // 阶段 2: 找到环的起始节点
                ListNode entry = head; // 从头节点开始
                while (entry != slow) { // entry 和 slow 都以相同速度移动
                    entry = entry.next;
                    slow = slow.next;
                }
                return entry; // entry 即为环的起始节点
            }
        }

        return null; // 无环
    }

    public static void main(String[] args) {
        DetectCycleSolution solution = new DetectCycleSolution();

        // 测试用例 1: 有环的链表
        ListNode head1 = new ListNode(3);
        head1.next = new ListNode(2);
        head1.next.next = new ListNode(0);
        head1.next.next.next = new ListNode(-4);
        head1.next.next.next.next = head1.next; // 尾部连接到第二个节点
        System.out.println("Test Case 1: " + (solution.detectCycle(head1) != null ? solution.detectCycle(head1).val : "null")); // 应该输出 2

        // 测试用例 2: 有环的链表
        ListNode head2 = new ListNode(1);
        head2.next = new ListNode(2);
        head2.next.next = head2; // 尾部连接到第一个节点
        System.out.println("Test Case 2: " + (solution.detectCycle(head2) != null ? solution.detectCycle(head2).val : "null")); // 应该输出 1

        // 测试用例 3: 无环的链表
        ListNode head3 = new ListNode(1);
        System.out.println("Test Case 3: " + (solution.detectCycle(head3) != null ? solution.detectCycle(head3).val : "null")); // 应该输出 null
    }
}

27.合并两个有序链表(简单)

题目描述

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:输入:l1 = [], l2 = [] 输出:[]

示例 3:输入:l1 = [], l2 = [0] 输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1 和 l2 均按 非递减顺序 排列

解题思路

要将两个升序链表合并为一个新的升序链表,我们可以使用双指针法来逐个比较两个链表的节点,然后将较小的节点添加到结果链表中。

  1. 初始化:创建一个哨兵节点 (dummy),它的 next 指向合并后的链表的头节点。这样可以避免处理链表头部特殊情况。创建一个指针 current,指向 dummy

  2. 遍历两个链表:比较 l1l2 的当前节点值,将较小的那个节点添加到 currentnext,并移动相应链表的指针;current 也相应地向前移动。

  3. 处理剩余节点:当一个链表遍历完后,将另一个链表的剩余部分直接连接到 currentnext

  4. 返回结果:返回 dummy.next,这是合并后链表的头节点。

复杂度分析

  • 时间复杂度O(m + n): mn 分别是两个链表的长度,我们只需遍历一次所有节点。
  • 空间复杂度O(1): 只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 合并两个有序链表​
 * @author: zhangyanfeng
 * @create: 2024-08-22 09:49
 **/
public class MergeTwoListsSolution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 哨兵节点,方便返回结果
        ListNode dummy = new ListNode(-1);
        ListNode current = dummy;

        // 遍历两个链表
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }

        // 处理剩余的节点
        if (l1 != null) {
            current.next = l1;
        } else {
            current.next = l2;
        }

        return dummy.next;
    }

    public static void main(String[] args) {
        MergeTwoListsSolution solution = new MergeTwoListsSolution();

        // 测试用例 1
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(4);

        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(3);
        l2.next.next = new ListNode(4);

        ListNode mergedList = solution.mergeTwoLists(l1, l2);
        printList(mergedList); // 应该输出 [1, 1, 2, 3, 4, 4]

        // 测试用例 2
        ListNode l3 = null;
        ListNode l4 = null;
        ListNode mergedList2 = solution.mergeTwoLists(l3, l4);
        printList(mergedList2); // 应该输出 []

        // 测试用例 3
        ListNode l5 = null;
        ListNode l6 = new ListNode(0);
        ListNode mergedList3 = solution.mergeTwoLists(l5, l6);
        printList(mergedList3); // 应该输出 [0]
    }

    // 打印链表的辅助函数
    private static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " ");
            node = node.next;
        }
        System.out.println();
    }
}

28.两数相加(中等)

题目描述

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:输入:l1 = [0], l2 = [0] 输出:[0]

示例 3:输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出:[8,9,9,9,0,0,0,1]

提示:

  • 每个链表中的节点数在范围 [1, 100] 内
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

解题思路

要将两个逆序存储的链表表示的非负整数相加,并返回一个新的链表表示它们的和,可以逐位相加,处理进位问题。每一位的加法要考虑两个链表当前节点的值以及前一位的进位。

  1. 初始化:创建一个哨兵节点 (dummy),用于构建结果链表;创建一个指针 current 指向 dummy;初始化进位 carry 为 0。

  2. 逐位相加:遍历两个链表,直到所有节点都处理完;对应位置的值相加,再加上 carry;计算当前位的和以及新的进位 (sum = l1.val + l2.val + carry,新的节点值为 sum % 10,新的 carrysum / 10);将计算出的节点值添加到结果链表中。

  3. 处理剩余进位:如果最终 carry 不为 0,则需要在结果链表末尾添加一个新节点表示进位。

  4. 返回结果:返回 dummy.next,即结果链表的头节点。

复杂度分析

  • 时间复杂度O(max(m, n)): mn 是两个链表的长度,需要遍历较长的链表长度。
  • 空间复杂度O(max(m, n)): 结果链表的长度最多为较长链表的长度再加 1。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 两数相加
 * @author: zhangyanfeng
 * @create: 2024-08-22 09:55
 **/
public class AddTwoNumbersSolution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        // 哨兵节点,方便返回结果链表
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        int carry = 0;

        // 遍历两个链表
        while (l1 != null || l2 != null) {
            int x = (l1 != null) ? l1.val : 0;
            int y = (l2 != null) ? l2.val : 0;
            int sum = carry + x + y;
            carry = sum / 10;
            current.next = new ListNode(sum % 10);
            current = current.next;

            if (l1 != null) l1 = l1.next;
            if (l2 != null) l2 = l2.next;
        }

        // 处理进位
        if (carry > 0) {
            current.next = new ListNode(carry);
        }

        return dummy.next;
    }

    public static void main(String[] args) {
        AddTwoNumbersSolution solution = new AddTwoNumbersSolution();

        // 测试用例 1
        ListNode l1 = new ListNode(2);
        l1.next = new ListNode(4);
        l1.next.next = new ListNode(3);

        ListNode l2 = new ListNode(5);
        l2.next = new ListNode(6);
        l2.next.next = new ListNode(4);

        ListNode result = solution.addTwoNumbers(l1, l2);
        printList(result); // 输出应该为 [7, 0, 8]

        // 测试用例 2
        ListNode l3 = new ListNode(0);
        ListNode l4 = new ListNode(0);
        ListNode result2 = solution.addTwoNumbers(l3, l4);
        printList(result2); // 输出应该为 [0]

        // 测试用例 3
        ListNode l5 = new ListNode(9);
        l5.next = new ListNode(9);
        l5.next.next = new ListNode(9);
        l5.next.next.next = new ListNode(9);
        l5.next.next.next.next = new ListNode(9);
        l5.next.next.next.next.next = new ListNode(9);
        l5.next.next.next.next.next.next = new ListNode(9);

        ListNode l6 = new ListNode(9);
        l6.next = new ListNode(9);
        l6.next.next = new ListNode(9);
        l6.next.next.next = new ListNode(9);

        ListNode result3 = solution.addTwoNumbers(l5, l6);
        printList(result3); // 输出应该为 [8, 9, 9, 9, 0, 0, 0, 1]
    }

    // 打印链表的辅助函数
    private static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " ");
            node = node.next;
        }
        System.out.println();
    }
}

29.删除链表的倒数第 N 个结点(中等)

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:输入:head = [1], n = 1 输出:[]

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

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

进阶:你能尝试使用一趟扫描实现吗?

解题思路

要删除链表中的倒数第 n 个节点,进阶要求使用一趟扫描来实现。可以使用双指针法(快慢指针)来完成这个任务。

  1. 初始化双指针:使用两个指针 fastslow,都指向链表的头节点。

  2. 移动快指针:将 fast 指针先向前移动 n 步,这样 fastslow 之间就会有 n 的差距。

  3. 同时移动快慢指针:然后同时移动 fastslow 指针,直到 fast 到达链表的末尾;此时,slow 指针正好停在要删除的节点的前一个节点上。

  4. 删除节点:修改 slow.next 指针,跳过需要删除的节点。

  5. 返回链表头:如果删除的是头节点,需要特别处理,直接返回 head.next

复杂度分析

  • 时间复杂度O(sz): 其中 sz 是链表的长度,只需一次遍历。
  • 空间复杂度O(1): 只使用了常数个额外的指针。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 删除链表的倒数第 N 个结点(中等)  ​
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:01
 **/
public class RemoveNthFromEndSolution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 创建一个哨兵节点,以应对删除头节点的情况
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;

        // 快指针先移动 n 步
        for (int i = 0; i < n; i++) {
            fast = fast.next;
        }

        // 快慢指针一起移动,直到快指针到达链表末尾
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }

        // 删除慢指针所指节点的下一个节点
        slow.next = slow.next.next;

        // 返回链表头节点
        return dummy.next;
    }

    public static void main(String[] args) {
        RemoveNthFromEndSolution solution = new RemoveNthFromEndSolution();

        // 测试用例 1
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(3);
        l1.next.next.next = new ListNode(4);
        l1.next.next.next.next = new ListNode(5);
        ListNode result1 = solution.removeNthFromEnd(l1, 2);
        printList(result1); // 输出应该为 [1, 2, 3, 5]

        // 测试用例 2
        ListNode l2 = new ListNode(1);
        ListNode result2 = solution.removeNthFromEnd(l2, 1);
        printList(result2); // 输出应该为 []

        // 测试用例 3
        ListNode l3 = new ListNode(1);
        l3.next = new ListNode(2);
        ListNode result3 = solution.removeNthFromEnd(l3, 1);
        printList(result3); // 输出应该为 [1]
    }

    // 打印链表的辅助函数
    private static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " ");
            node = node.next;
        }
        System.out.println();
    }
}

30.两两交换链表中的节点(中等)

题目描述

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:输入:head = [] 输出:[]

示例 3:输入:head = [1] 输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100] 内
  • 0 <= Node.val <= 100

解题思路

要实现两两交换链表中的相邻节点,可以使用迭代的方法,借助指针操作来完成节点的交换。这里我们不修改节点的值,只通过调整节点之间的连接顺序来达到目的。

  1. 使用虚拟头节点:为了方便处理链表头节点的特殊情况,我们可以创建一个虚拟头节点 dummy,其 next 指向原链表的头节点。

  2. 迭代交换节点:我们使用三个指针 prevfirstsecond 来指向要交换的节点及其前驱节点;通过调整指针的连接顺序来交换 firstsecond 节点的位置;然后将 prev 移动到 first 的位置,继续处理下一个节点对。

  3. 返回新头节点:最后返回 dummy.next 作为新的头节点。

复杂度分析

  • 时间复杂度O(n): 其中 n 是链表的节点数。我们只遍历了一次链表。
  • 空间复杂度O(1): 只使用了常数个指针。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 两两交换链表中的节点(中等)  ​
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:07
 **/
public class SwapPairsSolution {
    public ListNode swapPairs(ListNode head) {
        // 创建一个虚拟头节点
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        // 初始化指针
        ListNode prev = dummy;

        // 迭代地交换相邻节点
        while (prev.next != null && prev.next.next != null) {
            ListNode first = prev.next;
            ListNode second = first.next;

            // 调整指针以交换节点
            first.next = second.next;
            second.next = first;
            prev.next = second;

            // 移动 prev 指针到下一个要处理的节点对
            prev = first;
        }

        // 返回新链表的头节点
        return dummy.next;
    }

    public static void main(String[] args) {
        SwapPairsSolution solution = new SwapPairsSolution();

        // 测试用例 1
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(3);
        l1.next.next.next = new ListNode(4);
        ListNode result1 = solution.swapPairs(l1);
        printList(result1); // 输出应该为 [2, 1, 4, 3]

        // 测试用例 2
        ListNode l2 = new ListNode(1);
        ListNode result2 = solution.swapPairs(l2);
        printList(result2); // 输出应该为 [1]

        // 测试用例 3
        ListNode result3 = solution.swapPairs(null);
        printList(result3); // 输出应该为 []
    }

    // 打印链表的辅助函数
    private static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " ");
            node = node.next;
        }
        System.out.println();
    }
}

31.K 个一组翻转链表 (困难)

题目描述

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

示例 2:

输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

解题思路

要解决每 k 个节点一组进行翻转的链表问题,我们可以按照以下步骤实现:

  1. 找到每组的起始节点:我们从头节点开始,遍历链表,找到每 k 个节点作为一组需要翻转的部分。

  2. 翻转每组节点:对于每一组 k 个节点,我们将它们翻转。可以借助一个辅助函数来实现链表的一部分翻转。

  3. 处理剩余节点:如果最后剩余的节点数量不足 k,则保持其原有顺序不动。

  4. 链接翻转后的部分:在翻转每一组的过程中,注意将前一组翻转后的最后一个节点连接到当前组翻转后的第一个节点上。

复杂度分析

  • 时间复杂度O(n):其中 n 是链表的节点数。我们遍历了链表,且每个节点只处理了一次。
  • 空间复杂度O(1):我们只使用了常量空间来存储指针。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: K 个一组翻转链表困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:14
 **/
public class ReverseKGroupSolution {
    public ListNode reverseKGroup(ListNode head, int k) {
        if (head == null || k == 1) return head;

        // 创建虚拟头节点
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode prevGroupEnd = dummy;

        while (true) {
            ListNode groupStart = prevGroupEnd.next;
            ListNode groupEnd = prevGroupEnd;

            // 找到当前组的最后一个节点
            for (int i = 0; i < k; i++) {
                groupEnd = groupEnd.next;
                if (groupEnd == null) {
                    return dummy.next; // 如果节点数不足 k,返回结果
                }
            }

            // 记录下一组的起始节点
            ListNode nextGroupStart = groupEnd.next;

            // 翻转当前组
            reverse(groupStart, groupEnd);

            // 连接前一组和当前组
            prevGroupEnd.next = groupEnd;
            groupStart.next = nextGroupStart;

            // 更新 prevGroupEnd 为当前组的最后一个节点(翻转后即 groupStart)
            prevGroupEnd = groupStart;
        }
    }

    // 翻转链表部分区域
    private void reverse(ListNode start, ListNode end) {
        ListNode prev = null;
        ListNode curr = start;
        ListNode next = null;

        while (prev != end) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
    }

    public static void main(String[] args) {
        ReverseKGroupSolution solution = new ReverseKGroupSolution();

        // 测试用例 1
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(3);
        l1.next.next.next = new ListNode(4);
        l1.next.next.next.next = new ListNode(5);
        ListNode result1 = solution.reverseKGroup(l1, 2);
        printList(result1); // 输出应该为 [2, 1, 4, 3, 5]

        // 测试用例 2
        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(2);
        l2.next.next = new ListNode(3);
        l2.next.next.next = new ListNode(4);
        l2.next.next.next.next = new ListNode(5);
        ListNode result2 = solution.reverseKGroup(l2, 3);
        printList(result2); // 输出应该为 [3, 2, 1, 4, 5]
    }

    // 打印链表的辅助函数
    private static void printList(ListNode node) {
        while (node != null) {
            System.out.print(node.val + " ");
            node = node.next;
        }
        System.out.println();
    }
}

32.随机链表的复制(中等)

题目描述

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

提示:

  • 0 <= n <= 1000
  • -104 <= Node.val <= 104
  • Node.random 为 null 或指向链表中的节点。

解题思路

要实现一个带有随机指针的链表的深拷贝,我们可以使用一个三步法来确保新链表节点的 nextrandom 指针正确指向。

  1. 复制每个节点并将其插入到原节点之后:我们遍历原链表,对于每一个节点,我们创建一个新的节点,并将其插入到当前节点的 next 位置。这样新节点会紧随其原节点之后。

  2. 设置新节点的 random 指针:遍历链表,设置每个新节点的 random 指针。因为新节点紧随其原节点,所以新节点的 random 指针可以通过 oldNode.random.next 得到。

  3. 将新链表从原链表中分离出来:最后,我们再一次遍历链表,将新节点从旧节点中分离出来形成新的链表。

复杂度分析

  • 时间复杂度O(n):我们对链表进行了三次遍历。
  • 空间复杂度O(1):我们没有使用额外的空间,只是利用了原链表的节点进行操作。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

/**
 * @program: zyfboot-javabasic
 * @description: 随机链表的复制(中等)  ​
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:20
 **/
public class CopyRandomListSolution {
    static class Node {
        int val;
        Node next;
        Node random;

        public Node(int val) {
            this.val = val;
            this.next = null;
            this.random = null;
        }
    }

    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }

        // Step 1: 在每个节点后面创建一个新的节点,并插入链表
        Node curr = head;
        while (curr != null) {
            Node newNode = new Node(curr.val);
            newNode.next = curr.next;
            curr.next = newNode;
            curr = newNode.next;
        }

        // Step 2: 复制 random 指针
        curr = head;
        while (curr != null) {
            if (curr.random != null) {
                curr.next.random = curr.random.next;
            }
            curr = curr.next.next;
        }

        // Step 3: 分离两个链表
        curr = head;
        Node newHead = head.next;
        Node copy = newHead;

        while (curr != null) {
            curr.next = curr.next.next;
            if (copy.next != null) {
                copy.next = copy.next.next;
            }
            curr = curr.next;
            copy = copy.next;
        }

        return newHead;
    }

    public static void main(String[] args) {
        CopyRandomListSolution solution = new CopyRandomListSolution();

        // 创建测试用例链表 [[7,null],[13,0],[11,4],[10,2],[1,0]]
        Node node1 = new Node(7);
        Node node2 = new Node(13);
        Node node3 = new Node(11);
        Node node4 = new Node(10);
        Node node5 = new Node(1);

        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;

        node2.random = node1;
        node3.random = node5;
        node4.random = node3;
        node5.random = node1;

        Node result = solution.copyRandomList(node1);

        // 打印结果链表
        Node current = result;
        while (current != null) {
            System.out.print("[" + current.val + ", ");
            if (current.random != null) {
                System.out.print(current.random.val);
            } else {
                System.out.print("null");
            }
            System.out.print("] ");
            current = current.next;
        }
    }
}

33.排序链表(中等)

题目描述

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

示例 1:

输入:head = [4,2,1,3]
输出:[1,2,3,4]

示例 2:

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:输入:head = [] 输出:[]

提示:

  • 链表中节点的数目在范围 [0, 5 * 104] 内
  • -105 <= Node.val <= 105

进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

解题思路

要对链表进行排序,并且满足 O(n log n) 的时间复杂度,最合适的算法是归并排序(Merge Sort)。归并排序适合链表的排序,因为它能在 O(1) 的空间复杂度下完成,而数组的归并排序需要额外的 O(n) 空间复杂度。

  1. 找到链表的中间节点:使用快慢指针(快指针一次走两步,慢指针一次走一步),当快指针到达末尾时,慢指针就在链表的中间位置。
  2. 递归地对链表的两部分进行排序:将链表分成两部分,分别对这两部分递归地进行排序。
  3. 合并两个有序链表:使用合并两个有序链表的技巧,将两个已经排序的链表合并成一个有序的链表。

复杂度分析

  • 时间复杂度O(n log n):归并排序的时间复杂度。
  • 空间复杂度O(log n):由于递归调用栈的深度为 log n。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

/**
 * @program: zyfboot-javabasic
 * @description: 排序链表(中等)  ​
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:26
 **/
public class SortListSolution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        // Step 1: 找到链表的中间节点,并将链表分成两部分
        ListNode mid = getMid(head);
        ListNode left = head;
        ListNode right = mid.next;
        mid.next = null;

        // Step 2: 递归地对两部分排序
        left = sortList(left);
        right = sortList(right);

        // Step 3: 合并两部分
        return merge(left, right);
    }

    // 使用快慢指针找链表的中间节点
    private ListNode getMid(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 合并两个有序链表
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                tail.next = l1;
                l1 = l1.next;
            } else {
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        tail.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }

    public static void main(String[] args) {
        SortListSolution solution = new SortListSolution();

        // 测试用例 1
        ListNode node1 = new ListNode(4);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(1);
        ListNode node4 = new ListNode(3);
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        ListNode sortedList1 = solution.sortList(node1);
        printList(sortedList1); // 输出:[1, 2, 3, 4]

        // 测试用例 2
        ListNode node5 = new ListNode(-1);
        ListNode node6 = new ListNode(5);
        ListNode node7 = new ListNode(3);
        ListNode node8 = new ListNode(4);
        ListNode node9 = new ListNode(0);
        node5.next = node6;
        node6.next = node7;
        node7.next = node8;
        node8.next = node9;
        ListNode sortedList2 = solution.sortList(node5);
        printList(sortedList2); // 输出:[-1, 0, 3, 4, 5]

        // 测试用例 3
        ListNode sortedList3 = solution.sortList(null);
        printList(sortedList3); // 输出:[]
    }

    // 打印链表
    public static void printList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " ");
            head = head.next;
        }
        System.out.println();
    }
}

34.合并 K 个升序链表 (困难)

题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例 2:输入:lists = [] 输出:[]

示例 3:输入:lists = [[]] 输出:[]

提示:

  • k == lists.length
  • 0 <= k <= 10^4
  • 0 <= lists[i].length <= 500
  • -10^4 <= lists[i][j] <= 10^4
  • lists[i] 按 升序 排列
  • lists[i].length 的总和不超过 10^4

解题思路

要将多个有序链表合并成一个有序链表,可以使用优先队列(最小堆)来实现,这样能够有效地将多个链表中的最小节点逐步合并,最终形成一个有序的链表。

  1. 使用优先队列来存储每个链表的头节点。优先队列能够保证每次从多个链表中取出的节点都是当前最小的节点。
  2. 将每个链表的头节点加入优先队列。
  3. 每次从优先队列中取出最小节点,并将该节点的下一个节点加入优先队列。
  4. 重复上述步骤,直到所有节点都被处理完。

复杂度分析

  • 时间复杂度:对于每个节点的插入和删除操作,优先队列的时间复杂度为 O(log k),其中 k 是链表的数量。总的时间复杂度为 O(N log k),其中 N 是所有节点的总数。

  • 空间复杂度:空间复杂度主要取决于优先队列的存储空间,最坏情况下为 O(k)。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import org.zyf.javabasic.letcode.list.base.ListNode;

import java.util.PriorityQueue;

/**
 * @program: zyfboot-javabasic
 * @description: 合并 K 个升序链表 (困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:32
 **/
public class MergeKListsSolution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }

        // 创建优先队列,并指定排序规则为节点值的升序
        PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);

        // 将每个链表的头节点加入优先队列
        for (ListNode list : lists) {
            if (list != null) {
                pq.offer(list);
            }
        }

        // 创建一个哨兵节点,方便操作
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;

        // 逐步从优先队列中取出最小节点,加入结果链表
        while (!pq.isEmpty()) {
            ListNode minNode = pq.poll();
            current.next = minNode;
            current = current.next;

            // 如果最小节点有下一个节点,将其加入优先队列
            if (minNode.next != null) {
                pq.offer(minNode.next);
            }
        }

        return dummy.next;
    }

    public static void main(String[] args) {
        MergeKListsSolution solution = new MergeKListsSolution();

        // 示例 1
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(4);
        l1.next.next = new ListNode(5);

        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(3);
        l2.next.next = new ListNode(4);

        ListNode l3 = new ListNode(2);
        l3.next = new ListNode(6);

        ListNode[] lists = {l1, l2, l3};
        ListNode mergedList = solution.mergeKLists(lists);
        printList(mergedList); // 输出:[1, 1, 2, 3, 4, 4, 5, 6]

        // 示例 2
        ListNode[] emptyLists = {};
        ListNode mergedList2 = solution.mergeKLists(emptyLists);
        printList(mergedList2); // 输出:[]

        // 示例 3
        ListNode[] singleEmptyList = {null};
        ListNode mergedList3 = solution.mergeKLists(singleEmptyList);
        printList(mergedList3); // 输出:[]
    }

    // 打印链表
    public static void printList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " ");
            head = head.next;
        }
        System.out.println();
    }
}

35.LRU 缓存(中等)

题目描述

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 105 次 get 和 put

解题思路

要实现一个满足 LRU (最近最少使用) 缓存约束的数据结构,可以使用 哈希表 配合 双向链表 来实现,能够在 O(1) 的时间复杂度下完成 getput 操作。

  1. 双向链表

    链表用于按顺序存储缓存中的元素,头部节点为最近使用的节点,尾部节点为最久未使用的节点;每次访问缓存中的某个节点时,将该节点移动到链表头部;插入新节点时,将其插入到链表头部。如果缓存超出容量,移除链表尾部的节点。
  2. 哈希表

    哈希表用于存储缓存中每个键值对的地址(即链表节点的引用),以便在 O(1) 的时间内找到节点并进行移动或删除操作。
  3. 实现步骤

    get(key):在哈希表中查找对应的节点。如果存在,将该节点移动到链表头部,并返回节点的值。如果不存在,返回 -1;put(key, value):在哈希表中查找键对应的节点。如果存在,更新节点的值并将其移动到链表头部。如果不存在,创建新节点并插入链表头部。如果缓存超过容量,移除链表尾部的节点并从哈希表中删除对应的键。

代码实现

package org.zyf.javabasic.letcode.hot100.list;

import java.util.HashMap;

/**
 * @program: zyfboot-javabasic
 * @description: LRU 缓存(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:38
 **/
public class LRUCache {
    private class Node {
        int key;
        int value;
        Node prev;
        Node next;

        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private final int capacity;
    private final HashMap<Integer, Node> cache;
    private final Node head;
    private final Node tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();

        // 初始化头尾虚节点
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        if (!cache.containsKey(key)) {
            return -1;
        }
        // 移动访问的节点到头部
        Node node = cache.get(key);
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            Node node = cache.get(key);
            node.value = value;
            moveToHead(node);
        } else {
            if (cache.size() == capacity) {
                // 移除尾部节点
                Node tailPrev = tail.prev;
                removeNode(tailPrev);
                cache.remove(tailPrev.key);
            }
            // 插入新节点
            Node newNode = new Node(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
        }
    }

    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
}

八、二叉树

36.二叉树的中序遍历(简单)

题目描述

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。

示例 1:

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:输入:root = [] 输出:[]

示例 3:输入:root = [1] 输出:[1]

提示:

  • 树中节点数目在范围 [0, 100] 内
  • -100 <= Node.val <= 100

进阶: 递归算法很简单,你可以通过迭代算法完成吗?

解题思路

中序遍历的顺序是:

  1. 先遍历左子树
  2. 然后访问根节点
  3. 最后遍历右子树

要实现二叉树的中序遍历,最常见的方式是使用递归。

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

public class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        inorder(root, result);
        return result;
    }

    private void inorder(TreeNode node, List<Integer> result) {
        if (node == null) {
            return;
        }
        // 先递归遍历左子树
        inorder(node.left, result);
        // 然后访问根节点
        result.add(node.val);
        // 最后递归遍历右子树
        inorder(node.right, result);
    }
}

迭代方法使用栈来替代递归。具体步骤如下:

  1. 从根节点开始,沿着左子树不断往下走,把所有节点压入栈中。
  2. 当没有左子树时,弹出栈顶节点,访问该节点,然后处理该节点的右子树。

复杂度分析

  • 时间复杂度:O(n),其中 n 是二叉树的节点数,因为每个节点都恰好被遍历一次。
  • 空间复杂度:O(h),其中 h 是二叉树的高度。栈的空间开销取决于树的高度,最坏情况下(退化为链表),空间复杂度为 O(n)。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

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

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的中序遍历(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:45
 **/
public class InorderTraversalSolution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode current = root;

        while (current != null || !stack.isEmpty()) {
            // 先遍历左子树,将节点入栈
            while (current != null) {
                stack.push(current);
                current = current.left;
            }

            // 弹出栈顶元素并访问
            current = stack.pop();
            result.add(current.val);

            // 处理右子树
            current = current.right;
        }

        return result;
    }

    // 测试主函数
    public static void main(String[] args) {
        // 构造测试用例
        TreeNode root = new TreeNode(1);
        root.right = new TreeNode(2);
        root.right.left = new TreeNode(3);

        // 创建 Solution 实例并进行测试
        InorderTraversalSolution solution = new InorderTraversalSolution();
        List<Integer> result = solution.inorderTraversal(root);

        // 打印结果
        System.out.println(result);  // 输出应为 [1, 3, 2]
    }
}

37.二叉树的最大深度(简单)

题目描述

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:3

示例 2:输入:root = [1,null,2] 输出:2

提示:

  • 树中节点的数量在 [0, 104] 区间内。
  • -100 <= Node.val <= 100

解题思路

递归方法较为简洁直观。它的基本思想是:

  1. 对于每个节点,最大深度是其左子树和右子树深度的最大值加上 1。
  2. 基础情况是,如果节点为空,则深度为 0。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的最大深度(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 10:51
 **/
public class MaxDepthSolution {
    // 递归计算二叉树的最大深度
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 计算左右子树的深度
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        // 返回较大深度加上根节点本身
        return Math.max(leftDepth, rightDepth) + 1;
    }

    // 测试主函数
    public static void main(String[] args) {
        // 构造测试用例
        TreeNode root1 = new TreeNode(3);
        root1.left = new TreeNode(9);
        root1.right = new TreeNode(20);
        root1.right.left = new TreeNode(15);
        root1.right.right = new TreeNode(7);

        TreeNode root2 = new TreeNode(1);
        root2.right = new TreeNode(2);

        // 创建 Solution 实例并进行测试
        MaxDepthSolution solution = new MaxDepthSolution();
        int depth1 = solution.maxDepth(root1);
        int depth2 = solution.maxDepth(root2);

        // 打印结果
        System.out.println(depth1);  // 输出应为 3
        System.out.println(depth2);  // 输出应为 2
    }
}

38.翻转二叉树(简单)

题目描述

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:

输入:root = [2,1,3]
输出:[2,3,1]

示例 3:输入:root = [] 输出:[]

提示:

  • 树中节点数目范围在 [0, 100] 内
  • -100 <= Node.val <= 100

解题思路

定义:翻转二叉树的操作是交换每个节点的左子树和右子树。这个操作从根节点开始,然后递归地进行到每个子节点。递归

  1. 基本情况:如果当前节点为空(root == null),则返回 null
  2. 交换子树:交换当前节点的左子树和右子树。
  3. 递归调用:递归地对左子树和右子树进行翻转。
  4. 返回节点:返回当前节点(其子树已经翻转)。

复杂度分析

  • 时间复杂度O(n)。其中 n 是树中的节点数。每个节点都需要访问一次以进行交换和递归操作。因此,时间复杂度是 O(n)。

  • 空间复杂度O(h)。其中 h 是树的高度。递归调用会消耗栈空间,栈的深度是树的高度。最坏情况下,树是链式结构(即退化为单边树),高度为 n,所以空间复杂度是 O(n)。但对于平衡树,树的高度是 log(n),因此在平衡树的情况下,空间复杂度是 O(log n)。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 翻转二叉树(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:00
 **/
public class InvertTreeSolution {
    // 翻转二叉树的递归方法
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        // 交换左右子树
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        // 递归翻转左右子树
        invertTree(root.left);
        invertTree(root.right);

        return root;
    }

    // 打印树的中序遍历(用于验证)
    public void printInOrder(TreeNode root) {
        if (root != null) {
            printInOrder(root.left);
            System.out.print(root.val + " ");
            printInOrder(root.right);
        }
    }

    // 测试主函数
    public static void main(String[] args) {
        // 构造测试用例
        TreeNode root1 = new TreeNode(4);
        root1.left = new TreeNode(2);
        root1.right = new TreeNode(7);
        root1.left.left = new TreeNode(1);
        root1.left.right = new TreeNode(3);
        root1.right.left = new TreeNode(6);
        root1.right.right = new TreeNode(9);

        TreeNode root2 = new TreeNode(2);
        root2.left = new TreeNode(1);
        root2.right = new TreeNode(3);

        TreeNode root3 = null;

        // 创建 Solution 实例并进行测试
        InvertTreeSolution solution = new InvertTreeSolution();

        // 翻转并打印结果
        TreeNode invertedRoot1 = solution.invertTree(root1);
        TreeNode invertedRoot2 = solution.invertTree(root2);
        TreeNode invertedRoot3 = solution.invertTree(root3);

        System.out.print("Inverted Tree 1 (InOrder): ");
        solution.printInOrder(invertedRoot1); // 输出应为 9 7 6 4 3 2 1
        System.out.println();

        System.out.print("Inverted Tree 2 (InOrder): ");
        solution.printInOrder(invertedRoot2); // 输出应为 3 2 1
        System.out.println();

        System.out.print("Inverted Tree 3 (InOrder): ");
        solution.printInOrder(invertedRoot3); // 输出应为空
        System.out.println();
    }
}

39.对称二叉树(简单)

题目描述

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

提示:

  • 树中节点数目在范围 [1, 1000] 内
  • -100 <= Node.val <= 100

进阶:你可以运用递归和迭代两种方法解决这个问题吗?

解题思路

要检查一个二叉树是否是轴对称的,我们可以使用递归或迭代的方法。这里提供了两种方法的解题思路和复杂度分析。

1. 递归方法

递归检查子树:我们需要检查左右子树是否对称。对称的定义是:

  • 左子树的左子树与右子树的右子树对称。
  • 左子树的右子树与右子树的左子树对称。

步骤

  1. 基本情况:如果两个子树都为空,则它们是对称的;如果一个子树为空而另一个子树不为空,则它们不是对称的。
  2. 递归条件:检查当前两个节点的值是否相等;递归检查左子树的左子树与右子树的右子树;递归检查左子树的右子树与右子树的左子树。

2. 迭代方法

使用队列:可以使用队列来模拟递归检查过程,通过层次遍历来比较每层的节点对称性。步骤

  1. 使用一个队列来存储节点对。
  2. 初始时将根节点的左右子节点加入队列。
  3. 循环处理队列中的节点对:
    • 从队列中取出一对节点。
    • 如果两个节点都为空,继续下一对节点。
    • 如果一个节点为空而另一个不为空,返回 false
    • 如果两个节点的值不相等,返回 false
    • 将节点对的子节点按照对称的方式加入队列。

复杂度分析

1.递归方法

  • 时间复杂度:O(n),其中 n 是树中的节点数。每个节点都被访问一次。
  • 空间复杂度:O(h),其中 h 是树的高度。递归调用的栈深度最大为树的高度。

2. 迭代方法

  • 时间复杂度:O(n),与递归方法相同,每个节点都被访问一次。
  • 空间复杂度:O(n),在最坏情况下,队列中可能存储所有节点,特别是树完全时。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

import java.util.LinkedList;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 对称二叉树(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:15
 **/
public class IsSymmetricSolution {

    public boolean isSymmetric1(TreeNode root) {
        if (root == null) {
            return true;
        }
        return isMirror(root.left, root.right);
    }

    private boolean isMirror(TreeNode t1, TreeNode t2) {
        if (t1 == null && t2 == null) {
            return true;
        }
        if (t1 == null || t2 == null) {
            return false;
        }
        return (t1.val == t2.val) &&
                isMirror(t1.right, t2.left) &&
                isMirror(t1.left, t2.right);
    }

    public boolean isSymmetric2(TreeNode root) {
        if (root == null) {
            return true;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root.left);
        queue.offer(root.right);

        while (!queue.isEmpty()) {
            TreeNode t1 = queue.poll();
            TreeNode t2 = queue.poll();

            if (t1 == null && t2 == null) {
                continue;
            }
            if (t1 == null || t2 == null) {
                return false;
            }
            if (t1.val != t2.val) {
                return false;
            }

            queue.offer(t1.left);
            queue.offer(t2.right);
            queue.offer(t1.right);
            queue.offer(t2.left);
        }

        return true;
    }

    public static void main(String[] args) {
        IsSymmetricSolution solution = new IsSymmetricSolution();

        // 示例 1: 对称的二叉树
        TreeNode root1 = new TreeNode(1);
        root1.left = new TreeNode(2);
        root1.right = new TreeNode(2);
        root1.left.left = new TreeNode(3);
        root1.left.right = new TreeNode(4);
        root1.right.left = new TreeNode(4);
        root1.right.right = new TreeNode(3);

        System.out.println("Example 1: " + solution.isSymmetric1(root1)); // 输出: true

        // 示例 2: 不对称的二叉树
        TreeNode root2 = new TreeNode(1);
        root2.left = new TreeNode(2);
        root2.right = new TreeNode(2);
        root2.left.right = new TreeNode(3);
        root2.right.right = new TreeNode(3);

        System.out.println("Example 2: " + solution.isSymmetric2(root2)); // 输出: false

        // 示例 3: 空树
        TreeNode root3 = null;

        System.out.println("Example 3: " + solution.isSymmetric2(root3)); // 输出: true
    }
}

40.二叉树的直径(简单)

题目描述

给你一棵二叉树的根节点,返回该树的 直径 。

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。

两节点之间路径的 长度 由它们之间边数表示。

示例 1:

输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。

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

提示:

  • 树中节点数目在范围 [1, 104] 内
  • -100 <= Node.val <= 100

解题思路

要找到二叉树的直径,我们需要找到树中任意两个节点之间的最长路径。这个路径的长度由路径中的边数决定,而不是节点数,可以使用深度优先搜索(DFS)的方法:

  1. 定义直径:直径是树中两个节点之间最长的路径长度。这个路径可能会经过树的根节点,也可能不会。

  2. DFS 计算深度

    使用 DFS 递归遍历树中的每个节点,同时计算每个节点的左右子树的深度;对于每个节点,计算通过该节点的最长路径长度,即左右子树深度之和;更新全局变量 maxDiameter 以记录当前最大直径。
  3. 计算节点的深度:通过递归计算每个节点的左右子树的深度,返回节点的最大深度。

复杂度分析

时间复杂度

  • 计算每个节点的深度:对于树中的每个节点,我们都需要递归地计算其左右子树的深度。每个节点的处理(即计算其左右子树深度和更新最大直径)只需要常数时间。
  • 总时间复杂度:由于每个节点在 DFS 遍历过程中被访问一次,所以时间复杂度是 O(N),其中 NNN 是树中节点的总数。

空间复杂度

  • 递归栈的空间:由于使用了递归 DFS 方法,递归栈的最大深度等于树的高度。对于最坏情况(例如链式树结构),树的高度可能达到 NNN(节点数),此时递归栈的空间复杂度为 O(N)。对于平衡树,递归栈的深度是树的高度,即 O(log N)
  • 额外空间:除了递归栈外,额外使用的空间主要是 maxDiameter 变量,空间复杂度为 O(1)

因此,整体的空间复杂度主要由递归栈的深度决定,对于最坏情况下是 O(N),而对于平衡树是 O(log N)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的直径(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:23
 **/
public class DiameterOfBinaryTreeSolution {
    private int maxDiameter = 0; // 记录最大直径

    public int diameterOfBinaryTree(TreeNode root) {
        calculateDepth(root);
        return maxDiameter;
    }

    // 计算树的深度并更新最大直径
    private int calculateDepth(TreeNode node) {
        if (node == null) {
            return 0;
        }

        // 递归计算左右子树的深度
        int leftDepth = calculateDepth(node.left);
        int rightDepth = calculateDepth(node.right);

        // 更新最大直径
        maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);

        // 返回当前节点的深度
        return Math.max(leftDepth, rightDepth) + 1;
    }

    public static void main(String[] args) {
        DiameterOfBinaryTreeSolution solution = new DiameterOfBinaryTreeSolution();

        // 示例 1
        TreeNode root1 = new TreeNode(1);
        root1.left = new TreeNode(2);
        root1.right = new TreeNode(3);
        root1.left.left = new TreeNode(4);
        root1.left.right = new TreeNode(5);

        System.out.println("Example 1: " + solution.diameterOfBinaryTree(root1)); // 输出: 3

        // 示例 2
        TreeNode root2 = new TreeNode(1);
        root2.left = new TreeNode(2);

        System.out.println("Example 2: " + solution.diameterOfBinaryTree(root2)); // 输出: 1
    }
}

41.二叉树的层序遍历(中等)

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

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

示例 3:输入:root = [] 输出:[]

提示:

  • 树中节点数目在范围 [0, 2000] 内
  • -1000 <= Node.val <= 1000

解题思路

层序遍历(二叉树的宽度优先遍历)可以使用队列(FIFO)来实现。我们逐层遍历树中的节点,每次处理一层的所有节点,并将它们的子节点加入队列中。下面是实现层序遍历的步骤:

  1. 初始化队列:将根节点放入队列中。
  2. 遍历队列:每次从队列中取出当前层的所有节点,处理它们(即收集它们的值),并将它们的子节点(左子节点和右子节点)加入队列中。
  3. 重复步骤 2,直到队列为空。
  4. 返回结果:每层的节点值保存在一个列表中,最终返回所有层的节点值列表。

复杂度分析

时间复杂度

  • 遍历所有节点:每个节点被访问一次,处理其值并将其子节点添加到队列中。
  • 时间复杂度O(N),其中 NNN 是树中节点的总数。

空间复杂度

  • 队列的最大空间:队列中最多会存储树的最宽层的所有节点。如果树是完全二叉树,队列的最大空间复杂度是 O(W),其中 WWW 是树的最大宽度。在完全二叉树中,宽度 WWW 最多为 N/2N/2N/2,因此空间复杂度为 O(N)
  • 额外空间:用于存储每层的节点值的列表,最坏情况下也是 O(N)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的层序遍历(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:28
 **/
public class LevelOrderSolution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) {
            return result;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int levelSize = queue.size();
            List<Integer> levelNodes = new ArrayList<>();

            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                levelNodes.add(node.val);

                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }

            result.add(levelNodes);
        }

        return result;
    }

    public static void main(String[] args) {
        // 创建一个示例二叉树
        TreeNode root = new TreeNode(3);
        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);

        LevelOrderSolution solution = new LevelOrderSolution();
        List<List<Integer>> result = solution.levelOrder(root);

        // 打印结果
        System.out.println(result);
    }
}

42.将有序数组转换为二叉搜索树(简单)

题目描述

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。

示例 1:

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

示例 2:

输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 按 严格递增 顺序排列

解题思路

要将一个升序排列的整数数组转换为一棵平衡二叉搜索树(BST),我们可以利用递归方法构建树。这是因为一个平衡的BST的中序遍历应该是升序排列的,因此我们可以通过递归的方式选择中间的元素作为根节点,递归构建左右子树,从而保持平衡。

  1. 递归构建树

    选择数组的中间元素作为根节点;递归地构建左子树,左子树的节点来源于数组的左半部分;递归地构建右子树,右子树的节点来源于数组的右半部分。
  2. 树的平衡性:由于数组已经是升序排列的,选择中间元素作为根节点可以保证树的高度平衡。

复杂度分析

  • 时间复杂度O(n),其中 n 是数组的长度。每个节点只被创建一次,且数组每次被划分为两个部分,时间复杂度为 O(n)
  • 空间复杂度O(log n),主要是递归栈的空间复杂度。在最坏情况下,递归栈的深度为树的高度,树的高度为 O(log n)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 将有序数组转换为二叉搜索树(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:34
 **/
public class SortedArrayToBSTSolution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return sortedArrayToBSTHelper(nums, 0, nums.length - 1);
    }

    private TreeNode sortedArrayToBSTHelper(int[] nums, int left, int right) {
        if (left > right) {
            return null;
        }

        int mid = left + (right - left) / 2;
        TreeNode root = new TreeNode(nums[mid]);
        root.left = sortedArrayToBSTHelper(nums, left, mid - 1);
        root.right = sortedArrayToBSTHelper(nums, mid + 1, right);

        return root;
    }

    public static void main(String[] args) {
        SortedArrayToBSTSolution solution = new SortedArrayToBSTSolution();

        // Example 1
        int[] nums1 = {-10, -3, 0, 5, 9};
        TreeNode root1 = solution.sortedArrayToBST(nums1);
        printTree(root1);  // Output should be a balanced BST

        // Example 2
        int[] nums2 = {1, 3};
        TreeNode root2 = solution.sortedArrayToBST(nums2);
        printTree(root2);  // Output should be a balanced BST
    }

    private static void printTree(TreeNode root) {
        if (root == null) {
            System.out.println("null");
            return;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node != null) {
                System.out.print(node.val + " ");
                queue.add(node.left);
                queue.add(node.right);
            } else {
                System.out.print("null ");
            }
        }
        System.out.println();
    }
}

43.验证二叉搜索树(中等)

题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:root = [2,1,3]
输出:true

示例 2:

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:

  • 树中节点数目范围在[1, 104] 内
  • -231 <= Node.val <= 231 - 1

解题思路

要判断一个二叉树是否是一个有效的二叉搜索树(BST),可以利用 BST 的性质进行递归检查:

  1. 定义

    左子树的所有节点的值都必须小于当前节点的值;右子树的所有节点的值都必须大于当前节点的值;每个子树也必须是 BST。
  2. 递归方法

    在递归中,维护一个有效的值范围(minmax),用于确保每个节点的值都在正确的范围内;对于每个节点,检查其值是否在给定的范围内,然后递归检查其左子树和右子树,更新有效值范围。

复杂度分析

  • 时间复杂度O(n),其中 n 是树的节点数。每个节点会被访问一次。
  • 空间复杂度O(h),其中 h 是树的高度。递归栈的空间复杂度为树的高度。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 验证二叉搜索树(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:42
 **/
public class IsValidBSTSolution {
    public boolean isValidBST(TreeNode root) {
        return isValidBSTHelper(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    private boolean isValidBSTHelper(TreeNode node, long min, long max) {
        if (node == null) {
            return true;
        }

        // Check current node value
        if (node.val <= min || node.val >= max) {
            return false;
        }

        // Recursively check left and right subtrees
        return isValidBSTHelper(node.left, min, node.val) &&
                isValidBSTHelper(node.right, node.val, max);
    }

    public static void main(String[] args) {
        IsValidBSTSolution solution = new IsValidBSTSolution();

        // Example 1
        TreeNode root1 = new TreeNode(2);
        root1.left = new TreeNode(1);
        root1.right = new TreeNode(3);
        System.out.println(solution.isValidBST(root1));  // Output: true

        // Example 2
        TreeNode root2 = new TreeNode(5);
        root2.left = new TreeNode(1);
        root2.right = new TreeNode(4);
        root2.right.left = new TreeNode(3);
        root2.right.right = new TreeNode(6);
        System.out.println(solution.isValidBST(root2));  // Output: false
    }
}

44.二叉搜索树中第 K 小的元素(中等)

题目描述

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

示例 1:

输入:root = [3,1,4,null,2], k = 1
输出:1

示例 2:

输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3

提示:

  • 树中的节点数为 n 。
  • 1 <= k <= n <= 104
  • 0 <= Node.val <= 104

进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

解题思路

要查找二叉搜索树(BST)中的第 k 小元素,我们可以利用 BST 的中序遍历特性。中序遍历 BST 会以升序方式访问所有节点,因此第 k 小的元素就是中序遍历结果中的第 k 个元素。

  1. 中序遍历:中序遍历 BST 的结果是一个升序排列的节点值列表。可以使用递归或迭代的方式进行中序遍历。

  2. 递归方法:在遍历过程中,维护一个计数器来记录已经遍历的节点数量,当计数器等于 k 时,返回当前节点的值。

复杂度分析

  • 时间复杂度O(n),其中 n 是树的节点数。需要遍历整个树的节点,直到找到第 k 小的元素。
  • 空间复杂度O(h),其中 h 是树的高度。递归栈的空间复杂度为树的高度。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉搜索树中第 K 小的元素(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:48
 **/
public class KthSmallestSolution {
    private int count = 0; // Count of nodes visited
    private int result = -1; // To store the kth smallest value

    public int kthSmallest(TreeNode root, int k) {
        // Perform in-order traversal
        inOrderTraversal(root, k);
        return result;
    }

    private void inOrderTraversal(TreeNode node, int k) {
        if (node == null) {
            return;
        }

        // Traverse left subtree
        inOrderTraversal(node.left, k);

        // Visit node
        count++;
        if (count == k) {
            result = node.val;
            return;
        }

        // Traverse right subtree
        inOrderTraversal(node.right, k);
    }

    public static void main(String[] args) {
        KthSmallestSolution solution = new KthSmallestSolution();

        // Example 1
        TreeNode root1 = new TreeNode(3);
        root1.left = new TreeNode(1);
        root1.right = new TreeNode(4);
        root1.left.right = new TreeNode(2);
        System.out.println(solution.kthSmallest(root1, 1));  // Output: 1

        // Example 2
        TreeNode root2 = new TreeNode(5);
        root2.left = new TreeNode(3);
        root2.right = new TreeNode(6);
        root2.left.left = new TreeNode(2);
        root2.left.right = new TreeNode(4);
        root2.left.left.left = new TreeNode(1);
        System.out.println(solution.kthSmallest(root2, 3));  // Output: 3
    }
}

45.二叉树的右视图(中等)

题目描述

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

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

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

示例 3:输入: [] 输出: []

提示:

  • 二叉树的节点个数的范围是 [0,100]
  • -100 <= Node.val <= 100 

解题思路

要从二叉树的右侧查看并返回节点值,我们可以使用层序遍历(广度优先遍历)来实现。具体来说,我们需要从右侧依次访问每一层的节点,并从每一层的最右侧节点开始返回结果。

  1. 层序遍历

    使用一个队列来实现层序遍历;遍历每一层的节点时,记录每层的最后一个节点值,因为它代表了从右侧可以看到的节点;将每一层的节点值添加到结果列表中。
  2. 实现步骤

    • 初始化一个队列,将根节点加入队列。
    • 对于每一层,记录层的节点数(即队列的当前大小)。
    • 遍历该层的所有节点,并更新队列(将当前节点的左子节点和右子节点加入队列)。
    • 记录每层最后一个节点的值(即右侧可见节点)。
    • 返回结果列表。

复杂度分析

  • 时间复杂度O(n),其中 n 是树中的节点数。每个节点被访问一次。
  • 空间复杂度O(w),其中 w 是树的最大宽度(即队列中最大的元素数)。在最坏的情况下,队列的大小等于树的最大宽度。

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的右视图(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:53
 **/
public class RightSideViewSolution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) {
            return result;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int levelSize = queue.size();
            Integer rightMostValue = null;

            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                rightMostValue = node.val;

                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }

            result.add(rightMostValue);
        }

        return result;
    }

    public static void main(String[] args) {
        RightSideViewSolution solution = new RightSideViewSolution();

        // Example 1
        TreeNode root1 = new TreeNode(1);
        root1.left = new TreeNode(2);
        root1.right = new TreeNode(3);
        root1.left.right = new TreeNode(5);
        root1.right.right = new TreeNode(4);
        System.out.println(solution.rightSideView(root1));  // Output: [1, 3, 4]

        // Example 2
        TreeNode root2 = new TreeNode(1);
        root2.right = new TreeNode(3);
        System.out.println(solution.rightSideView(root2));  // Output: [1, 3]

        // Example 3
        TreeNode root3 = null;
        System.out.println(solution.rightSideView(root3));  // Output: []
    }
}

46.二叉树展开为链表(中等)

题目描述

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

示例 1:

输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]

示例 2:输入:root = [] 输出:[]

示例 3:输入:root = [0] 输出:[0]

提示:

  • 树中结点数在范围 [0, 2000] 内
  • -100 <= Node.val <= 100

进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗?

解题思路

要将二叉树展开成一个单链表,我们可以使用先序遍历的方式,同时在遍历过程中重组树的结构,使得每个节点的右子指针指向链表中的下一个节点,左子指针始终为 null。我们可以通过递归或迭代的方式来实现这一目标。

  1. 先序遍历:按照先序遍历的顺序访问节点,将它们连接成链表。

  2. 重组树的结构

    在遍历过程中,每访问一个节点时,将其右子树接到链表的末尾;将当前节点的左子指针设置为 null,并将右子指针指向下一个节点。
  3. 实现方法

    使用递归的方式来进行先序遍历,并在遍历过程中修改树的结构;使用迭代的方式也可以实现,通过栈来模拟递归的过程。

复杂度分析

  • 时间复杂度O(n),其中 n 是树中的节点数。每个节点被访问和处理一次。
  • 空间复杂度O(h),其中 h 是树的高度。递归调用的栈空间与树的高度有关。对于平衡树,h 约为 O(log n),对于不平衡树,h 约为 O(n)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树展开为链表(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 11:59
 **/
public class FlattenSolution {
    // 主函数:将二叉树展开为单链表
    public void flatten(TreeNode root) {
        if (root == null) return;  // 如果树为空,直接返回

        // 先序遍历并展开右子树
        flattenTree(root);
    }

    // 辅助函数:先序遍历树并将其展开为链表
    private TreeNode flattenTree(TreeNode node) {
        if (node == null) return null;  // 如果节点为空,返回空

        // 递归展开左子树
        TreeNode left = flattenTree(node.left);
        // 递归展开右子树
        TreeNode right = flattenTree(node.right);

        // 如果左子树不为空,将其插入到右子树前面
        if (left != null) {
            // 将左子树的右子树连接到右子树上
            TreeNode temp = left;
            while (temp.right != null) {
                temp = temp.right;
            }
            temp.right = right;  // 将右子树接到左子树的末尾

            // 将左子树作为右子树
            node.right = left;
            node.left = null;  // 左子树应为空
        }

        // 返回当前节点
        return node;
    }

    // 主函数,用于测试
    public static void main(String[] args) {
        // 构造测试用例 [1,2,5,3,4,null,6]
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(5);
        root.left.left = new TreeNode(3);
        root.left.right = new TreeNode(4);
        root.right.right = new TreeNode(6);

        // 调用函数展开树
        FlattenSolution solution = new FlattenSolution();
        solution.flatten(root);

        // 打印展开后的链表
        TreeNode current = root;
        while (current != null) {
            System.out.print(current.val);
            if (current.right != null) {
                System.out.print(" -> ");
            }
            current = current.right;
        }
    }
}

47.从前序与中序遍历序列构造二叉树(中等)

题目描述

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

示例 2:输入: preorder = [-1], inorder = [-1] 输出: [-1]

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorder 和 inorder 均 无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

解题思路

构造二叉树的过程基于先序遍历(preorder)和中序遍历(inorder)信息。在先序遍历中,第一个元素是树的根节点,而在中序遍历中,根节点左边的元素属于左子树,右边的元素属于右子树。

  1. 确定根节点:先序遍历的第一个元素是根节点。
  2. 分割中序遍历:根据根节点在中序遍历中的位置,将中序遍历分成左子树和右子树。
  3. 递归构建子树:使用左子树的中序遍历和右子树的中序遍历来递归构建左子树和右子树;同时,更新先序遍历以反映子树节点。

复杂度分析

  • 时间复杂度:构建树的时间复杂度是 O(n),其中 n 是树中节点的总数。由于每个节点被访问一次并进行常数时间的操作。
  • 空间复杂度:主要空间复杂度来自于递归调用栈的深度和存储中序遍历位置的 HashMap。递归调用栈的深度为 O(h),其中 h 是树的高度。HashMap 需要 O(n) 空间用于存储中序遍历的索引。总空间复杂度为 O(n)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

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

/**
 * @program: zyfboot-javabasic
 * @description: 从前序与中序遍历序列构造二叉树(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:04
 **/
public class BuildTreeSolution {
    private int preIndex = 0;  // 用于记录当前先序遍历的索引
    private Map<Integer, Integer> inOrderMap = new HashMap<>();  // 记录中序遍历中节点的索引位置

    // 主函数:根据先序遍历和中序遍历构建二叉树
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        // 记录中序遍历中每个节点的位置
        for (int i = 0; i < inorder.length; i++) {
            inOrderMap.put(inorder[i], i);
        }
        // 调用递归函数构建树
        return buildTreeHelper(preorder, 0, preorder.length - 1, 0, inorder.length - 1);
    }

    // 递归函数:构建树的节点
    private TreeNode buildTreeHelper(int[] preorder, int preStart, int preEnd, int inStart, int inEnd) {
        // 如果子树为空,返回null
        if (preStart > preEnd || inStart > inEnd) return null;

        // 根节点的值为先序遍历中的第一个元素
        int rootValue = preorder[preStart];
        TreeNode root = new TreeNode(rootValue);

        // 找到根节点在中序遍历中的索引
        int inRootIndex = inOrderMap.get(rootValue);
        int leftTreeSize = inRootIndex - inStart;  // 左子树的大小

        // 递归构建左子树
        root.left = buildTreeHelper(preorder, preStart + 1, preStart + leftTreeSize, inStart, inRootIndex - 1);
        // 递归构建右子树
        root.right = buildTreeHelper(preorder, preStart + leftTreeSize + 1, preEnd, inRootIndex + 1, inEnd);

        return root;
    }

    // 主函数,用于测试
    public static void main(String[] args) {
        BuildTreeSolution solution = new BuildTreeSolution();

        // 示例1
        int[] preorder1 = {3, 9, 20, 15, 7};
        int[] inorder1 = {9, 3, 15, 20, 7};
        TreeNode root1 = solution.buildTree(preorder1, inorder1);
        printInOrder(root1);  // 打印中序遍历检查结果
        System.out.println();
        printPreOrder(root1); // 打印先序遍历检查结果

        // 示例2
        int[] preorder2 = {-1};
        int[] inorder2 = {-1};
        TreeNode root2 = solution.buildTree(preorder2, inorder2);
        printInOrder(root2);  // 打印中序遍历检查结果
        System.out.println();
        printPreOrder(root2); // 打印先序遍历检查结果
    }

    // 打印树的中序遍历
    private static void printInOrder(TreeNode root) {
        if (root == null) return;
        printInOrder(root.left);
        System.out.print(root.val + " ");
        printInOrder(root.right);
    }

    // 打印树的先序遍历
    private static void printPreOrder(TreeNode root) {
        if (root == null) return;
        System.out.print(root.val + " ");
        printPreOrder(root.left);
        printPreOrder(root.right);
    }
}

48.路径总和 III(中等)

题目描述

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

示例 2:输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 输出:3

提示:

  • 二叉树的节点个数的范围是 [0,1000]
  • -109 <= Node.val <= 109 
  • -1000 <= targetSum <= 1000 

解题思路

  • 定义前缀和

    • 使用哈希表 prefix 存储从根节点到当前节点的路径和的前缀和及其出现次数。
    • prefix.getOrDefault(curr - targetSum, 0) 用于计算当前路径和减去目标和的前缀和的出现次数,这个次数就是当前节点作为路径终点时的路径数。
  • 递归深度优先搜索(DFS)

    • 在递归过程中,更新当前路径和 curr
    • 使用哈希表 prefix 记录当前路径和的出现次数,并更新路径和的计数。
    • 递归访问左子树和右子树。
    • 递归结束后,恢复哈希表的状态,移除当前路径和的计数,以便继续处理其他路径。
  • 处理路径和

    • 每次访问一个节点时,检查当前路径和 curr 减去 targetSum 的值是否在哈希表中出现过。如果出现,说明存在从某个祖先节点到当前节点的路径和等于 targetSum
    • 通过累加满足条件的路径数,得到最终结果。

复杂度分析

  • 时间复杂度:每个节点访问一次,哈希表的操作(插入、查找、删除)平均时间复杂度为 O(1)。因此,总时间复杂度是 O(n),其中 n 是树的节点数。

  • 空间复杂度:哈希表 prefix 的空间复杂度为 O(n),在最坏情况下,哈希表需要存储所有节点的路径和;递归调用栈的深度在最坏情况下为树的高度 h,对平衡树而言,hO(log n),对退化树(链状树)而言,hO(n)。因此,总体空间复杂度是 O(n)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

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

/**
 * @program: zyfboot-javabasic
 * @description: 路径总和 III(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:12
 **/
public class PathSumSolution {
    public int pathSum(TreeNode root, int targetSum) {
        // 哈希表存储前缀和及其出现次数
        Map<Long, Integer> prefix = new HashMap<>();
        // 初始前缀和为0,出现次数为1
        prefix.put(0L, 1);
        // 进行深度优先搜索
        return dfs(root, prefix, 0, targetSum);
    }

    private int dfs(TreeNode node, Map<Long, Integer> prefix, long curr, int targetSum) {
        if (node == null) {
            return 0;
        }

        int result = 0;
        // 更新当前路径和
        curr += node.val;

        // 当前路径和减去目标和的前缀和出现次数
        result = prefix.getOrDefault(curr - targetSum, 0);

        // 更新前缀和出现次数
        prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);

        // 递归访问左子树和右子树
        result += dfs(node.left, prefix, curr, targetSum);
        result += dfs(node.right, prefix, curr, targetSum);

        // 恢复哈希表状态
        prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);

        return result;
    }

    public static void main(String[] args) {
        // 构造测试用例1
        TreeNode root1 = new TreeNode(10);
        root1.left = new TreeNode(5);
        root1.right = new TreeNode(-3);
        root1.left.left = new TreeNode(3);
        root1.left.right = new TreeNode(2);
        root1.right.right = new TreeNode(11);
        root1.left.left.left = new TreeNode(3);
        root1.left.left.right = new TreeNode(-2);
        root1.left.right.right = new TreeNode(1);

        PathSumSolution solution = new PathSumSolution();
        int result1 = solution.pathSum(root1, 8);
        System.out.println("Test Case 1 Result: " + result1);  // Expected output: 3

        // 构造测试用例2
        TreeNode root2 = new TreeNode(5);
        root2.left = new TreeNode(4);
        root2.right = new TreeNode(8);
        root2.left.left = new TreeNode(11);
        root2.right.left = new TreeNode(13);
        root2.right.right = new TreeNode(4);
        root2.left.left.left = new TreeNode(7);
        root2.left.left.right = new TreeNode(2);
        root2.right.right.right = new TreeNode(1);

        int result2 = solution.pathSum(root2, 22);
        System.out.println("Test Case 2 Result: " + result2);  // Expected output: 3
    }

}

49.二叉树的最近公共祖先(中等)

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:输入:root = [1,2], p = 1, q = 2 输出:1

提示:

  • 树中节点数目在范围 [2, 105] 内。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同 。
  • p != q
  • p 和 q 均存在于给定的二叉树中。

解题思路

要找到二叉树中两个指定节点的最近公共祖先(LCA),可以通过递归算法实现。我们将利用递归来查找左右子树的最近公共祖先,并根据节点的情况决定返回的结果。

  1. 递归遍历

    对于每个节点,递归地查找其左右子树中是否存在节点 pq;如果在当前节点的左子树中找到了 pq,并且在右子树中也找到了另外一个节点,那么当前节点就是 pq 的最近公共祖先;如果在某一侧的子树中找到了 pq,而另一侧子树没有找到,则返回找到的节点。
  2. 返回条件

    当节点为空时(递归到叶子节点),返回 null;当节点等于 pq 时,返回该节点本身;合并左右子树的结果来确定当前节点是否为 LCA。

复杂度分析

  • 时间复杂度O(N),其中 N 是树中节点的数量。每个节点在递归中被访问一次。
  • 空间复杂度O(H),其中 H 是树的高度。递归调用栈的深度与树的高度成正比。对于平衡树,空间复杂度为 O(log N);对于不平衡树,空间复杂度为 O(N)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树的最近公共祖先(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:18
 **/
public class LowestCommonAncestorSolution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        // 递归终止条件:如果当前节点为空或等于 p 或 q,直接返回当前节点
        if (root == null || root == p || root == q) {
            return root;
        }

        // 递归查找左子树
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        // 递归查找右子树
        TreeNode right = lowestCommonAncestor(root.right, p, q);

        // 如果左子树和右子树都找到了 p 或 q,那么当前节点是 LCA
        if (left != null && right != null) {
            return root;
        }

        // 如果左子树找到了 p 或 q,则返回左子树的结果,否则返回右子树的结果
        return left != null ? left : right;
    }

    public static void main(String[] args) {
        // 构造测试用例1
        TreeNode root1 = new TreeNode(3);
        root1.left = new TreeNode(5);
        root1.right = new TreeNode(1);
        root1.left.left = new TreeNode(6);
        root1.left.right = new TreeNode(2);
        root1.right.left = new TreeNode(0);
        root1.right.right = new TreeNode(8);
        root1.left.right.left = new TreeNode(7);
        root1.left.right.right = new TreeNode(4);

        LowestCommonAncestorSolution solution = new LowestCommonAncestorSolution();
        TreeNode p1 = root1.left; // Node 5
        TreeNode q1 = root1.right; // Node 1
        TreeNode result1 = solution.lowestCommonAncestor(root1, p1, q1);
        System.out.println("Test Case 1 Result: " + result1.val); // Expected output: 3

        // 构造测试用例2
        TreeNode root2 = new TreeNode(3);
        root2.left = new TreeNode(5);
        root2.right = new TreeNode(1);
        root2.left.left = new TreeNode(6);
        root2.left.right = new TreeNode(2);
        root2.right.left = new TreeNode(0);
        root2.right.right = new TreeNode(8);
        root2.left.right.left = new TreeNode(7);
        root2.left.right.right = new TreeNode(4);

        TreeNode p2 = root2.left; // Node 5
        TreeNode q2 = root2.left.right.right; // Node 4
        TreeNode result2 = solution.lowestCommonAncestor(root2, p2, q2);
        System.out.println("Test Case 2 Result: " + result2.val); // Expected output: 5

        // 构造测试用例3
        TreeNode root3 = new TreeNode(1);
        root3.left = new TreeNode(2);

        LowestCommonAncestorSolution solution3 = new LowestCommonAncestorSolution();
        TreeNode p3 = root3; // Node 1
        TreeNode q3 = root3.left; // Node 2
        TreeNode result3 = solution3.lowestCommonAncestor(root3, p3, q3);
        System.out.println("Test Case 3 Result: " + result3.val); // Expected output: 1
    }
}

50.二叉树中的最大路径和 (困难)

题目描述

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000

解题思路

要解决这个问题,我们需要找到二叉树中的最大路径和。这种路径可以是从任何节点开始并到任何节点结束的路径,不一定经过根节点。为了实现这一点,我们需要考虑路径的两种情况:

  1. 路径通过当前节点:这意味着路径可以从当前节点的左子树或右子树扩展到当前节点,并可能继续到右子树或左子树的路径上。路径的总和可能是当前节点的值加上左子树和右子树的最大路径和。

  2. 路径不通过当前节点:这意味着最大路径和仅仅包括当前节点和它的左或右子树中的路径和,但不会跨越到另一侧的子树。

解题思路

  1. 递归函数:定义一个递归函数 maxPathSum,用于计算以当前节点为起点的路径和,并返回从当前节点向下延伸的最大路径和;在这个函数内部,我们会同时更新全局最大路径和变量 maxSum

  2. 路径和计算:对于每个节点,计算以该节点为根的路径和,包括左子树和右子树的最大路径和;更新全局最大路径和 maxSum,这是当前节点作为路径的“根”的最大路径和。

  3. 返回值:返回从当前节点向下延伸的最大路径和(即节点的值加上左或右子树的最大路径和)。

复杂度分析

  • 时间复杂度O(N),其中 N 是树中的节点数。每个节点被访问一次。
  • 空间复杂度O(H),其中 H 是树的高度。递归调用栈的深度与树的高度成正比。对于平衡树,空间复杂度为 O(log N);对于不平衡树,空间复杂度为 O(N)

代码实现

package org.zyf.javabasic.letcode.hot100.tree;

import org.zyf.javabasic.letcode.tree.base.TreeNode;

/**
 * @program: zyfboot-javabasic
 * @description: 二叉树中的最大路径和 (困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:23
 **/
public class MaxPathSumSolution {
    private int maxSum = Integer.MIN_VALUE; // 全局变量,存储最大路径和

    public int maxPathSum(TreeNode root) {
        maxPathSumHelper(root);
        return maxSum;
    }

    // 递归函数,计算从当前节点向下延伸的最大路径和
    private int maxPathSumHelper(TreeNode node) {
        if (node == null) {
            return 0;
        }

        // 递归计算左子树和右子树的最大路径和
        int left = Math.max(maxPathSumHelper(node.left), 0); // 负数路径不贡献
        int right = Math.max(maxPathSumHelper(node.right), 0); // 负数路径不贡献

        // 当前节点的最大路径和为当前节点值 + 左右子树的最大路径和
        int currentSum = node.val + left + right;

        // 更新全局最大路径和
        maxSum = Math.max(maxSum, currentSum);

        // 返回当前节点的最大路径和
        return node.val + Math.max(left, right);
    }

    public static void main(String[] args) {
        // 构造测试用例1
        TreeNode root1 = new TreeNode(1);
        root1.left = new TreeNode(2);
        root1.right = new TreeNode(3);

        MaxPathSumSolution solution1 = new MaxPathSumSolution();
        int result1 = solution1.maxPathSum(root1);
        System.out.println("Test Case 1 Result: " + result1); // Expected output: 6

        // 构造测试用例2
        TreeNode root2 = new TreeNode(-10);
        root2.left = new TreeNode(9);
        root2.right = new TreeNode(20);
        root2.right.left = new TreeNode(15);
        root2.right.right = new TreeNode(7);

        MaxPathSumSolution solution2 = new MaxPathSumSolution();
        int result2 = solution2.maxPathSum(root2);
        System.out.println("Test Case 2 Result: " + result2); // Expected output: 42
    }
}

九、图论

51.岛屿数量(中等)

题目描述

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0' 或 '1'

解题思路

这个问题可以通过图遍历算法来解决,具体来说可以使用深度优先搜索(DFS)或者广度优先搜索(BFS)来遍历网格。这里的主要步骤如下:

  1. 初始化:使用一个二维布尔数组 visited 来标记每个位置是否已被访问。

  2. 遍历网格:对于网格中的每一个位置,如果该位置是陆地('1')并且没有被访问过,那么它是一个新的岛屿的起点。

  3. DFS/BFS 遍历

    从当前陆地位置开始,使用 DFS 或 BFS 遍历所有与之相连的陆地,标记这些位置为已访问,以确保不重复计数;在 DFS 或 BFS 中,访问相邻的四个方向(上、下、左、右),如果相邻的位置也是陆地并且没有被访问过,则继续递归或入队。
  4. 计数:每当发现一个新的岛屿的起点时,将岛屿数量加一。

复杂度分析

  • 时间复杂度:O(m * n),其中 m 和 n 分别是网格的行数和列数。每个位置最多被访问一次,因此整体时间复杂度是 O(m * n)。
  • 空间复杂度:O(m * n),主要用于存储 visited 数组和递归调用栈的空间(如果使用 DFS)。如果使用 BFS,则空间复杂度主要体现在队列的使用上。

代码实现

package org.zyf.javabasic.letcode.hot100.graph;

/**
 * @program: zyfboot-javabasic
 * @description: 岛屿数量(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:29
 **/
public class NumIslandsSolution {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) return 0;
        int rows = grid.length;
        int cols = grid[0].length;
        boolean[][] visited = new boolean[rows][cols];
        int numIslands = 0;

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1' && !visited[i][j]) {
                    // Found a new island
                    numIslands++;
                    // Perform DFS to mark all connected land
                    dfs(grid, visited, i, j);
                }
            }
        }

        return numIslands;
    }

    private void dfs(char[][] grid, boolean[][] visited, int row, int col) {
        // Base cases
        if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length || grid[row][col] == '0' || visited[row][col]) {
            return;
        }

        // Mark this cell as visited
        visited[row][col] = true;

        // Visit all adjacent cells (up, down, left, right)
        dfs(grid, visited, row - 1, col); // up
        dfs(grid, visited, row + 1, col); // down
        dfs(grid, visited, row, col - 1); // left
        dfs(grid, visited, row, col + 1); // right
    }

    public static void main(String[] args) {
        NumIslandsSolution solution = new NumIslandsSolution();

        char[][] grid1 = {
                {'1', '1', '1', '1', '0'},
                {'1', '1', '0', '1', '0'},
                {'1', '1', '0', '0', '0'},
                {'0', '0', '0', '0', '0'}
        };
        System.out.println("Number of islands in grid1: " + solution.numIslands(grid1)); // Output: 1

        char[][] grid2 = {
                {'1', '1', '0', '0', '0'},
                {'1', '1', '0', '0', '0'},
                {'0', '0', '1', '0', '0'},
                {'0', '0', '0', '1', '1'}
        };
        System.out.println("Number of islands in grid2: " + solution.numIslands(grid2)); // Output: 3
    }
}

52.腐烂的橘子(中等)

题目描述

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 值 0 代表空单元格;
  • 值 1 代表新鲜橘子;
  • 值 2 代表腐烂的橘子。

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。

示例 1:

输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4

示例 2:输入:grid = [[2,1,1],[0,1,1],[1,0,1]] 输出:-1 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。

示例 3:输入:grid = [[0,2]] 输出:0 解释:因为 0 分钟时已经没新鲜橘子了,所以答案就是 0 。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10
  • grid[i][j] 仅为 01 或 2

解题思路

这个问题可以通过广度优先搜索(BFS)来解决。使用 BFS 可以很好地模拟橘子的腐烂过程,因为 BFS 会逐层扩展,确保每分钟橘子的腐烂过程都被正确地模拟。

  1. 初始化:使用一个队列 queue 来存储所有初始腐烂橘子的坐标;使用一个变量 minutes 来记录所需的时间(分钟数)。

  2. 遍历网格:遍历网格,找到所有初始的腐烂橘子,并将它们的坐标加入队列。

  3. BFS 扩展:每次从队列中取出一个腐烂橘子,尝试将它周围的四个方向的相邻新鲜橘子腐烂;如果发现新鲜橘子腐烂了,将它们加入队列,并更新分钟数。

  4. 检查结果:在 BFS 结束后,检查网格中是否还有未腐烂的新鲜橘子。如果有,返回 -1;否则,返回记录的分钟数。

复杂度分析

  • 时间复杂度:O(m * n),每个单元格最多被访问一次,其中 m 和 n 分别是网格的行数和列数。
  • 空间复杂度:O(m * n),队列的空间复杂度,最坏情况下队列中会存储所有的单元格。

代码实现

package org.zyf.javabasic.letcode.hot100.graph;

import java.util.LinkedList;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 腐烂的橘子(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 12:56
 **/
public class OrangesRottingSolution {
    public int orangesRotting(int[][] grid) {
        // 获取网格的行数和列数
        int m = grid.length;
        int n = grid[0].length;

        // 用于保存新鲜橘子的位置
        Queue<int[]> queue = new LinkedList<>();
        // 记录新鲜橘子的数量
        int freshCount = 0;

        // 遍历整个网格
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 如果当前单元格是腐烂的橘子
                if (grid[i][j] == 2) {
                    queue.add(new int[]{i, j});
                }
                // 如果当前单元格是新鲜的橘子
                else if (grid[i][j] == 1) {
                    freshCount++;
                }
            }
        }

        // 如果没有新鲜橘子,直接返回0
        if (freshCount == 0) return 0;

        // 记录时间步数
        int minutes = 0;
        // 4个方向的移动数组
        int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

        // BFS遍历
        while (!queue.isEmpty()) {
            int size = queue.size();
            // 对当前时间步的所有腐烂橘子进行处理
            for (int i = 0; i < size; i++) {
                int[] cell = queue.poll();
                int x = cell[0];
                int y = cell[1];

                // 遍历4个方向
                for (int[] dir : directions) {
                    int newX = x + dir[0];
                    int newY = y + dir[1];

                    // 检查新位置是否在网格内且是新鲜橘子
                    if (newX >= 0 && newX < m && newY >= 0 && newY < n && grid[newX][newY] == 1) {
                        // 将新鲜橘子腐烂
                        grid[newX][newY] = 2;
                        // 将腐烂的橘子位置添加到队列
                        queue.add(new int[]{newX, newY});
                        // 新鲜橘子数量减少
                        freshCount--;
                    }
                }
            }
            // 如果队列不为空,增加时间步数
            if (!queue.isEmpty()) {
                minutes++;
            }
        }

        // 如果还有新鲜橘子未腐烂,返回-1
        return freshCount == 0 ? minutes : -1;
    }

    public static void main(String[] args) {
        OrangesRottingSolution solution = new OrangesRottingSolution();

        // 示例 1
        int[][] grid1 = {
                {2, 1, 1},
                {1, 1, 0},
                {0, 1, 1}
        };
        System.out.println(solution.orangesRotting(grid1)); // 输出: 4

        // 示例 2
        int[][] grid2 = {
                {2, 1, 1},
                {0, 1, 1},
                {1, 0, 1}
        };
        System.out.println(solution.orangesRotting(grid2)); // 输出: -1

        // 示例 3
        int[][] grid3 = {
                {0, 2}
        };
        System.out.println(solution.orangesRotting(grid3)); // 输出: 0
    }
}

53.课程表(中等)

题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:输入:numCourses = 2, prerequisites = [[1,0]] 输出:true 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:输入:numCourses = 2, prerequisites = [[1,0],[0,1]] 输出:false 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解题思路

要解决这个问题,我们可以使用图的概念,将课程作为图中的节点,先修课程关系作为有向边。问题转化为判断图中是否存在环。如果图中存在环,则无法完成所有课程,因为存在循环依赖;否则,课程可以完成。

具体来说,解决这个问题可以使用拓扑排序算法,常用的方法包括 Kahn's Algorithm(基于入度的拓扑排序)和深度优先搜索(DFS)来检测图中的环。

解题思路

  1. 构建图:使用邻接表表示图;记录每个课程的入度(指向该课程的边数)。

  2. 拓扑排序

    使用 Kahn's Algorithm(基于入度):
    • 记录处理过的节点数。
    • 如果邻接节点的入度变为 0,将其加入队列。
    • 从队列中取出节点,减少其邻接节点的入度。
    • 初始化一个队列,将所有入度为 0 的节点加入队列。
  3. 判断是否存在环:如果处理过的节点数等于课程总数,则返回 true;否则,返回 false,因为存在环导致无法完成所有课程。

复杂度分析

  • 时间复杂度:O(V + E),其中 V 是课程的数量,E 是先修课程对的数量。每个节点和边都只被处理一次。
  • 空间复杂度:O(V + E),用于存储图的邻接表和入度数组。

代码实现

package org.zyf.javabasic.letcode.hot100.graph;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * @program: zyfboot-javabasic
 * @description: 课程表(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:01
 **/
public class CanFinishSolution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 创建图的邻接表和入度数组
        List<Integer>[] graph = new ArrayList[numCourses];
        int[] inDegree = new int[numCourses];

        for (int i = 0; i < numCourses; i++) {
            graph[i] = new ArrayList<>();
        }

        // 构建图和入度数组
        for (int[] prereq : prerequisites) {
            int course = prereq[0];
            int preReq = prereq[1];
            graph[preReq].add(course);
            inDegree[course]++;
        }

        // 使用队列进行拓扑排序
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        int count = 0; // 记录处理的节点数

        while (!queue.isEmpty()) {
            int course = queue.poll();
            count++;
            for (int nextCourse : graph[course]) {
                inDegree[nextCourse]--;
                if (inDegree[nextCourse] == 0) {
                    queue.offer(nextCourse);
                }
            }
        }

        // 如果处理的节点数等于课程总数,则可以完成所有课程
        return count == numCourses;
    }

    public static void main(String[] args) {
        CanFinishSolution solution = new CanFinishSolution();

        // 示例 1
        int numCourses1 = 2;
        int[][] prerequisites1 = {{1, 0}};
        System.out.println(solution.canFinish(numCourses1, prerequisites1)); // 输出: true

        // 示例 2
        int numCourses2 = 2;
        int[][] prerequisites2 = {{1, 0}, {0, 1}};
        System.out.println(solution.canFinish(numCourses2, prerequisites2)); // 输出: false
    }
}

54.实现 Trie (前缀树)(中等)

题目描述

Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word 。
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True

提示:

  • 1 <= word.length, prefix.length <= 2000
  • word 和 prefix 仅由小写英文字母组成
  • insertsearch 和 startsWith 调用次数 总计 不超过 3 * 104 次

解题思路

Trie(前缀树)是一种高效的树形数据结构,专门用于处理字符串的前缀匹配问题。实现 Trie 需要以下几个基本操作:

  1. 插入字符串(insert:从根节点开始,逐字符遍历字符串。如果字符对应的子节点不存在,则创建该子节点;插入完成后,可以在最后一个字符节点上标记该字符串的结束。

  2. 搜索字符串(search:从根节点开始,逐字符遍历字符串。如果某个字符的子节点不存在,则说明该字符串不在 Trie 中;遍历完所有字符后,检查最后一个字符节点是否标记了该字符串的结束。

  3. 检查前缀(startsWith:从根节点开始,逐字符遍历前缀。如果某个字符的子节点不存在,则说明没有以该前缀开头的字符串;遍历完所有字符后,只要路径存在即返回 true

Trie 数据结构

Trie 数据结构的基本组成包括:

  • TrieNode 类:表示 Trie 树的节点,包含一个 children 字典(子节点)和一个布尔值 isEndOfWord(标记是否为一个单词的结束)。
  • Trie 类:包含插入、搜索和前缀检查操作的方法。

复杂度分析

  • 时间复杂度insertsearch 操作的时间复杂度为 O(L),其中 L 是字符串的长度;startsWith 操作的时间复杂度为 O(P),其中 P 是前缀的长度。

  • 空间复杂度:Trie 的空间复杂度取决于插入的单词数和每个单词的长度。最坏情况下,空间复杂度为 O(N * L),其中 N 是单词的数量,L 是单词的平均长度。

代码实现

package org.zyf.javabasic.letcode.hot100.graph;

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

/**
 * @program: zyfboot-javabasic
 * @description: 实现 Trie (前缀树)(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:07
 **/
public class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    // 插入一个单词到 Trie 中
    public void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            // 如果当前节点没有该字符的子节点,创建一个新节点
            if (!node.children.containsKey(c)) {
                node.children.put(c, new TrieNode());
            }
            // 移动到下一个节点
            node = node.children.get(c);
        }
        // 标记单词的结尾
        node.isEndOfWord = true;
    }

    // 检索单词是否在 Trie 中
    public boolean search(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            // 如果当前节点没有该字符的子节点,单词不存在
            if (!node.children.containsKey(c)) {
                return false;
            }
            // 移动到下一个节点
            node = node.children.get(c);
        }
        // 返回是否为单词的结尾
        return node.isEndOfWord;
    }

    // 检查是否有单词以给定前缀开头
    public boolean startsWith(String prefix) {
        TrieNode node = root;
        for (char c : prefix.toCharArray()) {
            // 如果当前节点没有该字符的子节点,前缀不存在
            if (!node.children.containsKey(c)) {
                return false;
            }
            // 移动到下一个节点
            node = node.children.get(c);
        }
        // 前缀存在
        return true;
    }

    class TrieNode {
        // 子节点映射
        Map<Character, TrieNode> children;
        // 是否为单词的结尾
        boolean isEndOfWord;

        public TrieNode() {
            children = new HashMap<>();
            isEndOfWord = false;
        }
    }

    public static void main(String[] args) {
        Trie trie = new Trie();

        // 测试插入和搜索
        trie.insert("apple");
        System.out.println(trie.search("apple")); // 输出: true
        System.out.println(trie.search("app"));   // 输出: false
        System.out.println(trie.startsWith("app")); // 输出: true
        trie.insert("app");
        System.out.println(trie.search("app"));   // 输出: true
    }
}

十、回溯

55.全排列(中等)

题目描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

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

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

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

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

解题思路

要生成给定数组 nums 的所有可能的全排列,可以使用递归回溯算法来探索所有的排列组合。全排列问题的解决可以通过以下步骤:

  1. 递归回溯

    基准情况:当所有的数字都被使用时,将当前排列加入结果集中。递归情况:选择一个数字作为当前排列的一部分,递归地生成剩余数字的全排列,并在递归调用完成后撤销选择,以探索其他可能的排列。
  2. 交换元素

    使用交换操作来生成不同的排列。每次选择一个数字作为当前排列的一部分,然后递归处理剩余的数字。

复杂度分析

  • 时间复杂度:O(n!),其中 n 是数组 nums 的长度。生成全排列的时间复杂度是阶乘级别,因为每个排列的生成都涉及到递归调用和元素交换。
  • 空间复杂度:O(n),用于存储递归调用栈和 used 数组,result 列表的空间复杂度为 O(n!),用于存储所有的排列结果。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 全排列(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:12
 **/
public class PermuteSolution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, new ArrayList<>(), result, new boolean[nums.length]);
        return result;
    }

    private void backtrack(int[] nums, List<Integer> current, List<List<Integer>> result, boolean[] used) {
        if (current.size() == nums.length) {
            result.add(new ArrayList<>(current));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue; // Skip used elements
            used[i] = true; // Mark as used
            current.add(nums[i]); // Add current number to the permutation
            backtrack(nums, current, result, used); // Recur
            current.remove(current.size() - 1); // Backtrack
            used[i] = false; // Unmark as used
        }
    }

    public static void main(String[] args) {
        PermuteSolution solution = new PermuteSolution();
        int[] nums1 = {1, 2, 3};
        System.out.println(solution.permute(nums1)); // [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

        int[] nums2 = {0, 1};
        System.out.println(solution.permute(nums2)); // [[0, 1], [1, 0]]

        int[] nums3 = {1};
        System.out.println(solution.permute(nums3)); // [[1]]
    }
}

56.子集(中等)

题目描述

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

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

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

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

解题思路

要生成给定数组 nums 的所有可能子集(即幂集),可以使用递归回溯算法。由于题目中的元素互不相同,这意味着我们不需要处理重复的子集。

  1. 递归回溯

    基准情况:当处理完所有元素时,将当前子集加入结果集中。递归情况:撤销选择(即回溯),以探索其他可能的子集;选择当前元素,将其加入子集,并递归处理下一个元素。
  2. 子集生成:递归生成所有包含当前元素的子集和所有不包含当前元素的子集。

复杂度分析

  • 时间复杂度:O(2^n),其中 n 是数组 nums 的长度。生成所有子集的时间复杂度是指数级别,因为每个元素可以选择加入或不加入子集。
  • 空间复杂度:O(n * 2^n),用于存储所有子集和递归调用栈。result 列表存储了所有的子集,共有 2^n 个子集,每个子集最多包含 n 个元素,因此空间复杂度为 O(n * 2^n)。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 子集(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:17
 **/
public class SubsetsSolution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int[] nums, int start, List<Integer> current, List<List<Integer>> result) {
        // 将当前子集加入结果集中
        result.add(new ArrayList<>(current));

        // 从当前位置开始遍历所有元素
        for (int i = start; i < nums.length; i++) {
            current.add(nums[i]); // 选择当前元素
            backtrack(nums, i + 1, current, result); // 递归处理下一个元素
            current.remove(current.size() - 1); // 撤销选择(即回溯)
        }
    }

    public static void main(String[] args) {
        SubsetsSolution solution = new SubsetsSolution();
        int[] nums1 = {1, 2, 3};
        System.out.println(solution.subsets(nums1)); // [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

        int[] nums2 = {0};
        System.out.println(solution.subsets(nums2)); // [[], [0]]
    }
}

57.电话号码的字母组合(中等)

题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:输入:digits = "" 输出:[]

示例 3:输入:digits = "2" 输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

解题思路

要解决这个问题,我们可以使用递归回溯算法来生成所有可能的字母组合。每个数字(2-9)对应着一定的字母,这些字母可以用于生成字母组合。我们需要根据输入的数字字符串来生成所有可能的组合。

  1. 映射关系:创建一个映射,将每个数字(2-9)映射到其对应的字母列表。
  2. 递归回溯基准情况---当处理完所有数字时,将当前生成的字母组合加入结果列表;递归情况---对于当前数字对应的每个字母,递归生成剩余数字的所有可能的组合。

复杂度分析

  • 时间复杂度:O(3^N * 4^M),其中 N 是输入中数字 2-6 的个数,M 是数字 7-9 的个数。每个数字有不同数量的字母可能性。
  • 空间复杂度:O(3^N * 4^M * N),主要用于存储结果和递归调用栈。每种组合的长度为 N,并且可能的组合总数为 3^N * 4^M。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 电话号码的字母组合(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:21
 **/
public class LetterCombinationsSolution {
    // 映射数字到字母
    private final String[] mapping = {
            "",    // 0
            "",    // 1
            "abc", // 2
            "def", // 3
            "ghi", // 4
            "jkl", // 5
            "mno", // 6
            "pqrs",// 7
            "tuv", // 8
            "wxyz" // 9
    };

    public List<String> letterCombinations(String digits) {
        List<String> result = new ArrayList<>();
        if (digits == null || digits.length() == 0) {
            return result;
        }
        backtrack(result, new StringBuilder(), digits, 0);
        return result;
    }

    private void backtrack(List<String> result, StringBuilder current, String digits, int index) {
        // 如果当前组合的长度等于输入的数字长度,添加到结果中
        if (index == digits.length()) {
            result.add(current.toString());
            return;
        }

        // 获取当前数字对应的字母
        String letters = mapping[digits.charAt(index) - '0'];

        // 遍历当前数字对应的每个字母
        for (char letter : letters.toCharArray()) {
            current.append(letter); // 选择当前字母
            backtrack(result, current, digits, index + 1); // 递归处理下一个数字
            current.deleteCharAt(current.length() - 1); // 撤销选择(回溯)
        }
    }

    public static void main(String[] args) {
        LetterCombinationsSolution solution = new LetterCombinationsSolution();
        System.out.println(solution.letterCombinations("23")); // ["ad","ae","af","bd","be","bf","cd","ce","cf"]
        System.out.println(solution.letterCombinations(""));  // []
        System.out.println(solution.letterCombinations("2")); // ["a","b","c"]
    }
}

58.组合总和(中等)

题目描述

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:输入:candidates = [2,3,6,7], target = 7 输出:[[2,2,3],[7]] 解释: 2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。

示例 2:输入: candidates = [2,3,5], target = 8 输出: [[2,2,2,2],[2,3,3],[3,5]]

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

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

解题思路

为了找出数组 candidates 中所有和为目标整数 target 的不同组合,我们可以使用回溯算法:

  1. 从候选数组中选择一个元素,将其加入当前组合。
  2. 减去该元素的值,递归地继续寻找目标值。
  3. 当目标值为 0 时,当前组合符合条件,添加到结果中。
  4. 当目标值小于 0 时,当前组合无效,返回上一步尝试其他可能性。
  5. 由于可以重复使用元素,因此在递归时,不移动到下一个元素,而是继续使用当前元素。

复杂度分析

  • 时间复杂度:回溯算法的时间复杂度较高,最坏情况下可能需要遍历所有可能的组合。时间复杂度大致为 O(2^N),其中 N 是候选数组的长度。
  • 空间复杂度:主要由递归栈和存储结果的空间组成。递归栈的最大深度为 O(T/M),其中 T 是目标值,M 是最小的候选值。结果空间的大小取决于解的数量。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 组合总和(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:26
 **/
public class CombinationSumSolution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(result, new ArrayList<>(), candidates, target, 0);
        return result;
    }

    private void backtrack(List<List<Integer>> result, List<Integer> current, int[] candidates, int target, int start) {
        // 如果目标值为0,则当前组合是一个有效解
        if (target == 0) {
            result.add(new ArrayList<>(current));
            return;
        }

        // 如果目标值小于0,当前组合无效,返回
        if (target < 0) {
            return;
        }

        // 从当前索引开始遍历候选数组
        for (int i = start; i < candidates.length; i++) {
            // 选择当前元素
            current.add(candidates[i]);
            // 递归,注意传递 i 而不是 i + 1,因为可以重复使用当前元素
            backtrack(result, current, candidates, target - candidates[i], i);
            // 撤销选择
            current.remove(current.size() - 1);
        }
    }

    public static void main(String[] args) {
        CombinationSumSolution solution = new CombinationSumSolution();
        System.out.println(solution.combinationSum(new int[]{2, 3, 6, 7}, 7)); // [[2,2,3],[7]]
        System.out.println(solution.combinationSum(new int[]{2, 3, 5}, 8)); // [[2,2,2,2],[2,3,3],[3,5]]
        System.out.println(solution.combinationSum(new int[]{2}, 1)); // []
    }
}

59.括号生成(中等)

题目描述

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:输入:n = 1 输出:["()"]

提示:

  • 1 <= n <= 8

解题思路

要生成所有可能且有效的括号组合,可以使用回溯算法(backtracking)。括号组合的有效性:一个有效的括号组合必须满足:在任何时刻,左括号的数量不能少于右括号的数量。

  1. 从一个空字符串开始构建括号组合。
  2. 使用两个计数器:一个跟踪左括号的数量(leftCount),另一个跟踪右括号的数量(rightCount)。
  3. 当左括号数量少于 n 时,添加一个左括号,并递归调用。
  4. 当右括号数量少于左括号数量时,添加一个右括号,并递归调用。
  5. 当左括号和右括号都用完时,将当前组合加入结果列表。

复杂度分析

  • 时间复杂度:生成所有有效括号组合的时间复杂度为 O(4^n / √n),其中 n 是括号对的数量。这是因为括号组合的数量是由 Catalan 数(C_n)决定的。
  • 空间复杂度:主要由递归栈和存储结果的空间组成。递归栈的最大深度为 O(n),结果空间的大小取决于生成的组合数量。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 括号生成(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:31
 **/
public class GenerateParenthesisSolution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        backtrack(result, "", 0, 0, n);
        return result;
    }

    private void backtrack(List<String> result, String current, int leftCount, int rightCount, int n) {
        // 当当前组合的长度等于 2 * n 时,说明已经生成了一个有效的组合
        if (current.length() == 2 * n) {
            result.add(current);
            return;
        }

        // 只要左括号的数量小于 n,就可以添加一个左括号
        if (leftCount < n) {
            backtrack(result, current + "(", leftCount + 1, rightCount, n);
        }

        // 只要右括号的数量小于左括号的数量,就可以添加一个右括号
        if (rightCount < leftCount) {
            backtrack(result, current + ")", leftCount, rightCount + 1, n);
        }
    }

    public static void main(String[] args) {
        GenerateParenthesisSolution solution = new GenerateParenthesisSolution();
        System.out.println(solution.generateParenthesis(3)); // ["((()))","(()())","(())()","()(())","()()()"]
        System.out.println(solution.generateParenthesis(1)); // ["()"]
    }
}

60.单词搜索(中等)

题目描述

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true

示例 3:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false

提示:

  • m == board.length
  • n = board[i].length
  • 1 <= m, n <= 6
  • 1 <= word.length <= 15
  • board 和 word 仅由大小写英文字母组成

进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?

解题思路

  • 探索路径:从当前单元格出发,尝试匹配目标单词的当前字符;递归地探索四个方向(上、下、左、右)中的每一个方向。

  • 标记访问状态:在进入递归之前,将当前单元格标记为访问状态,防止重复访问;递归调用完成后,将单元格标记恢复为未访问状态,以便其他路径能够访问这个单元格。

  • 回溯:在探索完成后,撤销之前的操作(标记恢复),继续探索其他路径。

复杂度分析

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

/**
 * @program: zyfboot-javabasic
 * @description: 单词搜索(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:38
 **/
public class ExistSolution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board.length == 0 || board[0].length == 0) {
            return false;
        }

        int m = board.length;
        int n = board[0].length;
        boolean[][] visited = new boolean[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, word, i, j, 0, visited)) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean dfs(char[][] board, String word, int i, int j, int index, boolean[][] visited) {
        // 检查边界条件和字符匹配
        if (index == word.length()) {
            return true;
        }
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length ||
                visited[i][j] || board[i][j] != word.charAt(index)) {
            return false;
        }

        // 标记当前单元格为访问过
        visited[i][j] = true;

        // 尝试四个方向
        boolean result = dfs(board, word, i + 1, j, index + 1, visited) ||
                dfs(board, word, i - 1, j, index + 1, visited) ||
                dfs(board, word, i, j + 1, index + 1, visited) ||
                dfs(board, word, i, j - 1, index + 1, visited);

        // 回溯:恢复当前单元格为未访问状态
        visited[i][j] = false;

        return result;
    }

    public static void main(String[] args) {
        ExistSolution solution = new ExistSolution();
        char[][] board1 = {
                {'A','B','C','E'},
                {'S','F','C','S'},
                {'A','D','E','E'}
        };
        System.out.println(solution.exist(board1, "ABCCED")); // true

        char[][] board2 = {
                {'A','B','C','E'},
                {'S','F','C','S'},
                {'A','D','E','E'}
        };
        System.out.println(solution.exist(board2, "SEE")); // true

        char[][] board3 = {
                {'A','B','C','E'},
                {'S','F','C','S'},
                {'A','D','E','E'}
        };
        System.out.println(solution.exist(board3, "ABCB")); // false
    }
}

61.分割回文串(中等)

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串。返回 s 所有可能的分割方案。

示例 1:输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]

示例 2:输入:s = "a" 输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

解题思路

要解决这个问题,我们需要生成字符串 s 的所有可能的回文分割方案。回文分割的核心在于每个分割出来的子串都是一个回文串,可以通过回溯算法来解决这个问题:

  1. 回溯算法:使用回溯算法来尝试不同的分割方式;从当前位置开始,尝试将子串切分为回文串,并递归地继续分割剩下的字符串。

  2. 回文判断:在每次切分子串时,需要判断当前子串是否是回文串。

  3. 回溯步骤

    • 从字符串的起始位置开始,尝试所有可能的子串。
    • 如果当前子串是回文串,则继续递归地处理剩余的字符串。
    • 递归到字符串的末尾时,记录当前的分割方案。

复杂度分析

  • 时间复杂度:回溯算法的时间复杂度在最坏情况下是 O(2^n),其中 n 是字符串的长度,因为每个字符都有可能在回溯过程中成为回文的开始。
  • 空间复杂度:包括递归栈空间和存储结果的空间。递归栈空间复杂度是 O(n),结果存储的空间复杂度是 O(2^n)(考虑到所有可能的子集)。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: 分割回文串(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:43
 **/
public class PartitionSolution {
    public List<List<String>> partition(String s) {
        List<List<String>> result = new ArrayList<>();
        List<String> currentList = new ArrayList<>();
        backtrack(s, 0, currentList, result);
        return result;
    }

    private void backtrack(String s, int start, List<String> currentList, List<List<String>> result) {
        if (start == s.length()) {
            result.add(new ArrayList<>(currentList));
            return;
        }

        for (int end = start + 1; end <= s.length(); end++) {
            String substring = s.substring(start, end);
            if (isPalindrome(substring)) {
                currentList.add(substring);
                backtrack(s, end, currentList, result);
                currentList.remove(currentList.size() - 1);
            }
        }
    }

    private boolean isPalindrome(String s) {
        int left = 0;
        int right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

    public static void main(String[] args) {
        PartitionSolution solution = new PartitionSolution();

        // 示例 1
        String s1 = "aab";
        System.out.println(solution.partition(s1));
        // 输出: [["a","a","b"],["aa","b"]]

        // 示例 2
        String s2 = "a";
        System.out.println(solution.partition(s2));
        // 输出: [["a"]]
    }
}

62.N 皇后 (困难)

题目描述

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:输入:n = 1 输出:[["Q"]]

提示:

  • 1 <= n <= 9

解题思路

解决 n 皇后问题可以使用回溯算法来尝试所有可能的皇后放置方案,并且确保每个皇后都满足不攻击其他皇后的条件。

  1. 回溯算法:从棋盘的第一行开始,尝试将皇后放置在该行的每一个位置;对每个位置进行尝试时,递归地处理下一行;需要保证当前放置的皇后不与已经放置的皇后相互攻击(即不在同一列、同一行或同一斜线上)。

  2. 攻击检查:使用三个集合来记录已被攻击的列、主对角线和副对角线;主对角线和副对角线可以通过索引计算得出,分别为 row - colrow + col

  3. 回溯步骤:在每行尝试将皇后放置在每个位置,若成功则递归处理下一行;如果成功放置了所有皇后,则将棋盘状态加入结果列表;在回溯阶段,需要将当前放置的皇后移除,恢复状态以便尝试其他位置。

复杂度分析

  • 时间复杂度:最坏情况下,回溯算法的时间复杂度是 O(n!)(由于每行都有 n 种选择,每个选择都可能导致递归)。
  • 空间复杂度:包括递归栈的空间复杂度和存储结果的空间复杂度。递归栈的空间复杂度是 O(n),结果存储的空间复杂度是 O(n!)。

代码实现

package org.zyf.javabasic.letcode.hot100.backtracking;

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

/**
 * @program: zyfboot-javabasic
 * @description: N 皇后(困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:48
 **/
public class SolveNQueensSolution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> result = new ArrayList<>();
        // 用来记录列的状态
        boolean[] cols = new boolean[n];
        // 用来记录主对角线的状态
        boolean[] diag1 = new boolean[2 * n - 1];
        // 用来记录副对角线的状态
        boolean[] diag2 = new boolean[2 * n - 1];
        // 临时棋盘,用来记录皇后的放置位置
        char[][] board = new char[n][n];

        // 初始化棋盘
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                board[i][j] = '.';
            }
        }

        // 从第一行开始回溯
        backtrack(result, board, 0, n, cols, diag1, diag2);
        return result;
    }

    private void backtrack(List<List<String>> result, char[][] board, int row, int n, boolean[] cols, boolean[] diag1, boolean[] diag2) {
        if (row == n) {
            result.add(construct(board));
            return;
        }

        for (int col = 0; col < n; col++) {
            if (cols[col] || diag1[row - col + n - 1] || diag2[row + col]) {
                continue;
            }

            // 放置皇后
            board[row][col] = 'Q';
            cols[col] = true;
            diag1[row - col + n - 1] = true;
            diag2[row + col] = true;

            // 递归处理下一行
            backtrack(result, board, row + 1, n, cols, diag1, diag2);

            // 撤回皇后
            board[row][col] = '.';
            cols[col] = false;
            diag1[row - col + n - 1] = false;
            diag2[row + col] = false;
        }
    }

    private List<String> construct(char[][] board) {
        List<String> result = new ArrayList<>();
        for (char[] row : board) {
            result.add(new String(row));
        }
        return result;
    }

    public static void main(String[] args) {
        SolveNQueensSolution solution = new SolveNQueensSolution();

        // 示例 1
        int n1 = 4;
        System.out.println(solution.solveNQueens(n1));
        // 输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]

        // 示例 2
        int n2 = 1;
        System.out.println(solution.solveNQueens(n2));
        // 输出: [["Q"]]
    }
}

十一、二分查找

63.搜索插入位置(简单)

题目描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

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

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

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

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 为 无重复元素 的 升序 排列数组
  • -104 <= target <= 104

解题思路

  • 二分查找算法

    • 通过将查找范围不断减半,快速定位目标值的位置或确定目标值的插入位置。
    • 如果目标值存在于数组中,返回其索引。
    • 如果目标值不存在于数组中,返回其应该插入的位置,以保持数组的排序。
  • 步骤

    • 使用两个指针 leftright 来定义当前的查找范围。
    • 计算中间位置 mid
    • 如果 nums[mid] 等于目标值,则返回 mid
    • 如果目标值小于 nums[mid],则调整查找范围为左半部分。
    • 如果目标值大于 nums[mid],则调整查找范围为右半部分。
    • 如果结束查找范围(left 大于 right),则 left 指针所在的位置就是目标值应该插入的位置。

复杂度分析

  • 时间复杂度: O(log⁡n)O(\log n)O(logn),因为每次查找范围减半。
  • 空间复杂度: O(1)O(1)O(1),只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 搜索插入位置(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:54
 **/
public class SearchInsertSolution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return left;
    }

    public static void main(String[] args) {
        SearchInsertSolution solution = new SearchInsertSolution();

        // 示例 1
        int[] nums1 = {1, 3, 5, 6};
        int target1 = 5;
        System.out.println(solution.searchInsert(nums1, target1)); // 输出: 2

        // 示例 2
        int[] nums2 = {1, 3, 5, 6};
        int target2 = 2;
        System.out.println(solution.searchInsert(nums2, target2)); // 输出: 1

        // 示例 3
        int[] nums3 = {1, 3, 5, 6};
        int target3 = 7;
        System.out.println(solution.searchInsert(nums3, target3)); // 输出: 4
    }
}

64.搜索二维矩阵(中等)

题目描述

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。

示例 1:

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

示例 2:

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 100
  • -104 <= matrix[i][j], target <= 104

解题思路

要在满足条件的 m×nm \times nm×n 整数矩阵中查找目标值 target,可以利用矩阵的递增特性,进行高效查找。从矩阵的右上角开始查找

  1. 从矩阵的右上角开始查找。
  2. 如果当前元素等于目标值,返回 true
  3. 如果当前元素大于目标值,向左移动(因为当前元素过大)。
  4. 如果当前元素小于目标值,向下移动(因为当前元素过小)。
  5. 如果超出矩阵的边界,返回 false

复杂度分析

  • 时间复杂度: O(m+n)O(m + n)O(m+n)。
  • 空间复杂度: O(1)O(1)O(1),使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 搜索二维矩阵(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 13:59
 **/
public class SearchMatrixSolution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return false;
        }

        int m = matrix.length;
        int n = matrix[0].length;
        int row = 0;
        int col = n - 1;

        while (row < m && col >= 0) {
            if (matrix[row][col] == target) {
                return true;
            } else if (matrix[row][col] > target) {
                col--;
            } else {
                row++;
            }
        }

        return false;
    }

    public static void main(String[] args) {
        SearchMatrixSolution solution = new SearchMatrixSolution();

        // 示例 1
        int[][] matrix1 = {
                {1, 3, 5, 7},
                {10, 11, 16, 20},
                {23, 30, 34, 60}
        };
        int target1 = 3;
        System.out.println(solution.searchMatrix(matrix1, target1)); // 输出: true

        // 示例 2
        int[][] matrix2 = {
                {1, 3, 5, 7},
                {10, 11, 16, 20},
                {23, 30, 34, 60}
        };
        int target2 = 13;
        System.out.println(solution.searchMatrix(matrix2, target2)); // 输出: false
    }
}

65.在排序数组中查找元素的第一个和最后一个位置(中等)

题目描述

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

示例 2:输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

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

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

解题思路

为了在一个按照非递减顺序排列的整数数组 nums 中找出目标值 target 的开始位置和结束位置,可以利用二分查找来实现 O(log⁡n) 的时间复杂度,二分查找能有效地定位到目标值的左边界和右边界。

  1. 二分查找的两个变种

    找左边界:找到目标值 target 在数组中的第一个出现位置;找右边界:找到目标值 target 在数组中的最后一个出现位置。
  2. 实现步骤

    • 首先,使用二分查找找出目标值 target 的左边界。
    • 然后,使用二分查找找出目标值 target 的右边界。
    • 如果找到了目标值,返回它的左边界和右边界;否则,返回 [-1, -1]

复杂度分析

  • 时间复杂度: O(log⁡n),每个二分查找的时间复杂度为 O(log⁡n),总共执行两次。
  • 空间复杂度: O(1),只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 在排序数组中查找元素的第一个和最后一个位置(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:04
 **/
public class SearchRangeSolution {
    public int[] searchRange(int[] nums, int target) {
        int[] result = {-1, -1};
        if (nums == null || nums.length == 0) {
            return result;
        }

        // 找到目标值的左边界
        int left = findLeft(nums, target);
        if (left == -1) {
            return result; // 如果左边界都找不到,说明目标值不在数组中
        }

        // 找到目标值的右边界
        int right = findRight(nums, target);
        return new int[]{left, right};
    }

    private int findLeft(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        int index = -1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                if (nums[mid] == target) {
                    index = mid;
                }
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        return index;
    }

    private int findRight(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        int index = -1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] <= target) {
                if (nums[mid] == target) {
                    index = mid;
                }
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return index;
    }

    public static void main(String[] args) {
        SearchRangeSolution solution = new SearchRangeSolution();

        // 示例 1
        int[] nums1 = {5, 7, 7, 8, 8, 10};
        int target1 = 8;
        int[] result1 = solution.searchRange(nums1, target1);
        System.out.println("Output: [" + result1[0] + "," + result1[1] + "]"); // 输出: [3,4]

        // 示例 2
        int[] nums2 = {5, 7, 7, 8, 8, 10};
        int target2 = 6;
        int[] result2 = solution.searchRange(nums2, target2);
        System.out.println("Output: [" + result2[0] + "," + result2[1] + "]"); // 输出: [-1,-1]

        // 示例 3
        int[] nums3 = {};
        int target3 = 0;
        int[] result3 = solution.searchRange(nums3, target3);
        System.out.println("Output: [" + result3[0] + "," + result3[1] + "]"); // 输出: [-1,-1]
    }
}

66.搜索旋转排序数组(中等)

题目描述

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 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
  • -104 <= nums[i] <= 104
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -104 <= target <= 104

解题思路

  1. 数组特点:数组被旋转后,分成两个有序的部分;一个部分是递增的,另一个部分也是递增的(但通常在旋转后前后不连贯)。

  2. 二分查找的改进:使用二分查找来决定在旋转后的哪一部分继续查找;比较中间值与目标值和边界值,以确定当前搜索区间的有序性,从而决定在旋转数组的哪一部分进行进一步的搜索。

实现步骤

  1. 初始化:设定 left 为数组的起始索引,right 为数组的结束索引。

  2. 二分查找:计算中间索引 mid;判断 mid 的值和目标值之间的关系;根据 mid 所在的部分(左半部分或右半部分)来决定搜索区间的更新。

  3. 搜索区间的更新:判断当前区间是否是有序的;根据目标值是否在有序部分内来决定更新哪一部分的区间。

复杂度分析

  • 时间复杂度: O(log⁡n),二分查找的每一步都将搜索范围缩小一半。
  • 空间复杂度: O(1),只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 搜索旋转排序数组(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:09
 **/
public class SearchSolution {
    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[left] <= nums[mid]) { // 左半部分是有序的
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1; // 目标在左半部分
                } else {
                    left = mid + 1; // 目标在右半部分
                }
            } else { // 右半部分是有序的
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1; // 目标在右半部分
                } else {
                    right = mid - 1; // 目标在左半部分
                }
            }
        }

        return -1; // 目标值不在数组中
    }

    public static void main(String[] args) {
        SearchSolution solution = new SearchSolution();

        // 示例 1
        int[] nums1 = {4, 5, 6, 7, 0, 1, 2};
        int target1 = 0;
        System.out.println("Output: " + solution.search(nums1, target1)); // 输出: 4

        // 示例 2
        int[] nums2 = {4, 5, 6, 7, 0, 1, 2};
        int target2 = 3;
        System.out.println("Output: " + solution.search(nums2, target2)); // 输出: -1

        // 示例 3
        int[] nums3 = {1};
        int target3 = 0;
        System.out.println("Output: " + solution.search(nums3, target3)); // 输出: -1
    }
}

67.寻找旋转排序数组中的最小值(中等)

题目描述

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 3 次得到输入数组。

示例 3:输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

解题思路

旋转排序数组是一个被旋转过的升序数组,因此可以利用旋转的性质来优化查找过程。

  1. 旋转数组的特点

    数组被旋转后,原来的升序数组被分成两个部分,其中一个部分是递增的,而另一个部分也是递增的;旋转后的数组中,最小元素总是出现在两个递增部分的交界处。
  2. 二分查找的改进:使用二分查找来决定在哪一部分继续查找最小元素;比较中间值与 leftright 值,判断中间值和目标值的关系来更新查找范围。

实现步骤

  1. 初始化:设定 left 为数组的起始索引,right 为数组的结束索引。

  2. 二分查找:计算中间索引 mid;比较 mid 的值与 leftright 的值,以决定搜索区间的更新。

  3. 判断旋转位置:如果 nums[mid] 是最小值,则直接返回 nums[mid];根据 nums[left]nums[mid] 的值判断旋转的部分,从而决定更新 leftright

复杂度分析

  • 时间复杂度: O(log⁡n),每次将搜索范围缩小一半。
  • 空间复杂度: O(1),只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 寻找旋转排序数组中的最小值(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:13
 **/
public class FindMinSolution {
    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]) {
                right = mid;
            } else {
                // 如果中间元素大于右端元素,则最小值在右侧
                left = mid + 1;
            }
        }

        return nums[left];
    }

    public static void main(String[] args) {
        FindMinSolution solution = new FindMinSolution();

        // 示例 1
        int[] nums1 = {3, 4, 5, 1, 2};
        System.out.println("Output: " + solution.findMin(nums1)); // 输出: 1

        // 示例 2
        int[] nums2 = {4, 5, 6, 7, 0, 1, 2};
        System.out.println("Output: " + solution.findMin(nums2)); // 输出: 0

        // 示例 3
        int[] nums3 = {11, 13, 15, 17};
        System.out.println("Output: " + solution.findMin(nums3)); // 输出: 11
    }
}

68.寻找两个正序数组的中位数 (困难)

题目描述

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

示例 1:输入:nums1 = [1,3], nums2 = [2] 输出:2.00000 解释:合并数组 = [1,2,3] ,中位数 2

示例 2:输入:nums1 = [1,2], nums2 = [3,4] 输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -106 <= nums1[i], nums2[i] <= 106

解题思路

要找到两个正序数组的中位数,并且确保算法的时间复杂度为 O(log⁡(m+n)),可以使用基于二分查找,利用了两个数组的有序特性,通过在两个数组中二分查找来定位中位数。

解题思路

  1. 定义问题:假设有两个数组 nums1nums2,长度分别为 mn。我们需要找到这两个数组合并后的中位数。

  2. 二分查找的思路:使用二分查找在较短的数组上查找,来确定两个数组的分割位置。通过调整分割位置,使得左半部分的最大值小于右半部分的最小值。

  3. 步骤

    • 确保 nums1 是较短的数组。如果不是,交换 nums1nums2
    • 使用二分查找确定 nums1 的分割位置 i,以及 nums2 的分割位置 j
    • 调整 ij 直到找到合适的分割,使得左半部分的最大值小于右半部分的最小值。
    • 计算中位数。

算法步骤

  1. 初始化:设定 i_mini_maxnums1 的分割范围;计算 jnums1 分割位置的对应值。

  2. 二分查找:计算中间位置 i;计算 j 使得 i + j 为合适的分割;比较分割边界值来调整 ij

  3. 计算中位数:如果总元素数是偶数,中位数是两个中间值的平均值;如果总元素数是奇数,中位数是左半部分的最大值。

复杂度分析

  • 时间复杂度: O(log⁡min⁡(m,n)),由于二分查找的时间复杂度为对较短数组的对数时间。
  • 空间复杂度: O(1),只使用了常量级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.binary;

/**
 * @program: zyfboot-javabasic
 * @description: 寻找两个正序数组的中位数 (困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:19
 **/
public class FindMedianSortedArraysSolution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        // Ensure nums1 is the smaller array
        if (nums1.length > nums2.length) {
            int[] temp = nums1;
            nums1 = nums2;
            nums2 = temp;
        }

        int m = nums1.length;
        int n = nums2.length;
        int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;

        while (iMin <= iMax) {
            int i = (iMin + iMax) / 2;
            int j = halfLen - i;

            if (i < m && nums2[j - 1] > nums1[i]) {
                // i is too small
                iMin = i + 1;
            } else if (i > 0 && nums1[i - 1] > nums2[j]) {
                // i is too big
                iMax = i - 1;
            } else {
                // i is perfect
                int maxLeft = 0;
                if (i == 0) {
                    maxLeft = nums2[j - 1];
                } else if (j == 0) {
                    maxLeft = nums1[i - 1];
                } else {
                    maxLeft = Math.max(nums1[i - 1], nums2[j - 1]);
                }

                if ((m + n) % 2 == 1) {
                    return maxLeft;
                }

                int minRight = 0;
                if (i == m) {
                    minRight = nums2[j];
                } else if (j == n) {
                    minRight = nums1[i];
                } else {
                    minRight = Math.min(nums1[i], nums2[j]);
                }

                return (maxLeft + minRight) / 2.0;
            }
        }

        throw new IllegalArgumentException("Input arrays are not sorted properly.");
    }

    public static void main(String[] args) {
        FindMedianSortedArraysSolution solution = new FindMedianSortedArraysSolution();

        // 示例 1
        int[] nums1 = {1, 3};
        int[] nums2 = {2};
        System.out.println("Output: " + solution.findMedianSortedArrays(nums1, nums2)); // 输出: 2.0

        // 示例 2
        int[] nums3 = {1, 2};
        int[] nums4 = {3, 4};
        System.out.println("Output: " + solution.findMedianSortedArrays(nums3, nums4)); // 输出: 2.5
    }
}

十二、栈

69.有效的括号(简单)

题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:输入:s = "()" 输出:true

示例 2:输入:s = "()[]{}" 输出:true

示例 3:输入:s = "(]" 输出:false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

解题思路

要判断给定的字符串 s 中的括号是否有效,可以使用栈(Stack)数据结构来解决。栈非常适合处理括号匹配问题,因为它遵循“后进先出”(LIFO)的原则,这与括号的匹配逻辑相符。

  1. 使用栈:遇到左括号((, {, [)时,将其压入栈中;遇到右括号(), }, ])时,检查栈顶元素是否匹配:

    • 如果匹配,将栈顶元素弹出。
    • 如果栈为空或栈顶元素不匹配,则字符串无效。
    • 遍历结束后,栈应为空(即所有的左括号都有对应的右括号)。
  2. 匹配逻辑:使用一个哈希表来存储左括号和右括号的对应关系:[ 对应 ];{ 对应 };( 对应 )

  3. 边界条件:空字符串应返回 true(有效);栈在匹配结束后应为空,若不为空,则说明有未闭合的左括号。

复杂度分析

  • 时间复杂度: O(n),其中 nnn 是字符串的长度。每个字符最多被处理两次(一次入栈,一次出栈)。
  • 空间复杂度: O(n),最坏情况下栈的空间复杂度为 O(n)。

代码实现

package org.zyf.javabasic.letcode.hot100.stack;

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

/**
 * @program: zyfboot-javabasic
 * @description: 有效的括号(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:25
 **/
public class IsValidSolution {
    public boolean isValid(String s) {
        // 哈希表存储括号对
        Map<Character, Character> bracketMap = new HashMap<>();
        bracketMap.put(')', '(');
        bracketMap.put('}', '{');
        bracketMap.put(']', '[');

        // 使用栈来存储左括号
        Stack<Character> stack = new Stack<>();

        // 遍历字符串中的每个字符
        for (char ch : s.toCharArray()) {
            // 如果是右括号
            if (bracketMap.containsKey(ch)) {
                // 弹出栈顶元素(左括号),若栈为空则设为'#'
                char topElement = stack.isEmpty() ? '#' : stack.pop();
                // 检查栈顶元素是否与当前右括号匹配
                if (topElement != bracketMap.get(ch)) {
                    return false;
                }
            } else {
                // 如果是左括号,压入栈中
                stack.push(ch);
            }
        }

        // 如果栈为空,则括号匹配有效;否则无效
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        IsValidSolution solution = new IsValidSolution();

        // 示例 1
        String s1 = "()";
        System.out.println("Output: " + solution.isValid(s1)); // 输出: true

        // 示例 2
        String s2 = "()[]{}";
        System.out.println("Output: " + solution.isValid(s2)); // 输出: true

        // 示例 3
        String s3 = "(]";
        System.out.println("Output: " + solution.isValid(s3)); // 输出: false
    }
}

70.最小栈(中等)

题目描述

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

示例 1:输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] 输出: [null,null,null,null,-3,null,0,-2] 解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.

提示:

  • -231 <= val <= 231 - 1
  • poptop 和 getMin 操作总是在 非空栈 上调用
  • pushpoptop, and getMin最多被调用 3 * 104 次

解题思路

为了实现一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈,我们可以使用两个栈来解决这个问题:

  1. 主栈 (stack) 用于存储所有的元素。
  2. 辅助栈 (minStack) 用于存储当前的最小值。

设计思路

  • push(int val):将 val 推入主栈 stack;对于 minStack,如果 minStack 为空或者 val 小于等于 minStack 栈顶元素,则将 val 推入 minStack

  • pop():从主栈 stack 弹出元素;如果弹出的元素等于 minStack 栈顶元素,则也从 minStack 弹出元素,因为当前最小值已经被移除。

  • top():返回主栈 stack 的栈顶元素。

  • getMin():返回 minStack 的栈顶元素,这就是当前的最小值。

复杂度分析

  • 时间复杂度:所有操作 (pushpoptopgetMin) 都是在常数时间内完成的,即 O(1)。
  • 空间复杂度:需要额外的空间存储 minStack,在最坏情况下,它的空间复杂度也是 O(n),其中 nnn 是 stack 的大小。

代码实现

package org.zyf.javabasic.letcode.hot100.stack;

import java.util.Stack;

/**
 * @program: zyfboot-javabasic
 * @description: 最小栈(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:29
 **/
public class MinStack {
    private Stack<Integer> stack;
    private Stack<Integer> minStack;

    /** Initialize your data structure here. */
    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }

    /** Push element val onto stack. */
    public void push(int val) {
        stack.push(val);
        // Push the new minimum value onto minStack
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }

    /** Removes the element on the top of the stack. */
    public void pop() {
        if (!stack.isEmpty()) {
            int top = stack.pop();
            // If the popped element is the same as the top of minStack, pop from minStack as well
            if (top == minStack.peek()) {
                minStack.pop();
            }
        }
    }

    /** Get the top element of the stack. */
    public int top() {
        return stack.peek();
    }

    /** Retrieve the minimum element in the stack. */
    public int getMin() {
        return minStack.peek();
    }

    public static void main(String[] args) {
        MinStack minStack = new MinStack();
        minStack.push(-2);
        minStack.push(0);
        minStack.push(-3);
        System.out.println(minStack.getMin()); // Returns -3
        minStack.pop();
        System.out.println(minStack.top());    // Returns 0
        System.out.println(minStack.getMin()); // Returns -2
    }
}

71.字符串解码(中等)

题目描述

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

示例 1:输入:s = "3[a]2[bc]" 输出:"aaabcbc"

示例 2:输入:s = "3[a2[c]]" 输出:"accaccacc"

示例 3:输入:s = "2[abc]3[cd]ef" 输出:"abcabccdcdcdef"

示例 4:输入:s = "abc3[cd]xyz" 输出:"abccdcdcdxyz"

提示:

  • 1 <= s.length <= 30
  • s 由小写英文字母、数字和方括号 '[]' 组成
  • s 保证是一个 有效 的输入。
  • s 中所有整数的取值范围为 [1, 300] 

解题思路

要解码经过编码的字符串,我们可以使用栈来解决问题。我们可以遍历字符串,当遇到数字时,记录重复的次数;当遇到方括号时,开始收集需要重复的字符串;当遇到闭括号时,弹出栈顶的内容,并进行解码。

  1. 栈的使用

    数字栈 countStack:用于保存当前的重复次数;字符串栈 stringStack:用于保存当前处理的字符串;当前字符串 currentString:用于累积当前字符直到遇到 ]
  2. 遍历字符串

    • 当遇到数字时,可能是一个多位数,继续读取直到完整数字并入栈。
    • 当遇到 [ 时,将当前累积的字符串和数字分别压入 stringStackcountStack,然后重置 currentString 以开始收集新的字符串。
    • 当遇到 ] 时,弹出栈顶的字符串和数字,进行重复并将结果附加到栈顶字符串后,继续处理。
    • 当遇到普通字符时,直接添加到 currentString
  3. 处理完字符串后,将结果合并返回。

复杂度分析

  • 时间复杂度:O(n),其中 nnn 是字符串的长度。我们只遍历一次字符串并且在栈操作中,所有操作均为常数时间。
  • 空间复杂度:O(n,其中 nnn 是字符串的长度。使用的栈空间取决于嵌套的深度和字符串长度。

代码实现

package org.zyf.javabasic.letcode.hot100.stack;

import java.util.Stack;

/**
 * @program: zyfboot-javabasic
 * @description: 字符串解码(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:34
 **/
public class DecodeString {
    public String decodeString(String s) {
        Stack<Integer> countStack = new Stack<>();
        Stack<StringBuilder> stringStack = new Stack<>();
        StringBuilder currentString = new StringBuilder();
        int k = 0;

        for (char ch : s.toCharArray()) {
            if (Character.isDigit(ch)) {
                k = k * 10 + (ch - '0'); // 计算数字(可能是多位数)
            } else if (ch == '[') {
                countStack.push(k); // 保存当前的重复次数
                stringStack.push(currentString); // 保存当前字符串
                currentString = new StringBuilder(); // 重置 currentString 开始处理新字符
                k = 0; // 重置 k
            } else if (ch == ']') {
                int count = countStack.pop(); // 弹出重复次数
                StringBuilder decodedString = stringStack.pop(); // 弹出栈顶字符串
                for (int i = 0; i < count; i++) {
                    decodedString.append(currentString); // 重复并拼接字符串
                }
                currentString = decodedString; // 将结果存入 currentString
            } else {
                currentString.append(ch); // 普通字符直接添加
            }
        }

        return currentString.toString(); // 返回最终解码后的字符串
    }

    public static void main(String[] args) {
        DecodeString ds = new DecodeString();
        System.out.println(ds.decodeString("3[a]2[bc]")); // 输出 "aaabcbc"
        System.out.println(ds.decodeString("3[a2[c]]")); // 输出 "accaccacc"
        System.out.println(ds.decodeString("2[abc]3[cd]ef")); // 输出 "abcabccdcdcdef"
        System.out.println(ds.decodeString("abc3[cd]xyz")); // 输出 "abccdcdcdxyz"
    }
}

72.每日温度(中等)

题目描述

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:输入: temperatures = [73,74,75,71,69,72,76,73] 输出: [1,1,4,2,1,1,0,0]

示例 2:输入: temperatures = [30,40,50,60] 输出: [1,1,1,0]

示例 3:输入: temperatures = [30,60,90] 输出: [1,1,0]

提示:

  • 1 <= temperatures.length <= 105
  • 30 <= temperatures[i] <= 100

解题思路

要解决这个问题,我们可以使用 单调栈 来寻找每一天温度之后的第一个更高温度的天数。这个方法能够高效地解决问题并满足时间复杂度的要求。

  1. 单调栈的定义:我们维护一个栈,栈中的元素存储的是温度的下标。栈中的温度是递减的,这样当我们遇到一个比栈顶元素大的温度时,就可以知道栈顶元素的下一个更高温度出现在当前下标。

  2. 遍历温度数组:当栈非空且当前温度高于栈顶温度时,说明找到了栈顶温度的下一个更高温度。计算距离,并将栈顶元素弹出;无论如何都将当前温度的下标压入栈中,继续处理下一个温度。

  3. 结果数组:最终得到的 answer 数组就是每一天到下一个更高温度的天数。

复杂度分析

  • 时间复杂度:O(n),因为每个元素最多只会被压入和弹出栈一次。
  • 空间复杂度:O(n),用于存储栈和结果数组。

代码实现

package org.zyf.javabasic.letcode.hot100.stack;

import java.util.Stack;

/**
 * @program: zyfboot-javabasic
 * @description: 每日温度(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:40
 **/
public class DailyTemperatures {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] answer = new int[n];
        Stack<Integer> stack = new Stack<>();

        for (int i = 0; i < n; i++) {
            // 当前温度比栈顶温度高,计算差值
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                int idx = stack.pop();
                answer[idx] = i - idx;
            }
            // 压入当前温度的下标
            stack.push(i);
        }

        return answer;
    }

    public static void main(String[] args) {
        DailyTemperatures dt = new DailyTemperatures();
        int[] result1 = dt.dailyTemperatures(new int[]{73, 74, 75, 71, 69, 72, 76, 73});
        int[] result2 = dt.dailyTemperatures(new int[]{30, 40, 50, 60});
        int[] result3 = dt.dailyTemperatures(new int[]{30, 60, 90});

        // 打印结果
        System.out.println(java.util.Arrays.toString(result1)); // [1, 1, 4, 2, 1, 1, 0, 0]
        System.out.println(java.util.Arrays.toString(result2)); // [1, 1, 1, 0]
        System.out.println(java.util.Arrays.toString(result3)); // [1, 1, 0]
    }
}

73.柱状图中最大的矩形(困难)

题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

示例 2:

输入: heights = [2,4]
输出: 4

提示:

  • 1 <= heights.length <=105
  • 0 <= heights[i] <= 104

解题思路

要在柱状图中找到最大的矩形面积,可以使用 单调栈 来实现。

  1. 单调栈的定义:维护一个栈,栈中存储的是柱子的下标,栈中的柱子的高度是单调递增的。当遇到一个柱子高度小于栈顶柱子高度时,说明栈顶柱子可以计算面积了。

  2. 计算面积:当当前柱子高度小于栈顶柱子高度时,从栈中弹出栈顶元素,计算以该高度为最矮柱子时形成的矩形面积;矩形的宽度是当前下标和栈顶下标的差值减一。

  3. 处理剩余柱子:最后当遍历完整个数组后,栈中可能还会剩下柱子的下标,需要继续计算面积。

复杂度分析

  • 时间复杂度:O(n),每个柱子只会被压入和弹出栈一次。
  • 空间复杂度:O(n),用于存储栈中的元素。

代码实现

package org.zyf.javabasic.letcode.hot100.stack;

import java.util.Stack;

/**
 * @program: zyfboot-javabasic
 * @description: 柱状图中最大的矩形(困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:45
 **/
public class LargestRectangle {
    public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<>();
        int maxArea = 0;
        int n = heights.length;

        for (int i = 0; i <= n; i++) {
            int currentHeight = (i == n) ? 0 : heights[i];

            while (!stack.isEmpty() && currentHeight < heights[stack.peek()]) {
                int height = heights[stack.pop()];
                int width = stack.isEmpty() ? i : i - stack.peek() - 1;
                maxArea = Math.max(maxArea, height * width);
            }

            stack.push(i);
        }

        return maxArea;
    }

    public static void main(String[] args) {
        LargestRectangle lr = new LargestRectangle();
        int[] heights1 = {2, 1, 5, 6, 2, 3};
        int[] heights2 = {2, 4};

        System.out.println(lr.largestRectangleArea(heights1)); // 输出: 10
        System.out.println(lr.largestRectangleArea(heights2)); // 输出: 4
    }
}

十三、堆

74.数组中的第K个最大元素(中等)

题目描述

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

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

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

提示:

  • 1 <= k <= nums.length <= 105
  • -104 <= nums[i] <= 104

解题思路

要找到数组中的第 k 个最大的元素,可以使用快速选择算法(Quickselect),它的平均时间复杂度为 O(n),可以满足题目要求。

快速选择算法与快速排序(Quicksort)类似,都是基于分治思想。不同的是,快速选择只需要找到第 k 大的元素,而不需要对整个数组排序。

  1. 选择一个基准元素(pivot),通常选择数组的最后一个元素。
  2. 分区操作:将数组划分为两部分,左边的元素都大于等于基准元素,右边的元素都小于基准元素。
  3. 递归选择:检查基准元素的位置是否就是第 k 大的元素。如果是,则直接返回基准元素;如果不是,根据基准元素的位置判断要在哪一部分继续寻找。

复杂度分析

  • 时间复杂度:平均时间复杂度为 O(n)。在最坏情况下,时间复杂度为 O(n^2),但通过随机选择基准元素可以有效避免最坏情况。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.heap;

import java.util.Random;

/**
 * @program: zyfboot-javabasic
 * @description: 数组中的第K个最大元素(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:50
 **/
public class KthLargestElement {
    public int findKthLargest(int[] nums, int k) {
        int n = nums.length;
        return quickSelect(nums, 0, n - 1, n - k);
    }

    private int quickSelect(int[] nums, int left, int right, int k) {
        if (left == right) {
            return nums[left];
        }

        // 随机选择一个pivot,避免最坏情况
        Random random = new Random();
        int pivotIndex = left + random.nextInt(right - left + 1);

        // 分区操作,返回pivot的最终位置
        pivotIndex = partition(nums, left, right, pivotIndex);

        // 根据k的位置,选择递归方向
        if (k == pivotIndex) {
            return nums[k];
        } else if (k < pivotIndex) {
            return quickSelect(nums, left, pivotIndex - 1, k);
        } else {
            return quickSelect(nums, pivotIndex + 1, right, k);
        }
    }

    private int partition(int[] nums, int left, int right, int pivotIndex) {
        int pivotValue = nums[pivotIndex];
        // 先将pivot放到最后
        swap(nums, pivotIndex, right);
        int storeIndex = left;

        for (int i = left; i < right; i++) {
            if (nums[i] < pivotValue) {
                swap(nums, storeIndex, i);
                storeIndex++;
            }
        }

        // 将pivot放回到它最终的位置
        swap(nums, storeIndex, right);

        return storeIndex;
    }

    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) {
        KthLargestElement solver = new KthLargestElement();
        int[] nums1 = {3, 2, 1, 5, 6, 4};
        int k1 = 2;
        System.out.println(solver.findKthLargest(nums1, k1)); // 输出: 5

        int[] nums2 = {3, 2, 3, 1, 2, 4, 5, 5, 6};
        int k2 = 4;
        System.out.println(solver.findKthLargest(nums2, k2)); // 输出: 4
    }
}

75.前 K 个高频元素(中等)

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

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

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

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

解题思路

要找出数组中出现频率最高的前 k 个元素,可以使用或者桶排序的方法。这些方法的时间复杂度可以达到 O(nlog⁡k)或 O(n),满足题目要求。

使用桶排序可以在 O(n)O(n)O(n) 的时间复杂度下解决问题。将元素根据频率放入不同的桶,桶的下标表示频率,然后从高频到低频依次遍历这些桶,直到找到 k 个元素。

复杂度分析

  • 时间复杂度:O(n),遍历数组统计频率,分配到桶中,最终遍历桶获取结果。
  • 空间复杂度:O(n),主要用来存储频率和桶。

代码实现

package org.zyf.javabasic.letcode.hot100.heap;

import java.util.*;

/**
 * @program: zyfboot-javabasic
 * @description: 前 K 个高频元素(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 14:56
 **/
public class TopKFrequentElements {
    public int[] topKFrequent(int[] nums, int k) {
        // 1. 统计每个元素出现的频率
        Map<Integer, Integer> frequencyMap = new HashMap<>();
        for (int num : nums) {
            frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
        }

        // 2. 创建桶数组,频率最高为nums.length
        List<Integer>[] bucket = new List[nums.length + 1];
        for (int key : frequencyMap.keySet()) {
            int frequency = frequencyMap.get(key);
            if (bucket[frequency] == null) {
                bucket[frequency] = new ArrayList<>();
            }
            bucket[frequency].add(key);
        }

        // 3. 从后向前遍历桶数组,收集频率最高的 k 个元素
        List<Integer> result = new ArrayList<>();
        for (int i = bucket.length - 1; i >= 0 && result.size() < k; i--) {
            if (bucket[i] != null) {
                result.addAll(bucket[i]);
            }
        }

        // 转换结果为数组
        return result.stream().mapToInt(Integer::intValue).toArray();
    }

    public static void main(String[] args) {
        TopKFrequentElements solver = new TopKFrequentElements();
        int[] nums1 = {1, 1, 1, 2, 2, 3};
        int k1 = 2;
        System.out.println(Arrays.toString(solver.topKFrequent(nums1, k1))); // 输出: [1, 2]

        int[] nums2 = {1};
        int k2 = 1;
        System.out.println(Arrays.toString(solver.topKFrequent(nums2, k2))); // 输出: [1]
    }
}

76.数据流的中位数(困难)

题目描述

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。

  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

示例 1:输入 ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"] [[], [1], [2], [], [3], []] 输出 [null, null, null, 1.5, null, 2.0] 解释 MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // arr = [1] medianFinder.addNum(2); // arr = [1, 2] medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2) medianFinder.addNum(3); // arr[1, 2, 3] medianFinder.findMedian(); // return 2.0

提示:

  • -105 <= num <= 105
  • 在调用 findMedian 之前,数据结构中至少有一个元素
  • 最多 5 * 104 次调用 addNum 和 findMedian

解题思路

为了实现 MedianFinder 类,我们可以利用两个堆来动态地维护数据流中的中位数。具体来说:

  • 最大堆maxHeap):存储数据流中较小的一半元素,堆顶元素是这部分的最大值。
  • 最小堆minHeap):存储数据流中较大的一半元素,堆顶元素是这部分的最小值。

设计思路:

  1. addNum 方法:首先将新元素添加到最大堆中;然后检查最大堆和最小堆的平衡性。如果最大堆的元素大于最小堆的元素,那么将最大堆的堆顶元素移动到最小堆中,确保两个堆中的元素数量平衡或最大堆的元素数量最多比最小堆多一个。

  2. findMedian 方法:如果两个堆的元素数量相同,中位数就是最大堆和最小堆堆顶元素的平均值;如果最大堆的元素数量比最小堆多,中位数就是最大堆的堆顶元素。

复杂度分析

  • 时间复杂度
    • addNum 方法:O(log⁡n),因为涉及到堆的插入操作。
    • findMedian 方法:O(1),因为只需要访问堆顶元素。
  • 空间复杂度:O(n),其中 n 是数据流中的元素数量。需要存储所有元素。

代码实现

package org.zyf.javabasic.letcode.hot100.heap;

import java.util.Collections;
import java.util.PriorityQueue;

/**
 * @program: zyfboot-javabasic
 * @description: 数据流的中位数(困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 15:01
 **/
public class MedianFinder {
    private PriorityQueue<Integer> maxHeap; // 用于存储较小的一半元素
    private PriorityQueue<Integer> minHeap; // 用于存储较大的一半元素

    /** 初始化数据结构 */
    public MedianFinder() {
        maxHeap = new PriorityQueue<>(Collections.reverseOrder()); // 最大堆
        minHeap = new PriorityQueue<>(); // 最小堆
    }

    /** 添加一个数字到数据结构中 */
    public void addNum(int num) {
        maxHeap.add(num);
        minHeap.add(maxHeap.poll()); // 平衡两个堆

        // 保证 maxHeap 的元素数量比 minHeap 多最多一个
        if (maxHeap.size() < minHeap.size()) {
            maxHeap.add(minHeap.poll());
        }
    }

    /** 返回目前所有元素的中位数 */
    public double findMedian() {
        if (maxHeap.size() > minHeap.size()) {
            return maxHeap.peek();
        } else {
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        }
    }

    public static void main(String[] args) {
        MedianFinder medianFinder = new MedianFinder();
        medianFinder.addNum(1);
        medianFinder.addNum(2);
        System.out.println(medianFinder.findMedian()); // 输出 1.5
        medianFinder.addNum(3);
        System.out.println(medianFinder.findMedian()); // 输出 2.0
    }
}

十四、贪心算法

77.买卖股票的最佳时机(简单)

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

解题思路

要解决这个问题,我们可以采用一个一次遍历的方法。核心思想是记录遍历到每一天时,之前几天中的最小价格,并计算当日卖出时的最大利润。

  1. 初始化变量 minPrice 为正无穷大,表示到目前为止最小的买入价格。

  2. 初始化变量 maxProfit 为 0,表示最大利润。

  3. 遍历数组 prices 中的每个价格:

    如果当前价格比 minPrice 小,更新 minPrice;否则计算当前价格与 minPrice 的差值,表示在当前价格卖出股票时能获得的利润。如果该利润大于 maxProfit,更新 maxProfit
  4. 遍历结束后,maxProfit 就是最大利润。

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 prices 的长度。我们只需要遍历一次数组。
  • 空间复杂度:O(1),我们只用了常数级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.greedy;

/**
 * @program: zyfboot-javabasic
 * @description: 买卖股票的最佳时机(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 15:06
 **/
public class MaxProfitSolution {
    public int maxProfit(int[] prices) {
        int minPrice = Integer.MAX_VALUE; // 初始化最小价格为正无穷
        int maxProfit = 0; // 初始化最大利润为0

        // 遍历每一天的股票价格
        for (int price : prices) {
            if (price < minPrice) {
                // 更新最小价格
                minPrice = price;
            } else if (price - minPrice > maxProfit) {
                // 计算利润,并更新最大利润
                maxProfit = price - minPrice;
            }
        }

        return maxProfit;
    }

    public static void main(String[] args) {
        MaxProfitSolution solution = new MaxProfitSolution();
        int[] prices1 = {7, 1, 5, 3, 6, 4};
        int[] prices2 = {7, 6, 4, 3, 1};

        System.out.println(solution.maxProfit(prices1)); // 输出: 5
        System.out.println(solution.maxProfit(prices2)); // 输出: 0
    }
}

78.跳跃游戏(中等)

题目描述

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

示例 1:输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 105

解题思路

要判断是否能到达数组的最后一个下标,可以通过维护一个能够到达的最远位置来实现。具体步骤如下:

  1. 初始化 maxReach 为 0,表示当前能够到达的最远位置。
  2. 遍历数组 nums 中的每个位置 i:如果当前位置 i 超过了 maxReach,说明不能到达该位置,直接返回 false;更新 maxReachi + nums[i],表示从当前位置出发,能够到达的最远位置。
  3. 如果遍历结束后,maxReach 大于或等于最后一个下标,返回 true,否则返回 false

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们只需要遍历一次数组。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.greedy;

/**
 * @program: zyfboot-javabasic
 * @description: 跳跃游戏(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:01
 **/
public class CanJumpSolution {
    public boolean canJump(int[] nums) {
        int maxReach = 0; // 初始化最远可达位置

        for (int i = 0; i < nums.length; i++) {
            if (i > maxReach) {
                // 当前下标i大于最远可达位置,返回false
                return false;
            }
            // 更新最远可达位置
            maxReach = Math.max(maxReach, i + nums[i]);

            // 如果最远可达位置已经覆盖最后一个下标,直接返回true
            if (maxReach >= nums.length - 1) {
                return true;
            }
        }

        return true;
    }

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

        System.out.println(solution.canJump(nums1)); // 输出: true
        System.out.println(solution.canJump(nums2)); // 输出: false
    }
}

79.跳跃游戏 II(中等)

题目描述

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i] 
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

示例 1:输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是 2。   从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

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

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 nums[n-1]

解题思路

要解决这个问题,我们可以采用贪心算法来计算到达数组最后一个位置所需的最小跳跃次数。主要思想是遍历数组时,始终记录当前能到达的最远位置,并在达到该位置时增加跳跃次数,直到最终到达数组末尾。

  1. 初始化变量 jumps 表示跳跃次数,currentEnd 表示当前跳跃的结束位置,farthest 表示在当前跳跃内可以到达的最远位置。
  2. 遍历数组(不包括最后一个元素),在每一步中:更新 farthest,表示在当前位置 i 可以到达的最远位置;如果当前索引 i 达到了 currentEnd,说明需要进行一次跳跃,因此将 jumps 增加 1,并将 currentEnd 更新为 farthest
  3. 当遍历结束时,jumps 就是最小的跳跃次数。

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们只需要遍历一次数组。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.greedy;

/**
 * @program: zyfboot-javabasic
 * @description: 跳跃游戏 II(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:07
 **/
public class JumpSolution {
    public int jump(int[] nums) {
        int jumps = 0;         // 记录跳跃次数
        int currentEnd = 0;    // 当前跳跃的结束位置
        int farthest = 0;      // 在当前跳跃内能到达的最远位置

        for (int i = 0; i < nums.length - 1; i++) {
            farthest = Math.max(farthest, i + nums[i]); // 更新最远能到达的位置

            if (i == currentEnd) { // 到达当前跳跃的结束位置
                jumps++;            // 增加跳跃次数
                currentEnd = farthest; // 更新跳跃结束位置为最远能到达的位置
            }
        }

        return jumps; // 返回跳跃次数
    }

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

        System.out.println(solution.jump(nums1)); // 输出: 2
        System.out.println(solution.jump(nums2)); // 输出: 2
    }
}

80.划分字母区间(中等)

题目描述

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:输入:s = "ababcbacadefegdehijhklij" 输出:[9,7,8] 解释: 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。

示例 2:输入:s = "eccbbbbdec" 输出:[10]

提示:

  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

解题思路

要解决这个问题,我们可以使用贪心算法来找到字符串 s 中每个片段的长度,使得每个字母最多出现在一个片段中。具体的步骤如下:

  1. 确定每个字母的最远出现位置:我们首先遍历字符串,记录每个字母在字符串中最后一次出现的位置。

  2. 划分字符串

    再次遍历字符串,用两个指针 startend 来标记当前片段的开始和结束位置。end 初始化为当前遍历位置字符的最远出现位置;如果遍历到的位置等于 end,说明当前片段可以结束了,我们记录下这个片段的长度,并将 start 更新为下一个位置。
  3. 返回结果:最终,记录下的每个片段的长度就是我们要求的结果。

示例说明:

  • 对于输入 "ababcbacadefegdehijhklij"
    • 字符 'a' 的最远位置为 8,'b' 为 5,'c' 为 7…… 由此,划分的第一个片段为 "ababcbaca"(长度为 9)。
    • 继续处理剩下的字符串,划分的第二个片段为 "defegde"(长度为 7),最后一个片段为 "hijhklij"(长度为 8)。
  • 对于输入 "eccbbbbdec"
    • 整个字符串从 'e' 到 'c' 最远出现的位置都是在最后一个字符,因此整个字符串只能作为一个片段。

这个算法能够有效地解决问题,并保证每个片段中的字母最多只出现一次。

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。我们需要两次遍历字符串,一次用来记录每个字母的最后出现位置,另一次用来划分片段。
  • 空间复杂度:O(1),我们使用了固定大小的数组来存储字母的最后出现位置,因此空间复杂度为常数。

代码实现

package org.zyf.javabasic.letcode.hot100.greedy;

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

/**
 * @program: zyfboot-javabasic
 * @description: 划分字母区间(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:13
 **/
public class PartitionLabelsSolution {
    public List<Integer> partitionLabels(String s) {
        // 存储每个字母在字符串中的最后一次出现位置
        int[] last = new int[26];
        for (int i = 0; i < s.length(); i++) {
            last[s.charAt(i) - 'a'] = i;
        }

        List<Integer> result = new ArrayList<>();
        int start = 0; // 记录片段的起始位置
        int end = 0;   // 记录当前片段的最远结束位置

        // 遍历字符串
        for (int i = 0; i < s.length(); i++) {
            end = Math.max(end, last[s.charAt(i) - 'a']); // 更新当前片段的最远结束位置
            if (i == end) { // 当 i 达到当前片段的最远结束位置时,划分一个片段
                result.add(end - start + 1); // 记录片段长度
                start = i + 1; // 更新片段起始位置为下一个字符
            }
        }

        return result; // 返回所有片段的长度
    }

    public static void main(String[] args) {
        PartitionLabelsSolution solution = new PartitionLabelsSolution();
        System.out.println(solution.partitionLabels("ababcbacadefegdehijhklij")); // 输出: [9, 7, 8]
        System.out.println(solution.partitionLabels("eccbbbbdec")); // 输出: [10]
    }
}

十五、动态规划

81.爬楼梯(简单)

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

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

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

提示:

  • 1 <= n <= 45

解题思路

经典的动态规划问题,通常被称为“爬楼梯”问题,在这个问题中,爬到第 n 阶楼梯的方法数可以通过以下关系来计算:

  • 爬到第 n 阶可以从第 n-1 阶通过爬 1 个台阶到达,或者从第 n-2 阶通过爬 2 个台阶到达。

因此,我们可以用动态规划的方法来求解,其中 dp[i] 表示爬到第 i 阶的方法数。

递推关系:dp[i]=dp[i−1]+dp[i−2]

  • dp[0] = 1(到达第 0 阶的方法数是 1,代表站在地面上)
  • dp[1] = 1(到达第 1 阶的方法数是 1,仅有一种方法)

复杂度分析

  • 时间复杂度:O(n),我们需要遍历从 2 到 n 的所有台阶。
  • 空间复杂度:O(1)(使用空间优化方法)或者 O(n)(使用标准动态规划方法)。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

/**
 * @program: zyfboot-javabasic
 * @description: 爬楼梯(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:23
 **/
public class ClimbStairsSolution {
    public int climbStairs(int n) {
        if (n <= 1) return 1; // 如果楼梯只有1级或0级,则只有一种爬法

        int[] dp = new int[n + 1];
        dp[0] = 1; // 起点
        dp[1] = 1; // 第一个台阶

        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2]; // 爬到第 i 阶的方法数
        }

        return dp[n];
    }

    public static void main(String[] args) {
        ClimbStairsSolution solution = new ClimbStairsSolution();
        System.out.println(solution.climbStairs(2)); // 输出: 2
        System.out.println(solution.climbStairs(3)); // 输出: 3
    }
}

82.杨辉三角(简单)

题目描述

给定一个非负整数 numRows生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

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

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

提示:

  • 1 <= numRows <= 30

解题思路

对于杨辉三角,每一行的元素可以利用上一行的结果来计算。

  1. 初始化:杨辉三角的第一行是 [1]。这可以作为我们的初始状态。

  2. 状态转移:每一行的第一个和最后一个元素都是 1;每个内部元素是上一行中两个相邻元素的和,即:triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]

动态规划生成杨辉三角的步骤:

  1. 创建一个二维列表来存储杨辉三角的结果。
  2. 从第一行开始逐步构建到需要的行数。
  3. 使用已生成的行来计算当前行的元素。

复杂度分析

  • 时间复杂度:O(numRows^2),因为每一行的生成涉及遍历该行的元素,而每行的元素个数与行号成正比。
  • 空间复杂度:O(numRows^2),因为我们需要存储所有的行和每行的所有元素。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

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

/**
 * @program: zyfboot-javabasic
 * @description: 杨辉三角(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:28
 **/
public class PascalTriangle {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> triangle = new ArrayList<>();

        // 遍历每一行
        for (int i = 0; i < numRows; i++) {
            List<Integer> row = new ArrayList<>();
            // 每一行的第一个和最后一个元素都是 1
            row.add(1);
            // 计算中间的元素
            for (int j = 1; j < i; j++) {
                // 每个元素等于上一行的两个相邻元素之和
                row.add(triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j));
            }
            // 每一行的最后一个元素也是 1
            if (i > 0) {
                row.add(1);
            }
            // 将当前行添加到杨辉三角中
            triangle.add(row);
        }

        return triangle;
    }

    public static void main(String[] args) {
        PascalTriangle pt = new PascalTriangle();
        int numRows = 5;
        List<List<Integer>> result = pt.generate(numRows);
        System.out.println(result);
    }
}

83.打家劫舍(中等)

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。   偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。   偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

解题思路

经典的动态规划问题,通常称为“打家劫舍”问题:我们的目标是计算在不触发警报的情况下,能偷窃到的最大金额。

  1. 定义状态:使用 dp[i] 表示偷窃到第 i 个房屋时,能够获得的最大金额。

  2. 状态转移方程

    如果选择偷窃第 i 个房屋,则不能偷窃第 i-1 个房屋,最大金额为 dp[i-2] + nums[i];如果不选择偷窃第 i 个房屋,则最大金额为 dp[i-1];因此状态转移方程为: dp[i]=max⁡(dp[i−1],dp[i−2]+nums[i])
  3. 初始状态

    dp[0] = nums[0],只有一个房屋时,只能偷窃这个房屋;dp[1] = \max(nums[0], nums[1]),只有两个房屋时,选择偷窃金额更大的那个。
  4. 最终结果:最终的结果是 dp[n-1],其中 n 是房屋的总数。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

/**
 * @program: zyfboot-javabasic
 * @description: 打家劫舍(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:45
 **/
public class HouseRobber {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        if (n == 1) return nums[0];

        // dp数组
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);

        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }

        return dp[n-1];
    }

    public static void main(String[] args) {
        HouseRobber robber = new HouseRobber();
        int[] nums1 = {1, 2, 3, 1};
        System.out.println(robber.rob(nums1)); // 输出: 4

        int[] nums2 = {2, 7, 9, 3, 1};
        System.out.println(robber.rob(nums2)); // 输出: 12
    }
}

84.完全平方数(中等)

题目描述

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,149 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:输入:n = 12 输出:3 解释:12 = 4 + 4 + 4

示例 2:输入:n = 13 输出:2 解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

解题思路

经典的动态规划问题,目的是求解和为 n 的最少完全平方数的数量:

  1. 定义状态:使用 dp[i] 表示和为 i 的最少完全平方数的数量。

  2. 状态转移方程

    对于每个 i,我们需要尝试减去一个完全平方数 j*j(其中 j*j <= i),然后加上剩余部分的最少完全平方数数量,即: dp[i]=min⁡(dp[i],dp[i−j∗j]+1)。这个方程的意思是:对于 dp[i],我们遍历所有的 j,找到其中 dp[i - j*j] + 1 的最小值,即为和为 i 的最少完全平方数的数量。
  3. 初始状态dp[0] = 0,因为和为 0 时不需要任何完全平方数。

  4. 最终结果:最终的结果是 dp[n],表示和为 n 的最少完全平方数的数量。

复杂度分析

  • 内层循环的复杂度为 O(\sqrt{n}),外层循环的复杂度为 O(n),因此总体的时间复杂度为 O(n\cdot \sqrt{n})
  • 空间复杂度为 O(n),因为我们需要一个大小为 n+1 的数组来存储状态。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 完全平方数(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 19:54
 **/
public class PerfectSquares {
    public int numSquares(int n) {
        // 定义一个数组 dp,其中 dp[i] 表示和为 i 的最少完全平方数的数量
        int[] dp = new int[n + 1];
        // 初始化 dp 数组为最大值,表示初始状态下还没有计算出结果
        Arrays.fill(dp, Integer.MAX_VALUE);
        // 当 n=0 时,最少的完全平方数数量为 0
        dp[0] = 0;

        // 外层循环,遍历从 1 到 n 的所有值,计算每个 i 的最小完全平方数数量
        for (int i = 1; i <= n; i++) {
            // 内层循环,遍历所有的完全平方数 j*j
            for (int j = 1; j * j <= i; j++) {
                // 更新 dp[i] 为当前最小的完全平方数数量
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }

        // 返回 dp[n],即和为 n 的最少完全平方数数量
        return dp[n];
    }

    public static void main(String[] args) {
        PerfectSquares ps = new PerfectSquares();
        // 输出 3,表示 12 可以表示为 4+4+4,最少需要 3 个完全平方数
        System.out.println(ps.numSquares(12));
        // 输出 2,表示 13 可以表示为 4+9,最少需要 2 个完全平方数
        System.out.println(ps.numSquares(13));
    }
}

85.零钱兑换(中等)

题目描述

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1

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

示例 3:输入:coins = [1], amount = 0 输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

解题思路

这道题可以用动态规划来解决。核心思想是:对于每一个金额 i,我们可以通过选择一种硬币 coins[j] 来减少金额,从而递归地计算出最小硬币数。最终,我们可以通过以下递推关系来得到最小的硬币数:

  • dp[i] 表示凑成金额 i 所需的最少硬币数。
  • 初始状态:dp[0] = 0,即金额为 0 时不需要任何硬币。
  • 状态转移方程:对于每个硬币 coins[j],如果当前金额 i 大于等于 coins[j],那么 dp[i] = min(dp[i], dp[i - coins[j]] + 1)

最终,dp[amount] 就是凑成 amount 的最少硬币数。如果 dp[amount] 仍然是初始化的最大值,说明无法凑成该金额,返回 -1

复杂度分析

  • 时间复杂度O(n * m),其中 n 是金额 amountm 是硬币的种类数。因为我们需要计算 dp[amount + 1] 数组中的每个元素,而每个元素都需要遍历 coins 数组。
  • 空间复杂度O(n),我们需要一个长度为 amount + 1 的数组 dp 来存储每个金额的最小硬币数。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 零钱兑换(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:04
 **/
public class CoinChangeSolution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;  // 初始化一个超过amount的值,用于填充dp数组
        int[] dp = new int[amount + 1];  // dp数组,存储每个金额的最少硬币数
        Arrays.fill(dp, max);  // 将dp数组初始化为max
        dp[0] = 0;  // 金额为0时,所需硬币数为0

        // 遍历每个金额,从1到amount
        for (int i = 1; i <= amount; i++) {
            // 遍历每个硬币
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {  // 只有当硬币面值小于等于当前金额时,才考虑该硬币
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);  // 状态转移方程
                }
            }
        }

        // 如果dp[amount]仍然是初始值max,说明无法凑成该金额,返回-1
        return dp[amount] > amount ? -1 : dp[amount];
    }

    public static void main(String[] args) {
        CoinChangeSolution solution = new CoinChangeSolution();
        int[] coins = {1, 2, 5};
        int amount = 11;
        int result = solution.coinChange(coins, amount);
        System.out.println(result); // 输出3
    }
}

86.单词拆分 (中等)

题目描述

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。   注意,你可以重复使用字典中的单词。

示例 3:输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

解题思路

这是一个经典的动态规划问题。可以将问题视为判断字符串 s 是否可以由字典 wordDict 中的单词组合而成。我们定义一个布尔数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符是否可以被字典中的单词组合而成。

动态规划的转移方程:我们使用一个长度为 s.length + 1 的布尔数组 dp,其中 dp[0] 初始化为 true,表示空字符串可以被认为是可以被字典组合而成。

对于每一个 i,我们需要检查在字典中是否存在一个单词 word,使得 s[i - len(word):i] 等于 word,且 dp[i - len(word)]true,则 dp[i]true

复杂度分析

  • 时间复杂度: O(n * m),其中 n 是字符串 s 的长度,mwordDict 的长度。在最坏的情况下,需要遍历字符串 s 的每一个字符,并对于每个字符检查 wordDict 中的每个单词。
  • 空间复杂度: O(n),需要一个长度为 n + 1 的数组 dp 来存储状态。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

import com.google.common.collect.Lists;

import java.util.List;

/**
 * @program: zyfboot-javabasic
 * @description: 单词拆分(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:10
 **/
public class WordBreakSolution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // dp数组,其中dp[i]表示s的前i个字符能否被字典中的单词拼接而成
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true; // 初始化dp[0]为true,表示空字符串可以被拼接而成

        // 遍历字符串s的每个字符
        for (int i = 1; i <= s.length(); i++) {
            // 遍历字典中的每个单词
            for (String word : wordDict) {
                // 如果当前的单词长度不超过i,且s的前i个字符中的最后一个匹配word
                if (i >= word.length() && s.substring(i - word.length(), i).equals(word)) {
                    dp[i] = dp[i] || dp[i - word.length()];
                }
            }
        }

        // 返回dp[s.length()],表示s能否被拼接而成
        return dp[s.length()];
    }

    public static void main(String[] args) {
        WordBreakSolution solution = new WordBreakSolution();
        List<String> wordDict = Lists.newArrayList("leet", "code");
        String s = "leetcode";
        System.out.println(solution.wordBreak(s, wordDict)); // 输出: true

        wordDict = Lists.newArrayList("apple", "pen");
        s = "applepenapple";
        System.out.println(solution.wordBreak(s, wordDict)); // 输出: true

        wordDict = Lists.newArrayList("cats", "dog", "sand", "and", "cat");
        s = "catsandog";
        System.out.println(solution.wordBreak(s, wordDict)); // 输出: false
    }
}

87.最长递增子序列  (中等)

题目描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 

示例 1:输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

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

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

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

进阶:你能将算法的时间复杂度降低到 O(n log(n)) 吗?

解题思路

动态规划 + 二分查找 (O(n log n))

使用一个辅助数组 tail,其中 tail[i] 保存长度为 i+1 的递增子序列的最小末尾元素。通过二分查找定位当前元素应放置在 tail 中的位置,从而高效更新序列。

解题思路

  1. 辅助数组 tail 的定义

    使用一个辅助数组 tailtail[i] 保存长度为 i+1 的递增子序列的最小末尾元素。tail 数组是递增的,但并不一定是最终的最长递增子序列。通过维护 tail,我们可以有效地跟踪最长递增子序列的长度。
  2. 遍历 nums 数组

    对于每个元素 num,使用二分查找来确定它应该插入到 tail 数组中的位置;如果 num 可以替换掉 tail 中的一个元素(即找到一个比 num 大的最小元素),则替换之;如果 num 大于 tail 中的所有元素,那么就将其添加到 tail 的末尾,并增加最长子序列的长度。
  3. 返回结果tail 数组的长度即为最长递增子序列的长度。

复杂度分析

  • 时间复杂度:O(n log n)。遍历数组的时间复杂度为 O(n),对于每个元素,通过二分查找插入位置的时间复杂度为 O(log n)。
  • 空间复杂度:O(n)。需要一个大小为 n 的辅助数组 tail

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

import java.util.Arrays;

/**
 * @program: zyfboot-javabasic
 * @description: 最长递增子序列(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:21
 **/
public class LongestIncreasingSubsequence {
    public int lengthOfLIS(int[] nums) {
        // 辅助数组 tail,用于存储递增子序列的最小末尾元素
        int[] tail = new int[nums.length];
        // 当前最长递增子序列的长度
        int size = 0;

        // 遍历数组中的每一个元素
        for (int num : nums) {
            // 使用二分查找确定 num 在 tail 数组中的插入位置
            int i = Arrays.binarySearch(tail, 0, size, num);
            // 如果未找到,则返回插入点 (使用 -1 来补偿数组索引的偏移)
            if (i < 0) {
                i = -(i + 1);
            }
            // 更新 tail 数组中的对应位置
            tail[i] = num;
            // 如果 num 被添加到 tail 数组的末尾,增加最长子序列的长度
            if (i == size) {
                size++;
            }
        }

        // 返回最长递增子序列的长度
        return size;
    }

    public static void main(String[] args) {
        LongestIncreasingSubsequence lis = new LongestIncreasingSubsequence();

        // 测试用例1
        int[] nums1 = {10, 9, 2, 5, 3, 7, 101, 18};
        System.out.println("测试用例1结果: " + lis.lengthOfLIS(nums1)); // 输出:4

        // 测试用例2
        int[] nums2 = {0, 1, 0, 3, 2, 3};
        System.out.println("测试用例2结果: " + lis.lengthOfLIS(nums2)); // 输出:4

        // 测试用例3
        int[] nums3 = {7, 7, 7, 7, 7, 7, 7};
        System.out.println("测试用例3结果: " + lis.lengthOfLIS(nums3)); // 输出:1
    }
}

88.乘积最大子数组 (中等)

题目描述

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

示例 1:输入: nums = [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。

示例 2:输入: nums = [-2,0,-1] 输出: 0 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

提示:

  • 1 <= nums.length <= 2 * 104
  • -10 <= nums[i] <= 10
  • nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数

解题思路

要解决这个问题可以使用动态规划来跟踪每个位置的最大和最小乘积。因为乘积可能会由于负数的存在而变化,因此我们需要同时记录最大和最小乘积,以便在乘以负数时正确计算。

  1. 定义状态

    maxProduct[i] 表示以 nums[i] 结尾的子数组中的最大乘积;minProduct[i] 表示以 nums[i] 结尾的子数组中的最小乘积。
  2. 状态转移

    对于每个元素 nums[i],可以通过以下公式更新 maxProduct[i]minProduct[i]
    • minProduct[i] = min(nums[i], nums[i] * maxProduct[i-1], nums[i] * minProduct[i-1])
    • maxProduct[i] = max(nums[i], nums[i] * maxProduct[i-1], nums[i] * minProduct[i-1])
    • 这里需要考虑当前元素 nums[i] 的单独乘积,以及与前一个最大和最小乘积相乘的结果。
  3. 初始化maxProduct[0]minProduct[0] 都初始化为 nums[0]

  4. 最终结果:在遍历过程中,记录全局最大乘积并返回。

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。我们需要遍历数组一次。
  • 空间复杂度:O(1),我们只使用了常数个变量来保存状态。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

/**
 * @program: zyfboot-javabasic
 * @description: 乘积最大子数组 (中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:25
 **/
public class MaximumProductSubarray {
    public int maxProduct(int[] nums) {
        // 边界条件
        if (nums == null || nums.length == 0) {
            return 0;
        }

        // 初始化最大和最小乘积为数组第一个元素
        int maxProduct = nums[0];
        int minProduct = nums[0];
        int result = nums[0];

        // 从数组的第二个元素开始遍历
        for (int i = 1; i < nums.length; i++) {
            // 如果当前元素是负数,交换最大和最小乘积
            if (nums[i] < 0) {
                int temp = maxProduct;
                maxProduct = minProduct;
                minProduct = temp;
            }

            // 更新最大和最小乘积
            maxProduct = Math.max(nums[i], nums[i] * maxProduct);
            minProduct = Math.min(nums[i], nums[i] * minProduct);

            // 更新结果
            result = Math.max(result, maxProduct);
        }

        return result;
    }

    public static void main(String[] args) {
        MaximumProductSubarray mps = new MaximumProductSubarray();

        // 测试用例1
        int[] nums1 = {2, 3, -2, 4};
        System.out.println("测试用例1结果: " + mps.maxProduct(nums1)); // 输出:6

        // 测试用例2
        int[] nums2 = {-2, 0, -1};
        System.out.println("测试用例2结果: " + mps.maxProduct(nums2)); // 输出:0
    }
}

89.分割等和子集 (中等)

题目描述

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

解题思路

要判断一个只包含正整数的非空数组 nums 是否可以分割成两个和相等的子集,我们可以使用动态规划来解决这个问题,实际上是一个经典的“分割等和子集”问题。

  1. 计算数组的总和:如果数组的总和是奇数,那么无法分割成两个和相等的子集,因为两个子集的和必须是总和的一半。

  2. 确定子集的目标和:设 target 为总和的一半,即 target = sum(nums) / 2。我们需要找出是否存在一个子集,其和为 target

  3. 动态规划

    • 使用一个布尔数组 dp,其中 dp[j] 表示是否可以从当前子集的元素中找到和为 j 的子集。
    • 初始化 dp[0]true,因为和为 0 的子集始终是存在的(即空子集)。
    • 对于每个元素 num,从 target 开始向下更新 dp 数组,确保每个元素只能使用一次。

复杂度分析

  • 时间复杂度:O(n * target),其中 n 是数组的长度,target 是目标和。
  • 空间复杂度:O(target),即布尔数组的大小。

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

/**
 * @program: zyfboot-javabasic
 * @description: 分割等和子集 (中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:29
 **/
public class PartitionEqualSubsetSum {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        // 计算数组的总和
        for (int num : nums) {
            sum += num;
        }

        // 如果总和是奇数,无法分割成两个和相等的子集
        if (sum % 2 != 0) {
            return false;
        }

        int target = sum / 2;
        // 初始化 dp 数组
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;

        // 动态规划更新 dp 数组
        for (int num : nums) {
            // 从后向前更新 dp 数组,防止重复使用元素
            for (int j = target; j >= num; j--) {
                dp[j] = dp[j] || dp[j - num];
            }
        }

        // 返回是否可以找到和为 target 的子集
        return dp[target];
    }

    public static void main(String[] args) {
        PartitionEqualSubsetSum ps = new PartitionEqualSubsetSum();

        // 测试用例1
        int[] nums1 = {1, 5, 11, 5};
        System.out.println("测试用例1结果: " + ps.canPartition(nums1)); // 输出:true

        // 测试用例2
        int[] nums2 = {1, 2, 3, 5};
        System.out.println("测试用例2结果: " + ps.canPartition(nums2)); // 输出:false
    }
}

90.最长有效括号(困难)

题目描述

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:输入:s = "(()" 输出:2 解释:最长有效括号子串是 "()"

示例 2:输入:s = ")()())" 输出:4 解释:最长有效括号子串是 "()()"

示例 3:输入:s = "" 输出:0

提示:

  • 0 <= s.length <= 3 * 104
  • s[i] 为 '(' 或 ')'

解题思路

  • 使用两个指针从左到右和从右到左分别遍历字符串。
  • 在左到右遍历时,维护计数器 leftright。遇到开括号 ( 增加 left,遇到闭括号 ) 增加 right
  • 如果 leftright 相等,更新最长有效子串长度。如果 right 大于 left,则重置计数器。
  • 从右到左遍历类似,处理由于开括号 ( 数量较多的情况。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

代码实现

package org.zyf.javabasic.letcode.hot100.dynamic;

/**
 * @program: zyfboot-javabasic
 * @description: 最长有效括号(困难)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:34
 **/
public class LongestValidParentheses {
    public int longestValidParentheses(String s) {
        int maxLength = 0;

        // 从左到右遍历
        int left = 0, right = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                left++;
            } else {
                right++;
            }
            if (left == right) {
                maxLength = Math.max(maxLength, 2 * right);
            } else if (right > left) {
                left = right = 0; // 重置计数器
            }
        }

        // 从右到左遍历
        left = right = 0;
        for (int i = s.length() - 1; i >= 0; i--) {
            if (s.charAt(i) == '(') {
                left++;
            } else {
                right++;
            }
            if (left == right) {
                maxLength = Math.max(maxLength, 2 * left);
            } else if (left > right) {
                left = right = 0; // 重置计数器
            }
        }

        return maxLength;
    }

    public static void main(String[] args) {
        LongestValidParentheses lvp = new LongestValidParentheses();

        // 测试用例1
        String s1 = "(()";
        System.out.println("测试用例1结果: " + lvp.longestValidParentheses(s1)); // 输出:2

        // 测试用例2
        String s2 = ")()())";
        System.out.println("测试用例2结果: " + lvp.longestValidParentheses(s2)); // 输出:4

        // 测试用例3
        String s3 = "";
        System.out.println("测试用例3结果: " + lvp.longestValidParentheses(s3)); // 输出:0
    }
}

十六、多维动态规划

91.不同路径 (中等)

题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28

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

示例 3:输入:m = 7, n = 3 输出:28

示例 4:输入:m = 3, n = 3 输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

解题思路

动态规划

  1. 定义状态:使用一个二维数组 dp,其中 dp[i][j] 表示从起始点到位置 (i, j) 的不同路径数量。

  2. 初始化:由于机器人只能从上面或左边到达当前位置 (i, j),所以如果 i = 0j = 0,路径数只有一种,即沿边缘移动。

  3. 状态转移:对于每个位置 (i, j),路径数等于上面位置和左边位置的路径数之和: dp[i][j]=dp[i−1][j]+dp[i][j−1]

代码实现

package org.zyf.javabasic.letcode.hot100.multidimensional;

/**
 * @program: zyfboot-javabasic
 * @description: 不同路径(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:41
 **/
public class UniquePaths {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];

        // 初始化第一行和第一列
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) {
            dp[0][j] = 1;
        }

        // 填充 dp 数组
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }

        return dp[m-1][n-1];
    }

    public static void main(String[] args) {
        UniquePaths up = new UniquePaths();

        // 测试用例1
        System.out.println("测试用例1结果: " + up.uniquePaths(3, 7)); // 输出:28

        // 测试用例2
        System.out.println("测试用例2结果: " + up.uniquePaths(3, 2)); // 输出:3

        // 测试用例3
        System.out.println("测试用例3结果: " + up.uniquePaths(7, 3)); // 输出:28

        // 测试用例4
        System.out.println("测试用例4结果: " + up.uniquePaths(3, 3)); // 输出:6
    }
}

92.最小路径和(中等)

题目描述

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:输入:grid = [[1,2,3],[4,5,6]] 输出:12

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200

解题思路

要解决这个问题可以使用动态规划方法。目标是找到从网格的左上角到右下角的路径,使得路径上的数字总和最小。每次只能向下或向右移动一步。

  1. 定义状态:使用一个二维数组 dp,其中 dp[i][j] 表示到达位置 (i, j) 的最小路径和。

  2. 初始化dp[0][0] 的值等于 grid[0][0],即起点的值。

  3. 状态转移

    对于每个位置 (i, j)dp[i][j] 可以通过从上面 (i-1, j) 或左边 (i, j-1) 到达: dp[i][j]=grid[i][j]+min⁡(dp[i−1][j],dp[i][j−1]),需要处理边界情况:
    • 第一列只可以从上面到达:dp[i][0] = grid[i][0] + dp[i-1][0]
    • 第一行只可以从左边到达:dp[0][j] = grid[0][j] + dp[0][j-1]
  4. 目标:返回 dp[m-1][n-1],即到达右下角的最小路径和。

复杂度分析

  • 时间复杂度:O(m * n),遍历整个网格一次。
  • 空间复杂度:O(m * n),使用了一个 m x n 的二维数组来存储最小路径和。

代码实现

package org.zyf.javabasic.letcode.hot100.multidimensional;

/**
 * @program: zyfboot-javabasic
 * @description: 最小路径和(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:46
 **/
public class MinPathSum {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        // 创建 dp 数组,和 grid 数组大小相同
        int[][] dp = new int[m][n];

        // 初始化 dp 数组的起点
        dp[0][0] = grid[0][0];

        // 初始化第一行
        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }

        // 初始化第一列
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }

        // 填充 dp 数组
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
            }
        }

        // 返回右下角的最小路径和
        return dp[m-1][n-1];
    }

    public static void main(String[] args) {
        MinPathSum mps = new MinPathSum();

        // 测试用例1
        int[][] grid1 = {
                {1, 3, 1},
                {1, 5, 1},
                {4, 2, 1}
        };
        System.out.println("测试用例1结果: " + mps.minPathSum(grid1)); // 输出:7

        // 测试用例2
        int[][] grid2 = {
                {1, 2, 3},
                {4, 5, 6}
        };
        System.out.println("测试用例2结果: " + mps.minPathSum(grid2)); // 输出:12
    }
}

93.最长回文子串(中等)

题目描述

给你一个字符串 s,找到 s 中最长的 回文子串。

示例 1:输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。

示例 2:输入:s = "cbbd" 输出:"bb"

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

解题思路

  • 使用二维数组 dp,其中 dp[i][j] 表示子串 s[i:j+1] 是否是回文。
  • 状态转移:dp[i][j] = true 当且仅当 s[i] == s[j]dp[i+1][j-1]true

复杂度分析

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

代码实现

package org.zyf.javabasic.letcode.hot100.multidimensional;

/**
 * @program: zyfboot-javabasic
 * @description: 最长回文子串(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:50
 **/
public class LongestPalindromicSubstring {
    public String longestPalindrome(String s) {
        int n = s.length();
        if (n == 0) return "";

        boolean[][] dp = new boolean[n][n];
        String longest = "";
        int maxLength = 0;

        for (int length = 1; length <= n; length++) {
            for (int i = 0; i <= n - length; i++) {
                int j = i + length - 1;
                if (length == 1) {
                    dp[i][j] = true;
                } else if (length == 2) {
                    dp[i][j] = s.charAt(i) == s.charAt(j);
                } else {
                    dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1];
                }
                if (dp[i][j] && length > maxLength) {
                    maxLength = length;
                    longest = s.substring(i, j + 1);
                }
            }
        }
        return longest;
    }

    public static void main(String[] args) {
        LongestPalindromicSubstring lps = new LongestPalindromicSubstring();

        // 测试用例1
        String s1 = "babad";
        System.out.println("测试用例1结果: " + lps.longestPalindrome(s1)); // 输出:"bab" 或 "aba"

        // 测试用例2
        String s2 = "cbbd";
        System.out.println("测试用例2结果: " + lps.longestPalindrome(s2)); // 输出:"bb"
    }
}

94.最长公共子序列 (中等)

题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:输入:text1 = "abc", text2 = "abc" 输出:3 解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:输入:text1 = "abc", text2 = "def" 输出:0 解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成。

解题思路

要解决找到两个字符串的最长公共子序列 (LCS) 的问题,可以使用动态规划方法:

  1. 定义状态:使用一个二维数组 dp,其中 dp[i][j] 表示字符串 text1 的前 i 个字符和字符串 text2 的前 j 个字符的最长公共子序列的长度。

  2. 初始化dp[0][j]dp[i][0] 都为 0,因为任何一个字符串和空字符串的公共子序列长度为 0。

  3. 状态转移

    • 如果 text1[i-1] == text2[j-1],则 dp[i][j] = dp[i-1][j-1] + 1
    • 如果 text1[i-1] != text2[j-1],则 dp[i][j] = max(dp[i-1][j], dp[i][j-1]),即去掉一个字符后计算最长公共子序列长度。
  4. 目标:返回 dp[m][n],即两个字符串的最长公共子序列的长度。

复杂度分析

  • 时间复杂度:O(m * n),其中 mn 分别是 text1text2 的长度。
  • 空间复杂度:O(m * n),用于存储动态规划表 dp

代码实现

package org.zyf.javabasic.letcode.hot100.multidimensional;

/**
 * @program: zyfboot-javabasic
 * @description: 最长公共子序列 (中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:54
 **/
public class LongestCommonSubsequence {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();

        // 创建 dp 数组
        int[][] dp = new int[m + 1][n + 1];

        // 填充 dp 数组
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        // 返回最长公共子序列的长度
        return dp[m][n];
    }

    public static void main(String[] args) {
        LongestCommonSubsequence lcs = new LongestCommonSubsequence();

        // 测试用例1
        String text1_1 = "abcde";
        String text2_1 = "ace";
        System.out.println("测试用例1结果: " + lcs.longestCommonSubsequence(text1_1, text2_1)); // 输出:3

        // 测试用例2
        String text1_2 = "abc";
        String text2_2 = "abc";
        System.out.println("测试用例2结果: " + lcs.longestCommonSubsequence(text1_2, text2_2)); // 输出:3

        // 测试用例3
        String text1_3 = "abc";
        String text2_3 = "def";
        System.out.println("测试用例3结果: " + lcs.longestCommonSubsequence(text1_3, text2_3)); // 输出:0
    }
}

95.编辑距离(中等)

题目描述

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')

示例 2:输入:word1 = "intention", word2 = "execution" 输出:5 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1 和 word2 由小写英文字母组成

解题思路

要解决将一个单词 word1 转换成另一个单词 word2 的最少操作数问题,可以使用动态规划算法来计算最小编辑距离(Levenshtein Distance):

  1. 定义状态:使用一个二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数。

  2. 初始化

    • dp[0][0] 为 0,因为两个空字符串之间的转换成本为 0。
    • dp[i][0] 表示将 word1 的前 i 个字符转换为空字符串所需的操作数,即删除所有字符,dp[i][0] = i
    • dp[0][j] 表示将空字符串转换为 word2 的前 j 个字符所需的操作数,即插入所有字符,dp[0][j] = j
  3. 状态转移

    • 如果 word1[i-1] == word2[j-1],则 dp[i][j] = dp[i-1][j-1],即不需要额外操作。
    • 如果 word1[i-1] != word2[j-1],则 dp[i][j] 可以通过以下三种操作之一得到:
      • 插入dp[i][j] = dp[i][j-1] + 1,表示在 word1 的前 i 个字符中插入一个字符。
      • 删除dp[i][j] = dp[i-1][j] + 1,表示在 word1 的前 i 个字符中删除一个字符。
      • 替换dp[i][j] = dp[i-1][j-1] + 1,表示将 word1 的前 i 个字符中的一个字符替换为 word2 的前 j 个字符中的一个字符。
    • 选择最小的操作数:dp[i][j] = \min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1)
  4. 目标:返回 dp[m][n],即将 word1 转换为 word2 所需的最少操作数,其中 mn 分别是 word1word2 的长度。

复杂度分析

  • 时间复杂度:O(m * n),其中 mn 分别是 word1word2 的长度。
  • 空间复杂度:O(m * n),用于存储动态规划表 dp

代码实现

package org.zyf.javabasic.letcode.hot100.multidimensional;

/**
 * @program: zyfboot-javabasic
 * @description: 编辑距离(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 20:59
 **/
public class EditDistance {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();

        // 创建 dp 数组
        int[][] dp = new int[m + 1][n + 1];

        // 初始化 dp 数组
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i; // 将 word1 的前 i 个字符转换为空字符串
        }
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j; // 将空字符串转换为 word2 的前 j 个字符
        }

        // 填充 dp 数组
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(
                            Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
                            dp[i - 1][j - 1] + 1
                    );
                }
            }
        }

        // 返回将 word1 转换为 word2 所需的最少操作数
        return dp[m][n];
    }

    public static void main(String[] args) {
        EditDistance ed = new EditDistance();

        // 测试用例1
        String word1_1 = "horse";
        String word2_1 = "ros";
        System.out.println("测试用例1结果: " + ed.minDistance(word1_1, word2_1)); // 输出:3

        // 测试用例2
        String word1_2 = "intention";
        String word2_2 = "execution";
        System.out.println("测试用例2结果: " + ed.minDistance(word1_2, word2_2)); // 输出:5
    }
}

十七、技巧

96.只出现一次的数字(简单)

题目描述

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

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

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

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

提示:

  • 1 <= nums.length <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104
  • 除了某个元素只出现一次以外,其余每个元素均出现两次。

解题思路

为了找出一个整数数组中只出现一次的那个元素,而其他每个元素均出现两次,我们可以使用 异或操作 的特性来实现,异或操作的特性

  1. 自反性:x⊕x=0。同一个数与自己异或的结果是 0。
  2. 结合律:x⊕(y⊕z)=(x⊕y)⊕z。异或操作可以任意组合。
  3. 单位元:x⊕0=x。任何数与 0 异或的结果是它本身。

解题思路

  1. 初始化:使用一个变量 result 来存储异或的结果,初始化为 0。

  2. 遍历数组:遍历数组中的每个元素,并将其与 result 进行异或操作。

  3. 结果:最后 result 中的值就是只出现一次的那个元素,因为所有其他成对出现的元素都会被消去,剩下的就是唯一出现的元素。

复杂度分析

  • 时间复杂度:O(n),需要遍历数组一次。
  • 空间复杂度:O(1),只使用了常量空间。

代码实现

package org.zyf.javabasic.letcode.hot100.skills;

/**
 * @program: zyfboot-javabasic
 * @description: 只出现一次的数字(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 21:05
 **/
public class SingleNumber {
    public int singleNumber(int[] nums) {
        int result = 0;
        for (int num : nums) {
            result ^= num; // 对每个数字进行异或
        }
        return result; // 返回只出现一次的元素
    }

    public static void main(String[] args) {
        SingleNumber sn = new SingleNumber();

        // 测试用例1
        int[] nums1 = {2, 2, 1};
        System.out.println("测试用例1结果: " + sn.singleNumber(nums1)); // 输出:1

        // 测试用例2
        int[] nums2 = {4, 1, 2, 1, 2};
        System.out.println("测试用例2结果: " + sn.singleNumber(nums2)); // 输出:4

        // 测试用例3
        int[] nums3 = {1};
        System.out.println("测试用例3结果: " + sn.singleNumber(nums3)); // 输出:1
    }
}

97.多数元素(简单)

题目描述

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

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

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

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

提示:

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

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

解题思路

要在数组中找到多数元素(出现次数大于 ⌊n/2⌋\lfloor n/2 \rfloor⌊n/2⌋ 的元素),可以使用 Boyer-Moore 投票算法: Boyer-Moore 投票算法是一种用于找出数组中出现次数最多的元素的高效算法。算法的基本思想是通过两个变量来跟踪候选多数元素和其计数。

  1. 初始化:使用 candidate 变量来存储当前的候选多数元素;使用 count 变量来记录 candidate 的计数。

  2. 遍历数组

    • 遇到 count 为 0 时,将当前元素设置为 candidate
    • 如果当前元素等于 candidate,增加 count
    • 如果当前元素不等于 candidate,减少 count
  3. 结果:遍历完成后,candidate 即为多数元素,因为题目保证数组中总是存在多数元素。

复杂度分析

  • 时间复杂度:O(n),需要遍历数组一次。
  • 空间复杂度:O(1),只使用了常量空间。

代码实现

package org.zyf.javabasic.letcode.hot100.skills;

/**
 * @program: zyfboot-javabasic
 * @description: 多数元素(简单)
 * @author: zhangyanfeng
 * @create: 2024-08-22 21:09
 **/
public class MajorityElement {
    public int majorityElement(int[] nums) {
        int candidate = nums[0];
        int count = 1;

        for (int i = 1; i < nums.length; i++) {
            if (count == 0) {
                candidate = nums[i];
                count = 1;
            } else if (nums[i] == candidate) {
                count++;
            } else {
                count--;
            }
        }

        return candidate;
    }

    public static void main(String[] args) {
        MajorityElement me = new MajorityElement();

        // 测试用例1
        int[] nums1 = {3, 2, 3};
        System.out.println("测试用例1结果: " + me.majorityElement(nums1)); // 输出:3

        // 测试用例2
        int[] nums2 = {2, 2, 1, 1, 1, 2, 2};
        System.out.println("测试用例2结果: " + me.majorityElement(nums2)); // 输出:2
    }
}

98.颜色分类(中等)

题目描述

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

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

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

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i] 为 01 或 2

进阶:你能想出一个仅使用常数空间的一趟扫描算法吗?

解题思路

要对一个只包含红色(0)、白色(1)和蓝色(2)的数组进行排序,可以使用 荷兰国旗问题 的解决方案。这个算法通过一次扫描和常量空间来将数组中的元素排序为红色、白色和蓝色。

荷兰国旗问题是一个经典的数组排序问题,它的核心是通过三个指针来分别处理不同的颜色,从而达到排序的效果。

算法步骤

  1. 定义指针low:表示当前红色区域的结束位置;mid:表示当前正在处理的位置;high:表示当前蓝色区域的开始位置。

  2. 初始化lowmid 都指向数组的起始位置;high 指向数组的末尾。

  3. 处理元素

    • 如果 nums[mid] 是 0(红色),将其与 nums[low] 交换,然后 lowmid 都向右移动一位。
    • 如果 nums[mid] 是 1(白色),直接将 mid 向右移动一位。
    • 如果 nums[mid] 是 2(蓝色),将其与 nums[high] 交换,然后 high 向左移动一位,mid 不变(因为交换后的元素还需要检查)。
  4. 结束条件:当 mid 超过 high 时,排序完成。

复杂度分析

  • 时间复杂度:O(n),每个元素最多被访问和交换一次。
  • 空间复杂度:O(1),只使用了常量空间。

代码实现

package org.zyf.javabasic.letcode.hot100.skills;

/**
 * @program: zyfboot-javabasic
 * @description: 颜色分类(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 21:15
 **/
public class SortColors {
    public void sortColors(int[] nums) {
        int low = 0;
        int mid = 0;
        int high = nums.length - 1;

        while (mid <= high) {
            if (nums[mid] == 0) {
                // 将 0 移动到红色区域
                swap(nums, low, mid);
                low++;
                mid++;
            } else if (nums[mid] == 1) {
                // 1 是白色,直接移动
                mid++;
            } else {
                // 将 2 移动到蓝色区域
                swap(nums, mid, high);
                high--;
            }
        }
    }

    // 交换数组中的两个元素
    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) {
        SortColors sc = new SortColors();

        // 测试用例1
        int[] nums1 = {2, 0, 2, 1, 1, 0};
        sc.sortColors(nums1);
        System.out.println("测试用例1结果: " + java.util.Arrays.toString(nums1)); // 输出:[0, 0, 1, 1, 2, 2]

        // 测试用例2
        int[] nums2 = {2, 0, 1};
        sc.sortColors(nums2);
        System.out.println("测试用例2结果: " + java.util.Arrays.toString(nums2)); // 输出:[0, 1, 2]
    }
}

99.下一个排列(中等)

题目描述

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1] 。

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

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

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

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

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

解题思路

  • 从右向左找到第一个递减的元素

    • 从数组的末尾开始,找到第一个满足 nums[i] < nums[i + 1] 的元素 nums[i]
    • 如果找不到这样的 i,说明整个数组是降序排列的,因此下一个排列是将数组排序为升序。
  • 找到 i 位置后面的最小的大于 nums[i] 的元素:从数组的末尾开始,找到第一个满足 nums[j] > nums[i] 的元素 nums[j]

  • 交换 nums[i]nums[j]:交换这两个元素。

  • 反转 i 位置后面的所有元素:将 i 位置之后的部分反转,以确保得到的排列是下一个最小的排列。

代码实现

package org.zyf.javabasic.letcode.hot100.skills;

/**
 * @program: zyfboot-javabasic
 * @description: 下一个排列(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 21:20
 **/
public class NextPermutation {
    public void nextPermutation(int[] nums) {
        int n = nums.length;
        int i = n - 2;

        // 步骤 1: 找到第一个递减的元素
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }

        if (i >= 0) {
            // 步骤 2: 找到第一个比 nums[i] 大的元素
            int j = n - 1;
            while (nums[j] <= nums[i]) {
                j--;
            }

            // 步骤 3: 交换 nums[i] 和 nums[j]
            swap(nums, i, j);
        }

        // 步骤 4: 反转 i 位置之后的部分
        reverse(nums, i + 1, n - 1);
    }

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

    // 反转数组的部分
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            swap(nums, start, end);
            start++;
            end--;
        }
    }

    public static void main(String[] args) {
        NextPermutation np = new NextPermutation();

        // 测试用例1
        int[] nums1 = {1, 2, 3};
        np.nextPermutation(nums1);
        System.out.println("测试用例1结果: " + java.util.Arrays.toString(nums1)); // 输出:[1, 3, 2]

        // 测试用例2
        int[] nums2 = {3, 2, 1};
        np.nextPermutation(nums2);
        System.out.println("测试用例2结果: " + java.util.Arrays.toString(nums2)); // 输出:[1, 2, 3]

        // 测试用例3
        int[] nums3 = {1, 1, 5};
        np.nextPermutation(nums3);
        System.out.println("测试用例3结果: " + java.util.Arrays.toString(nums3)); // 输出:[1, 5, 1]
    }
}

100.寻找重复数(中等)

题目描述

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

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

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

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

提示:

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

解题思路

要找到数组中唯一的重复数字,并且不修改数组且只用常量空间,可以利用 Floyd 的龟兔赛跑算法(Tortoise and Hare Algorithm),这是一种常用于检测循环的算法

  1. 构建问题模型

    由于数组 nums 中的每个元素都在 [1, n] 范围内,且数组长度为 n + 1,可以将数组视为一个链表,其中每个数字 nums[i] 表示链表中的下一节点的索引;数组中至少存在一个重复数字,意味着在这个链表中会存在环。
  2. 使用 Floyd 的算法检测循环

    • 阶段 1:找到环的相遇点

      使用两个指针:slowfastslow 每次移动一步,fast 每次移动两步;当两个指针相遇时,说明存在一个环。
    • 阶段 2:找到环的入口。将 slow 重置为起点,同时 fast 继续从相遇点出发,每次都移动一步;当两个指针相遇时,该位置即为重复的数字。

复杂度分析

  • 时间复杂度:O(n),算法只需要扫描数组一次。
  • 空间复杂度:O(1),只使用了常量级的额外空间。

代码实现

package org.zyf.javabasic.letcode.hot100.skills;

/**
 * @program: zyfboot-javabasic
 * @description: 寻找重复数(中等)
 * @author: zhangyanfeng
 * @create: 2024-08-22 21:25
 **/
public class FindDuplicateNumber {
    public int findDuplicate(int[] nums) {
        // 阶段 1: 找到环的相遇点
        int slow = nums[0];
        int fast = nums[0];

        // 快慢指针移动,寻找相遇点
        do {
            slow = nums[slow];
            fast = nums[nums[fast]];
        } while (slow != fast);

        // 阶段 2: 找到环的入口
        int finder = nums[0];
        while (finder != slow) {
            finder = nums[finder];
            slow = nums[slow];
        }

        return finder;
    }

    public static void main(String[] args) {
        FindDuplicateNumber fd = new FindDuplicateNumber();

        // 测试用例1
        int[] nums1 = {1, 3, 4, 2, 2};
        System.out.println("测试用例1结果: " + fd.findDuplicate(nums1)); // 输出: 2

        // 测试用例2
        int[] nums2 = {3, 1, 3, 4, 2};
        System.out.println("测试用例2结果: " + fd.findDuplicate(nums2)); // 输出: 3

        // 测试用例3
        int[] nums3 = {3, 3, 3, 3, 3};
        System.out.println("测试用例3结果: " + fd.findDuplicate(nums3)); // 输出: 3
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张彦峰ZYF

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

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

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

打赏作者

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

抵扣说明:

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

余额充值