当前位置: 首页 > news >正文

网站 布局WordPress非首页输出文章

网站 布局,WordPress非首页输出文章,水果电商网站开发方案,小程序制作二维码目录 一、哈希部分 1.两数之和 #xff08;简单#xff09; 2.字母异位词分组 #xff08;中等#xff09; 3.最长连续序列 #xff08;中等#xff09; 二、双指针部分 4.移动零 #xff08;简单#xff09; 5.盛最多水的容器 #xff08;中等#xff09; 6…目录 一、哈希部分 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 - nums[i] - target 只会存在一个有效答案 进阶你可以想出一个时间复杂度小于 O(n2) 的算法吗 解题思路 这个问题可以使用哈希表HashMap来实现。我们可以通过一次遍历数组的方式解决该问题。具体步骤如下 创建一个哈希表用来存储数组中已经访问过的元素及其对应的下标。遍历数组对数组中的每一个元素计算出它与目标值之间的差值 complement。查找补数检查哈希表中是否存在这个差值。如果存在说明我们已经找到了两个数它们的和等于目标值返回它们的下标。更新哈希表如果当前元素的补数不在哈希表中将当前元素和它的下标加入哈希表中。返回结果当找到符合条件的两个数时直接返回它们的下标。 复杂度分析 时间复杂度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) {// 创建一个哈希表来存储已经访问过的元素及其下标MapInteger, 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 0 strs[i].length 100strs[i] 仅包含小写字母 解题思路 要将字母异位词组合在一起我们可以利用哈希表HashMap的特性。字母异位词在重新排列后它们的字符顺序会相同因此我们可以通过以下步骤实现这一目标 创建哈希表用来存储排序后的字符串作为键和原始字符串的列表作为值。遍历字符串数组对于每一个字符串将它的字符排序后作为键如果这个键已经存在于哈希表中那么将当前字符串添加到这个键对应的列表中如果这个键不存在则创建一个新的列表并将当前字符串加入其中。返回结果遍历完成后哈希表中存储的所有值即为字母异位词分组的结果。 复杂度分析 时间复杂度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 ListListString groupAnagrams(String[] strs) {// 创建一个哈希表键为排序后的字符串值为包含该异位词的列表MapString, ListString 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 - nums[i] 解题思路 要在未排序的整数数组中找出最长的连续序列并且时间复杂度要求为 O(n)可以采用哈希集HashSet来进行优化。具体步骤如下 使用哈希集首先将所有元素放入一个哈希集中这样可以在 O(1) 时间内判断某个元素是否存在。遍历数组然后遍历数组中的每个元素对每个元素 x如果 x - 1 不存在于哈希集中说明这个元素 x 可能是某个连续序列的起点。查找最长序列从这个起点开始逐一检查 x1, x2, ... 是否存在于哈希集中统计这个序列的长度。记录最长长度在遍历过程中记录并更新最长的序列长度。 复杂度分析 时间复杂度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) {// 将所有数字放入哈希集SetInteger 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)); // 输出: 4int[] 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 -  nums[i]  - 1 进阶你能尽量减少完成的操作次数吗 解题思路 为了在不复制数组的情况下原地移动所有 0 到数组末尾同时保持非零元素的相对顺序我们可以使用双指针技术来实现。具体步骤如下 使用双指针我们使用两个指针i 和 j。其中i 用于遍历整个数组j 用于记录下一个非零元素应该放置的位置。遍历数组遍历数组当遇到非零元素时将其移动到 j 指针的位置然后将 j 向前移动一位。填充零当所有非零元素都按顺序放置好之后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;}}// 将剩下的位置全部填充为0for (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.length2 n 0 height[i] 解题思路 这个问题可以通过使用双指针的方式来解决。因为我们想要找到两个垂线使得它们能形成的容器容纳最多的水所以可以通过以下步骤实现 初始化双指针一个指针 left 指向数组的起始位置另一个指针 right 指向数组的末尾位置。计算容积在每一步中计算由 left 和 right 指针指向的垂线所形成的容器的容积公式为 min(height[left], height[right]) * (right - left)。移动指针为了找到更大的容积比较 height[left] 和 height[right]将较小的那个指针向中间移动一位如果左侧较小则左指针右移否则右指针左移。更新最大值在每次计算中记录最大容积的值。终止条件当两个指针相遇时遍历结束最大容积即为结果。 复杂度分析 时间复杂度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)); // 输出: 49int[] height2 {1,1};System.out.println(solution.maxArea(height2)); // 输出: 1} }6. 三数之和 中等 题目描述 给你一个整数数组 nums 判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! 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- nums[i] 解题思路 要在数组中找出所有和为 0 且不重复的三元组可以采用排序双指针的方法。具体步骤如下 排序首先对数组进行排序这样可以方便地使用双指针来寻找三元组并且可以避免重复。遍历数组从第一个元素开始固定一个元素接着使用双指针的方法寻找其后面的元素组成的三元组。双指针查找对于固定的元素 nums[i]使用左指针 left 指向 i1右指针 right 指向数组的末尾。当 nums[i] nums[left] nums[right] 0 时说明找到了一个三元组将其加入结果集。然后为了避免重复需要跳过重复的元素。移动指针如果三者之和大于 0说明右指针指向的元素过大需要将右指针左移如果三者之和小于 0说明左指针指向的元素过小需要将左指针右移。跳过重复元素为了避免重复的三元组对于 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 ListListInteger threeSum(int[] nums) {ListListInteger 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.length1 n 2 * 0 height[i] 解题思路 为了计算在柱子之间能够接住多少雨水可以使用双指针的方法。具体步骤如下 初始化指针和变量使用两个指针 left 和 right 分别指向数组的两端。leftMax 和 rightMax 分别记录从左侧到 left 位置的最大高度和从右侧到 right 位置的最大高度。water 用于记录接住的总雨水量。 双指针遍历如果 height[left] 小于 height[right]则说明 left 位置的柱子可能会接住雨水接住的水量取决于 leftMax 和当前 height[left] 的差值。如果 leftMax 大于 height[left]则能接住雨水并将 left 指针右移。否则移动 right 指针并以同样的方式计算 right 位置能接住的雨水量。 更新最大高度每次移动指针时更新 leftMax 或 rightMax以便在接下来的计算中使用。 终止条件当 left 和 right 指针相遇时遍历结束所有的雨水量已经计算完毕。 复杂度分析 时间复杂度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更新 leftMaxif (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();// 测试用例 1int[] height1 {0,1,0,2,1,0,1,3,2,1,2,1};System.out.println(solution.trap(height1)); // 输出: 6// 测试用例 2int[] 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 * s 由英文字母、数字、符号和空格组成 解题思路 为了解决这个问题可以使用滑动窗口的技术。滑动窗口可以动态地维护一个子串并且当发现子串中有重复字符时可以调整窗口的起始位置从而找到不含重复字符的最长子串。 具体步骤如下 初始化使用一个哈希集 set 来存储当前窗口内的字符。使用两个指针 left 和 right 表示滑动窗口的左右边界。初始时left 和 right 都指向字符串的起始位置。 移动窗口当 right 指针所指向的字符未出现在 set 中时将其加入 set并将 right 右移以扩大窗口当 right 指针所指向的字符已经在 set 中时说明出现了重复字符这时需要将 left 指针右移缩小窗口直到窗口内没有重复字符为止。 记录最大长度每次更新窗口后记录当前窗口的长度并与已知的最大长度进行比较保留较大的值。 终止条件当 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) {// 使用哈希集来存储当前窗口内的字符SetCharacter 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();// 测试用例 1String s1 abcabcbb;System.out.println(solution.lengthOfLongestSubstring(s1)); // 输出: 3// 测试用例 2String s2 bbbbb;System.out.println(solution.lengthOfLongestSubstring(s2)); // 输出: 1// 测试用例 3String 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 * 104s 和 p 仅包含小写字母 解题思路 为了解决这个问题我们可以使用滑动窗口和哈希表的方法。滑动窗口可以动态地维护一个长度为 p.length() 的子串同时我们可以通过比较该子串和 p 是否为异位词来判断是否记录其起始索引。具体步骤如下 构建目标频率表我们首先统计字符串 p 中每个字符的出现频率保存到一个数组 pFreq 中。 初始化滑动窗口使用两个指针 left 和 right 表示滑动窗口的左右边界。初始时left 和 right 都指向字符串 s 的起始位置。另外用一个数组 sFreq 来统计当前窗口中字符的频率。 滑动窗口遍历每次将 right 指向的字符加入 sFreq并移动 right 指针。当窗口大小等于 p.length() 时比较 sFreq 和 pFreq 是否相等如果相等则说明该窗口是 p 的一个异位词将 left 的位置加入结果列表。然后移动 left 指针并减少 left 指向字符的频率继续下一轮滑动。 终止条件当 right 指针遍历到字符串 s 的末尾时遍历结束。 复杂度分析 时间复杂度O(n)。其中 n 是字符串 s 的长度。滑动窗口每次移动都需要比较两个频率表这一步是 O(1) 的操作因此整个算法的时间复杂度为 O(n)。 空间复杂度O(1)。虽然我们使用了两个频率表 pFreq 和 sFreq但它们的大小是固定的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 ListInteger findAnagrams(String s, String p) {// 结果列表ListInteger 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();// 测试用例 1String s1 cbaebabacd;String p1 abc;System.out.println(solution.findAnagrams(s1, p1)); // 输出: [0, 6]// 测试用例 2String 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 * -1000 nums[i] 1000- k 解题思路 要解决这个问题我们可以使用前缀和Prefix Sum以及哈希表来优化查找和为 k 的子数组的个数。前缀和的基本思想是通过累积数组元素的和可以快速计算出任意子数组的和。 具体步骤如下 前缀和的定义我们定义 prefixSum[i] 为数组 nums 从第一个元素到第 i 个元素的累积和。即 prefixSum[i] nums[0] nums[1] ... nums[i]。 目标是找到一个下标对 (i, j)使得 prefixSum[j] - prefixSum[i-1] k这里的 i-1 是 prefixSum 中 i 的前一个位置。转换为查找问题就是prefixSum[j] - k prefixSum[i-1]。 使用哈希表记录前缀和的出现次数 我们用一个哈希表 prefixSumCount 来记录每个前缀和出现的次数键为前缀和值为该前缀和的出现次数在遍历数组的过程中计算当前的前缀和 currentSum然后检查哈希表中是否存在 currentSum - k。如果存在则说明在此之前有一个子数组的和为 k将结果计数加上对应的次数。 初始化和遍历 初始化 currentSum 为 0同时在哈希表中加入 prefixSumCount[0] 1表示在开始前有一个空子数组的和为 0遍历数组更新 currentSum并检查 currentSum - k 是否在哈希表中更新结果计数。最后更新哈希表 prefixSumCount 中 currentSum 的次数。 复杂度分析 时间复杂度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) {// 创建一个哈希表记录前缀和出现的次数MapInteger, 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();// 测试用例 1int[] nums1 {1, 1, 1};int k1 2;System.out.println(solution.subarraySum(nums1, k1)); // 输出: 2// 测试用例 2int[] 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 31 [3 -1 -3] 5 3 6 7 31 3 [-1 -3 5] 3 6 7 51 3 -1 [-3 5 3] 6 7 51 3 -1 -3 [5 3 6] 7 61 3 -1 -3 5 [3 6 7] 7示例 2输入nums [1], k 1 输出[1] 提示 1 nums.length -  nums[i] 1 k nums.length 解题思路 要解决这个问题我们可以使用**双端队列Deque**来高效地找到滑动窗口内的最大值。双端队列允许我们在 O(1) 的时间复杂度下在队列的两端进行插入和删除操作。 具体步骤如下 双端队列的定义与维护 我们使用一个双端队列 deque 来存储数组 nums 中元素的索引。这个队列中的索引按元素大小降序排列意味着队列的头部总是当前窗口的最大值每次移动窗口时我们会维护这个队列确保队列中的元素始终属于当前窗口并且在队列头部保存的是当前窗口的最大值。 窗口的移动与更新 随着窗口向右移动我们会依次从数组中添加新的元素到窗口中并将其索引添加到 deque 中如果 deque 中的第一个元素已经不在当前窗口范围内我们将其从 deque 中移除在将新元素加入 deque 时如果 deque 尾部的元素小于新元素则将尾部的元素移除因为这些元素不会再成为最大值。 记录结果 当窗口移动超过大小 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];// 使用双端队列存储索引DequeInteger 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();// 测试用例 1int[] 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();// 测试用例 2int[] 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.lengthn t.length1 m, n s 和 t 由英文字母组成 进阶你能设计一个在 o(mn) 时间内解决此问题的算法吗 解题思路 这个问题要求我们在字符串 s 中找到包含字符串 t 所有字符的最小子串。问题的核心是要用滑动窗口的技巧来找到最小的满足条件的子串。 滑动窗口算法步骤 初始化两个计数器一个用于记录当前窗口中各字符的出现次数 windowCount另一个用于记录字符串 t 中每个字符所需的出现次数 targetCount。 使用两个指针表示滑动窗口left 和 right 都初始化为 0表示滑动窗口的左右边界我们从 right 开始遍历字符串 s并将字符加入窗口中。 移动 right 指针扩大窗口每次移动 right 指针将字符加入 windowCount 中检查当前窗口是否包含字符串 t 中所有字符即检查 windowCount 是否满足 targetCount。 移动 left 指针缩小窗口当窗口包含 t 中所有字符时开始移动 left 指针以缩小窗口尽量找到最小的符合条件的子串在缩小窗口的过程中不断更新最小子串的长度和起始位置。 返回结果当遍历完成后返回找到的最小子串如果没有符合条件的子串则返回空字符串 。 复杂度分析 时间复杂度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中每个字符的出现次数MapCharacter, Integer targetCount new HashMap();for (char c : t.toCharArray()) {targetCount.put(c, targetCount.getOrDefault(c, 0) 1);}// 定义窗口计数器MapCharacter, 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)); // 输出: BANCString s2 a;String t2 a;System.out.println(solution.minWindow(s2, t2)); // 输出: aString 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。 具体步骤 初始化 globalMax 和 currentMax 为数组的第一个元素。从数组的第二个元素开始遍历每个元素更新 currentMax 为 Math.max(currentElement, currentMax currentElement)。即决定是继续扩展当前子数组还是从当前元素重新开始子数组更新 globalMax 为 Math.max(globalMax, currentMax)。返回 globalMax 作为最终结果。 这种方法的时间复杂度为 O(n)空间复杂度为 O(1)。 分治法分治法利用递归将数组分为左右两部分并结合三个部分来找到最大和 左半部分的最大子数组和。右半部分的最大子数组和。跨越中间的最大子数组和。 具体步骤 将数组分成左右两部分。递归地计算左半部分和右半部分的最大子数组和。计算跨越中间的最大子数组和。首先从中间向左扩展找到左侧最大和然后从中间向右扩展找到右侧最大和最后求和得到跨越中间的最大子数组和。返回三个值中的最大值。 这种方法的时间复杂度为 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();// 测试用例 1int[] nums1 {-2, 1, -3, 4, -1, 2, 1, -5, 4};System.out.println(solution.maxSubArray1(nums1)); // 输出: 6// 测试用例 2int[] nums2 {1};System.out.println(solution.maxSubArray1(nums2)); // 输出: 1// 测试用例 3int[] nums3 {5, 4, -1, 7, 8};System.out.println(solution.maxSubArray1(nums3)); // 输出: 23} }14.合并区间 中等 题目描述 以数组 intervals 表示若干个区间的集合其中单个区间为 intervals[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 intervals[i].length 20 解题思路 要解决这个问题我们需要合并所有重叠的区间。以下是详细的解题步骤和思路 排序首先我们需要按区间的起始位置对区间进行排序。这样可以确保我们处理区间时所有重叠区间都在一起便于合并。 合并区间初始化一个结果列表 merged用于存储合并后的区间。 遍历排序后的区间列表。对于每个区间如果当前区间与结果列表中的最后一个区间重叠则合并这两个区间。具体地更新结果列表中的最后一个区间的结束位置为当前区间的结束位置与最后一个区间结束位置中的较大者如果结果列表为空或当前区间与结果列表中的最后一个区间不重叠则将当前区间添加到结果列表。 返回结果遍历完成后结果列表 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]));Listint[] 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();// 测试用例 1int[][] 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]]// 测试用例 2int[][] 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 - nums[i] - 10 k 进阶 尽可能想出更多的解决方案至少有 三种 不同的方法可以解决这个问题。你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗 解题思路 要将数组中的元素向右轮转 k 个位置可以使用几种不同的方法 使用额外空间数组复制 复制原数组的部分到一个新的数组中并将它们合并以得到结果。这种方法简单但使用了额外的空间。 使用反转数组 这种方法不使用额外的空间O(1) 空间复杂度通过数组反转实现轮转。具体步骤是反转后 n-k 个元素反转前 k 个元素反转整个数组。 使用环状替换原地算法 这是一种在 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();// 测试用例 1int[] 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]// 测试用例 2int[] 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 -30 nums[i] 30保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内 进阶你可以在 O(1) 的额外空间复杂度内完成这个题目吗 出于对空间复杂度分析的目的输出数组 不被视为 额外空间。 解题思路 要解决这个问题并且避免使用除法我们可以利用前缀乘积和后缀乘积来计算每个位置的结果。我们可以按照以下步骤进行 前缀乘积 创建一个数组 prefix其中 prefix[i] 存储的是 nums 中从 0 到 i-1 的元素乘积初始化 prefix[0] 为 1表示没有元素前缀的乘积遍历 nums 数组从左到右填充 prefix 数组。 后缀乘积 创建一个变量 suffix用于计算从 i1 到数组末尾的乘积遍历 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();// 测试用例 1int[] nums1 {1, 2, 3, 4};int[] result1 solution.productExceptSelf(nums1);System.out.println(Arrays.toString(result1)); // 输出: [24, 12, 8, 6]// 测试用例 2int[] 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 - nums[i] - 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] 内的元素标记为 n1超出范围的数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;}}// 如果所有位置都被标记则返回 n1return n 1;}public static void main(String[] args) {FirstMissingPositiveSolution solution new FirstMissingPositiveSolution();// 测试用例 1int[] nums1 {1, 2, 0};System.out.println(solution.firstMissingPositive(nums1)); // 输出: 3// 测试用例 2int[] nums2 {3, 4, -1, 1};System.out.println(solution.firstMissingPositive(nums2)); // 输出: 2// 测试用例 3int[] nums3 {7, 8, 9, 11, 12};System.out.println(solution.firstMissingPositive(nums3)); // 输出: 1} }六、矩阵 18.矩阵置零 中等 题目描述 给定一个 m x n 的矩阵如果一个元素为 0 则将其所在行和列的所有元素都设为 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.lengthn matrix[0].length1 m, n 200- matrix[i][j] - 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: 判断第一行和第一列是否需要被置为 0boolean firstRowZero false;boolean firstColZero false;// 检查第一行是否有 0for (int j 0; j n; j) {if (matrix[0][j] 0) {firstRowZero true;break;}}// 检查第一列是否有 0for (int i 0; i m; i) {if (matrix[i][0] 0) {firstColZero true;break;}}// 步骤 2: 使用第一行和第一列作为标记记录其他行和列的 0for (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: 根据标记将相应的行和列置为 0for (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: 根据之前的标记将第一行和第一列置为 0if (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();// 测试用例 1int[][] matrix1 {{1, 1, 1},{1, 0, 1},{1, 1, 1}};solution.setZeroes(matrix1);printMatrix(matrix1); // 输出: [[1, 0, 1], [0, 0, 0], [1, 0, 1]]// 测试用例 2int[][] 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.lengthn matrix[i].length1 m, n 10-100 matrix[i][j] 100 解题思路 初始化边界定义四个边界 top、bottom、left 和 right它们分别代表当前矩阵的上下左右边界。初始值分别为矩阵的四个边界。 遍历矩阵 从左到右遍历当前 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 ListInteger spiralOrder(int[][] matrix) {ListInteger 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();// 测试用例 1int[][] 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]// 测试用例 2int[][] 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 × 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].length1 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 1int[][] 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 2int[][] 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 3int[][] 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.lengthn matrix[i].length1 n, m 300-  matrix[i][j] 每行的所有元素从左到右升序排列每列的所有元素从上到下升序排列-  target 解题思路 为了高效地搜索一个具有特定性质的矩阵中的目标值我们可以利用矩阵的排序特性来设计一个时间复杂度为 O(mn)O(m n)O(mn) 的算法从矩阵的右上角或左下角开始搜索并根据当前元素与目标值的比较结果决定搜索的方向。算法思路: 初始化从矩阵的右上角开始。初始化 row 为 0矩阵的行数 - 1col 为 0矩阵的列数 - 1。 搜索目标值小于当前元素由于每列的元素是升序的目标值在当前列的上方因此我们可以向左移动目标值大于当前元素由于每行的元素是升序的目标值在当前行的下方因此我们可以向下移动目标值等于当前元素找到目标值返回 true。 终止条件当 row 或 col 超出矩阵的边界时说明目标值不在矩阵中返回 false。 复杂度分析 时间复杂度 O(mn)其中 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 1int[][] 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 2int[][] 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 3int[][] matrix3 {{1, 2},{3, 4}};int target3 3;System.out.println(solution.searchMatrix(matrix3, target3)); // Expected: true// Test Case 4int[][] matrix4 {{1}};int target4 0;System.out.println(solution.searchMatrix(matrix4, target4)); // Expected: false} }七、链表 22.相交链表简单 题目描述 给你两个单链表的头节点 headA 和 headB 请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点返回 null 。 图示两个链表在节点 c1 开始相交 题目数据 保证 整个链式结构中不存在环。 注意函数返回结果后链表必须 保持其原始结构 。 自定义评测 评测系统 的输入如下你设计的程序 不适用 此输入 intersectVal - 相交的起始节点的值。如果不存在相交节点这一值为 0listA - 第一个链表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 中节点数目为 mlistB 中节点数目为 n1 m, n 3 * 1 Node.val 0 skipA m0 skipB n如果 listA 和 listB 没有交点intersectVal 为 0如果 listA 和 listB 有交点intersectVal listA[skipA] listB[skipB] 进阶你能否设计一个时间复杂度 O(m n) 、仅用 O(1) 内存的解决方案 解题思路 要解决两个单链表相交问题并找到它们的第一个交点我们可以使用双指针法 使用两个指针 pA 和 pB 分别指向链表 headA 和 headB 的头节点。遍历两个链表逐步移动指针。具体而言如果指针 pA 到达链表 headA 的末尾则将其重新指向 headB同理指针 pB 到达链表 headB 的末尾则将其重新指向 headA。这样当 pA 和 pB 再次遍历到相交的节点时它们将同步到相同的节点。如果两个链表有交点最终这两个指针将相遇于交点如果没有交点它们将最终都变为 null并结束循环。 复杂度分析 时间复杂度 O(mn)每个指针 pA 和 pB 都遍历每个链表一次。空间复杂度 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) {// 如果任何一个链表为空直接返回 nullif (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;}// 返回相交的节点或者 nullreturn 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 指针指向前一个节点更新 prev 和 curr 指针直到遍历完成。 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 105pos 为 -1 或者链表中的一个 有效索引 。 进阶你能用 O(1)即常量内存解决此问题吗 解题思路 判断链表中是否有环可以使用一种高效的算法即 Floyd 的兔子和乌龟算法也叫做 快慢指针算法可以在 O(n) 时间复杂度和 O(1) 空间复杂度下解决问题 使用快慢指针: 初始化两个指针slow 和 fast。slow 每次移动一个节点fast 每次移动两个节点如果链表中存在环那么 slow 和 fast 会在环中相遇如果链表中没有环fast 指针会到达链表末尾即 fast 或 fast.next 变为 null。 过程: 初始化 slow 和 fast 指针都指向链表的头节点遍历链表移动指针fast 移动到下两个节点。slow 移动到下一个节点。如果 slow 和 fast 指针在某一时刻相遇链表中存在环。如果 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 105pos 的值为 -1 或者链表中的一个有效索引 进阶你是否可以使用 O(1) 空间解决此题 解题思路 要找出链表中环的起始节点可以使用 Floyd 的循环检测算法也称为 快慢指针算法并在此基础上进一步寻找环的起始节点。解题思路 检测环的存在:使用快慢指针法兔子和乌龟算法来检测链表是否存在环。快指针每次移动两个节点慢指针每次移动一个节点。如果链表中存在环那么快慢指针会在环中相遇。 找到环的起始节点:当快慢指针相遇时将慢指针移回链表的头部同时保持快指针在相遇点二者都以相同的速度每次移动一个节点继续移动。它们会在环的起始节点相遇。 复杂度分析 时间复杂度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 100l1 和 l2 均按 非递减顺序 排列 解题思路 要将两个升序链表合并为一个新的升序链表我们可以使用双指针法来逐个比较两个链表的节点然后将较小的节点添加到结果链表中。 初始化:创建一个哨兵节点 (dummy)它的 next 指向合并后的链表的头节点。这样可以避免处理链表头部特殊情况。创建一个指针 current指向 dummy。 遍历两个链表:比较 l1 和 l2 的当前节点值将较小的那个节点添加到 current 的 next并移动相应链表的指针current 也相应地向前移动。 处理剩余节点:当一个链表遍历完后将另一个链表的剩余部分直接连接到 current 的 next。 返回结果:返回 dummy.next这是合并后链表的头节点。 复杂度分析 时间复杂度O(m n): m 和 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 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();// 测试用例 1ListNode 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]// 测试用例 2ListNode l3 null;ListNode l4 null;ListNode mergedList2 solution.mergeTwoLists(l3, l4);printList(mergedList2); // 应该输出 []// 测试用例 3ListNode 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题目数据保证列表表示的数字不含前导零 解题思路 要将两个逆序存储的链表表示的非负整数相加并返回一个新的链表表示它们的和可以逐位相加处理进位问题。每一位的加法要考虑两个链表当前节点的值以及前一位的进位。 初始化:创建一个哨兵节点 (dummy)用于构建结果链表创建一个指针 current 指向 dummy初始化进位 carry 为 0。 逐位相加:遍历两个链表直到所有节点都处理完对应位置的值相加再加上 carry计算当前位的和以及新的进位 (sum l1.val l2.val carry新的节点值为 sum % 10新的 carry 为 sum / 10)将计算出的节点值添加到结果链表中。 处理剩余进位:如果最终 carry 不为 0则需要在结果链表末尾添加一个新节点表示进位。 返回结果:返回 dummy.next即结果链表的头节点。 复杂度分析 时间复杂度O(max(m, n)): m 和 n 是两个链表的长度需要遍历较长的链表长度。空间复杂度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();// 测试用例 1ListNode 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]// 测试用例 2ListNode l3 new ListNode(0);ListNode l4 new ListNode(0);ListNode result2 solution.addTwoNumbers(l3, l4);printList(result2); // 输出应该为 [0]// 测试用例 3ListNode 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] 提示 链表中结点的数目为 sz1 sz 300 Node.val 1001 n sz 进阶你能尝试使用一趟扫描实现吗 解题思路 要删除链表中的倒数第 n 个节点进阶要求使用一趟扫描来实现。可以使用双指针法快慢指针来完成这个任务。 初始化双指针:使用两个指针 fast 和 slow都指向链表的头节点。 移动快指针:将 fast 指针先向前移动 n 步这样 fast 和 slow 之间就会有 n 的差距。 同时移动快慢指针:然后同时移动 fast 和 slow 指针直到 fast 到达链表的末尾此时slow 指针正好停在要删除的节点的前一个节点上。 删除节点:修改 slow.next 指针跳过需要删除的节点。 返回链表头:如果删除的是头节点需要特别处理直接返回 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();// 测试用例 1ListNode 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]// 测试用例 2ListNode l2 new ListNode(1);ListNode result2 solution.removeNthFromEnd(l2, 1);printList(result2); // 输出应该为 []// 测试用例 3ListNode 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 解题思路 要实现两两交换链表中的相邻节点可以使用迭代的方法借助指针操作来完成节点的交换。这里我们不修改节点的值只通过调整节点之间的连接顺序来达到目的。 使用虚拟头节点:为了方便处理链表头节点的特殊情况我们可以创建一个虚拟头节点 dummy其 next 指向原链表的头节点。 迭代交换节点:我们使用三个指针 prev、first 和 second 来指向要交换的节点及其前驱节点通过调整指针的连接顺序来交换 first 和 second 节点的位置然后将 prev 移动到 first 的位置继续处理下一个节点对。 返回新头节点:最后返回 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();// 测试用例 1ListNode 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]// 测试用例 2ListNode l2 new ListNode(1);ListNode result2 solution.swapPairs(l2);printList(result2); // 输出应该为 [1]// 测试用例 3ListNode 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] 提示 链表中的节点数目为 n1 k n 50000 Node.val 1000 进阶你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗 解题思路 要解决每 k 个节点一组进行翻转的链表问题我们可以按照以下步骤实现 找到每组的起始节点我们从头节点开始遍历链表找到每 k 个节点作为一组需要翻转的部分。 翻转每组节点对于每一组 k 个节点我们将它们翻转。可以借助一个辅助函数来实现链表的一部分翻转。 处理剩余节点如果最后剩余的节点数量不足 k则保持其原有顺序不动。 链接翻转后的部分在翻转每一组的过程中注意将前一组翻转后的最后一个节点连接到当前组翻转后的第一个节点上。 复杂度分析 时间复杂度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 为当前组的最后一个节点翻转后即 groupStartprevGroupEnd 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();// 测试用例 1ListNode 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]// 测试用例 2ListNode 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 104Node.random 为 null 或指向链表中的节点。 解题思路 要实现一个带有随机指针的链表的深拷贝我们可以使用一个三步法来确保新链表节点的 next 和 random 指针正确指向。 复制每个节点并将其插入到原节点之后我们遍历原链表对于每一个节点我们创建一个新的节点并将其插入到当前节点的 next 位置。这样新节点会紧随其原节点之后。 设置新节点的 random 指针遍历链表设置每个新节点的 random 指针。因为新节点紧随其原节点所以新节点的 random 指针可以通过 oldNode.random.next 得到。 将新链表从原链表中分离出来最后我们再一次遍历链表将新节点从旧节点中分离出来形成新的链表。 复杂度分析 时间复杂度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) 空间复杂度。 找到链表的中间节点使用快慢指针快指针一次走两步慢指针一次走一步当快指针到达末尾时慢指针就在链表的中间位置。递归地对链表的两部分进行排序将链表分成两部分分别对这两部分递归地进行排序。合并两个有序链表使用合并两个有序链表的技巧将两个已经排序的链表合并成一个有序的链表。 复杂度分析 时间复杂度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();// 测试用例 1ListNode 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]// 测试用例 2ListNode 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]// 测试用例 3ListNode 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.length0 k 10^40 lists[i].length 500-10^4 lists[i][j] 10^4lists[i] 按 升序 排列lists[i].length 的总和不超过 10^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;}// 创建优先队列并指定排序规则为节点值的升序PriorityQueueListNode 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();// 示例 1ListNode 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]// 示例 2ListNode[] emptyLists {};ListNode mergedList2 solution.mergeKLists(emptyLists);printList(mergedList2); // 输出[]// 示例 3ListNode[] 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); // 缓存是 {11} lRUCache.put(2, 2); // 缓存是 {11, 22} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废缓存是 {11, 33} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废缓存是 {44, 33} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4提示 1 capacity 30000 key 100000 value 105最多调用 2 * 105 次 get 和 put 解题思路 要实现一个满足 LRU (最近最少使用) 缓存约束的数据结构可以使用 哈希表 配合 双向链表 来实现能够在 O(1) 的时间复杂度下完成 get 和 put 操作。 双向链表 链表用于按顺序存储缓存中的元素头部节点为最近使用的节点尾部节点为最久未使用的节点每次访问缓存中的某个节点时将该节点移动到链表头部插入新节点时将其插入到链表头部。如果缓存超出容量移除链表尾部的节点。 哈希表 哈希表用于存储缓存中每个键值对的地址即链表节点的引用以便在 O(1) 的时间内找到节点并进行移动或删除操作。 实现步骤 get(key)在哈希表中查找对应的节点。如果存在将该节点移动到链表头部并返回节点的值。如果不存在返回 -1put(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 HashMapInteger, 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 进阶: 递归算法很简单你可以通过迭代算法完成吗 解题思路 中序遍历的顺序是 先遍历左子树然后访问根节点最后遍历右子树 要实现二叉树的中序遍历最常见的方式是使用递归。 import java.util.ArrayList; import java.util.List;public class Solution {public ListInteger inorderTraversal(TreeNode root) {ListInteger result new ArrayList();inorder(root, result);return result;}private void inorder(TreeNode node, ListInteger result) {if (node null) {return;}// 先递归遍历左子树inorder(node.left, result);// 然后访问根节点result.add(node.val);// 最后递归遍历右子树inorder(node.right, result);} }迭代方法使用栈来替代递归。具体步骤如下 从根节点开始沿着左子树不断往下走把所有节点压入栈中。当没有左子树时弹出栈顶节点访问该节点然后处理该节点的右子树。 复杂度分析 时间复杂度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 ListInteger inorderTraversal(TreeNode root) {ListInteger result new ArrayList();StackTreeNode 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();ListInteger 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。基础情况是如果节点为空则深度为 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); // 输出应为 3System.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 解题思路 定义翻转二叉树的操作是交换每个节点的左子树和右子树。这个操作从根节点开始然后递归地进行到每个子节点。递归 基本情况如果当前节点为空root null则返回 null。交换子树交换当前节点的左子树和右子树。递归调用递归地对左子树和右子树进行翻转。返回节点返回当前节点其子树已经翻转。 复杂度分析 时间复杂度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 1System.out.println();System.out.print(Inverted Tree 2 (InOrder): );solution.printInOrder(invertedRoot2); // 输出应为 3 2 1System.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. 递归方法 递归检查子树我们需要检查左右子树是否对称。对称的定义是 左子树的左子树与右子树的右子树对称。左子树的右子树与右子树的左子树对称。 步骤 基本情况如果两个子树都为空则它们是对称的如果一个子树为空而另一个子树不为空则它们不是对称的。递归条件检查当前两个节点的值是否相等递归检查左子树的左子树与右子树的右子树递归检查左子树的右子树与右子树的左子树。 2. 迭代方法 使用队列可以使用队列来模拟递归检查过程通过层次遍历来比较每层的节点对称性。步骤 使用一个队列来存储节点对。初始时将根节点的左右子节点加入队列。循环处理队列中的节点对 从队列中取出一对节点。如果两个节点都为空继续下一对节点。如果一个节点为空而另一个不为空返回 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;}QueueTreeNode 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的方法 定义直径直径是树中两个节点之间最长的路径长度。这个路径可能会经过树的根节点也可能不会。 DFS 计算深度 使用 DFS 递归遍历树中的每个节点同时计算每个节点的左右子树的深度对于每个节点计算通过该节点的最长路径长度即左右子树深度之和更新全局变量 maxDiameter 以记录当前最大直径。 计算节点的深度通过递归计算每个节点的左右子树的深度返回节点的最大深度。 复杂度分析 时间复杂度 计算每个节点的深度对于树中的每个节点我们都需要递归地计算其左右子树的深度。每个节点的处理即计算其左右子树深度和更新最大直径只需要常数时间。总时间复杂度由于每个节点在 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();// 示例 1TreeNode 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// 示例 2TreeNode 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来实现。我们逐层遍历树中的节点每次处理一层的所有节点并将它们的子节点加入队列中。下面是实现层序遍历的步骤 初始化队列将根节点放入队列中。遍历队列每次从队列中取出当前层的所有节点处理它们即收集它们的值并将它们的子节点左子节点和右子节点加入队列中。重复步骤 2直到队列为空。返回结果每层的节点值保存在一个列表中最终返回所有层的节点值列表。 复杂度分析 时间复杂度 遍历所有节点每个节点被访问一次处理其值并将其子节点添加到队列中。时间复杂度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 ListListInteger levelOrder(TreeNode root) {ListListInteger result new ArrayList();if (root null) {return result;}QueueTreeNode queue new LinkedList();queue.offer(root);while (!queue.isEmpty()) {int levelSize queue.size();ListInteger 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();ListListInteger 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] 104nums 按 严格递增 顺序排列 解题思路 要将一个升序排列的整数数组转换为一棵平衡二叉搜索树BST我们可以利用递归方法构建树。这是因为一个平衡的BST的中序遍历应该是升序排列的因此我们可以通过递归的方式选择中间的元素作为根节点递归构建左右子树从而保持平衡。 递归构建树 选择数组的中间元素作为根节点递归地构建左子树左子树的节点来源于数组的左半部分递归地构建右子树右子树的节点来源于数组的右半部分。 树的平衡性由于数组已经是升序排列的选择中间元素作为根节点可以保证树的高度平衡。 复杂度分析 时间复杂度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 1int[] nums1 {-10, -3, 0, 5, 9};TreeNode root1 solution.sortedArrayToBST(nums1);printTree(root1); // Output should be a balanced BST// Example 2int[] 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;}QueueTreeNode 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 的性质进行递归检查 定义 左子树的所有节点的值都必须小于当前节点的值右子树的所有节点的值都必须大于当前节点的值每个子树也必须是 BST。 递归方法 在递归中维护一个有效的值范围min 和 max用于确保每个节点的值都在正确的范围内对于每个节点检查其值是否在给定的范围内然后递归检查其左子树和右子树更新有效值范围。 复杂度分析 时间复杂度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 valueif (node.val min || node.val max) {return false;}// Recursively check left and right subtreesreturn isValidBSTHelper(node.left, min, node.val) isValidBSTHelper(node.right, node.val, max);}public static void main(String[] args) {IsValidBSTSolution solution new IsValidBSTSolution();// Example 1TreeNode root1 new TreeNode(2);root1.left new TreeNode(1);root1.right new TreeNode(3);System.out.println(solution.isValidBST(root1)); // Output: true// Example 2TreeNode 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 1040 Node.val 104 进阶如果二叉搜索树经常被修改插入/删除操作并且你需要频繁地查找第 k 小的值你将如何优化算法 解题思路 要查找二叉搜索树BST中的第 k 小元素我们可以利用 BST 的中序遍历特性。中序遍历 BST 会以升序方式访问所有节点因此第 k 小的元素就是中序遍历结果中的第 k 个元素。 中序遍历中序遍历 BST 的结果是一个升序排列的节点值列表。可以使用递归或迭代的方式进行中序遍历。 递归方法在遍历过程中维护一个计数器来记录已经遍历的节点数量当计数器等于 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 visitedprivate int result -1; // To store the kth smallest valuepublic int kthSmallest(TreeNode root, int k) {// Perform in-order traversalinOrderTraversal(root, k);return result;}private void inOrderTraversal(TreeNode node, int k) {if (node null) {return;}// Traverse left subtreeinOrderTraversal(node.left, k);// Visit nodecount;if (count k) {result node.val;return;}// Traverse right subtreeinOrderTraversal(node.right, k);}public static void main(String[] args) {KthSmallestSolution solution new KthSmallestSolution();// Example 1TreeNode 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 2TreeNode 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  解题思路 要从二叉树的右侧查看并返回节点值我们可以使用层序遍历广度优先遍历来实现。具体来说我们需要从右侧依次访问每一层的节点并从每一层的最右侧节点开始返回结果。 层序遍历 使用一个队列来实现层序遍历遍历每一层的节点时记录每层的最后一个节点值因为它代表了从右侧可以看到的节点将每一层的节点值添加到结果列表中。 实现步骤 初始化一个队列将根节点加入队列。对于每一层记录层的节点数即队列的当前大小。遍历该层的所有节点并更新队列将当前节点的左子节点和右子节点加入队列。记录每层最后一个节点的值即右侧可见节点。返回结果列表。 复杂度分析 时间复杂度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 ListInteger rightSideView(TreeNode root) {ListInteger result new ArrayList();if (root null) {return result;}QueueTreeNode 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 1TreeNode 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 2TreeNode root2 new TreeNode(1);root2.right new TreeNode(3);System.out.println(solution.rightSideView(root2)); // Output: [1, 3]// Example 3TreeNode 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。我们可以通过递归或迭代的方式来实现这一目标。 先序遍历按照先序遍历的顺序访问节点将它们连接成链表。 重组树的结构 在遍历过程中每访问一个节点时将其右子树接到链表的末尾将当前节点的左子指针设置为 null并将右子指针指向下一个节点。 实现方法 使用递归的方式来进行先序遍历并在遍历过程中修改树的结构使用迭代的方式也可以实现通过栈来模拟递归的过程。 复杂度分析 时间复杂度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 3000inorder.length preorder.length-3000 preorder[i], inorder[i] 3000preorder 和 inorder 均 无重复 元素inorder 均出现在 preorderpreorder 保证 为二叉树的前序遍历序列inorder 保证 为二叉树的中序遍历序列 解题思路 构造二叉树的过程基于先序遍历preorder和中序遍历inorder信息。在先序遍历中第一个元素是树的根节点而在中序遍历中根节点左边的元素属于左子树右边的元素属于右子树。 确定根节点先序遍历的第一个元素是根节点。分割中序遍历根据根节点在中序遍历中的位置将中序遍历分成左子树和右子树。递归构建子树使用左子树的中序遍历和右子树的中序遍历来递归构建左子树和右子树同时更新先序遍历以反映子树节点。 复杂度分析 时间复杂度构建树的时间复杂度是 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 MapInteger, 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) {// 如果子树为空返回nullif (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();// 示例1int[] 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); // 打印先序遍历检查结果// 示例2int[] 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对平衡树而言h 为 O(log n)对退化树链状树而言h 为 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: 路径总和 III中等* author: zhangyanfeng* create: 2024-08-22 12:12**/ public class PathSumSolution {public int pathSum(TreeNode root, int targetSum) {// 哈希表存储前缀和及其出现次数MapLong, Integer prefix new HashMap();// 初始前缀和为0出现次数为1prefix.put(0L, 1);// 进行深度优先搜索return dfs(root, prefix, 0, targetSum);}private int dfs(TreeNode node, MapLong, 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) {// 构造测试用例1TreeNode 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// 构造测试用例2TreeNode 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 ! qp 和 q 均存在于给定的二叉树中。 解题思路 要找到二叉树中两个指定节点的最近公共祖先LCA可以通过递归算法实现。我们将利用递归来查找左右子树的最近公共祖先并根据节点的情况决定返回的结果。 递归遍历 对于每个节点递归地查找其左右子树中是否存在节点 p 和 q如果在当前节点的左子树中找到了 p 或 q并且在右子树中也找到了另外一个节点那么当前节点就是 p 和 q 的最近公共祖先如果在某一侧的子树中找到了 p 或 q而另一侧子树没有找到则返回找到的节点。 返回条件 当节点为空时递归到叶子节点返回 null当节点等于 p 或 q 时返回该节点本身合并左右子树的结果来确定当前节点是否为 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那么当前节点是 LCAif (left ! null right ! null) {return root;}// 如果左子树找到了 p 或 q则返回左子树的结果否则返回右子树的结果return left ! null ? left : right;}public static void main(String[] args) {// 构造测试用例1TreeNode 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 5TreeNode q1 root1.right; // Node 1TreeNode result1 solution.lowestCommonAncestor(root1, p1, q1);System.out.println(Test Case 1 Result: result1.val); // Expected output: 3// 构造测试用例2TreeNode 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 5TreeNode q2 root2.left.right.right; // Node 4TreeNode result2 solution.lowestCommonAncestor(root2, p2, q2);System.out.println(Test Case 2 Result: result2.val); // Expected output: 5// 构造测试用例3TreeNode root3 new TreeNode(1);root3.left new TreeNode(2);LowestCommonAncestorSolution solution3 new LowestCommonAncestorSolution();TreeNode p3 root3; // Node 1TreeNode q3 root3.left; // Node 2TreeNode 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 解题思路 要解决这个问题我们需要找到二叉树中的最大路径和。这种路径可以是从任何节点开始并到任何节点结束的路径不一定经过根节点。为了实现这一点我们需要考虑路径的两种情况 路径通过当前节点这意味着路径可以从当前节点的左子树或右子树扩展到当前节点并可能继续到右子树或左子树的路径上。路径的总和可能是当前节点的值加上左子树和右子树的最大路径和。 路径不通过当前节点这意味着最大路径和仅仅包括当前节点和它的左或右子树中的路径和但不会跨越到另一侧的子树。 解题思路 递归函数定义一个递归函数 maxPathSum用于计算以当前节点为起点的路径和并返回从当前节点向下延伸的最大路径和在这个函数内部我们会同时更新全局最大路径和变量 maxSum。 路径和计算对于每个节点计算以该节点为根的路径和包括左子树和右子树的最大路径和更新全局最大路径和 maxSum这是当前节点作为路径的“根”的最大路径和。 返回值返回从当前节点向下延伸的最大路径和即节点的值加上左或右子树的最大路径和。 复杂度分析 时间复杂度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) {// 构造测试用例1TreeNode 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// 构造测试用例2TreeNode 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.lengthn grid[i].length1 m, n 300grid[i][j] 的值为 0 或 1 解题思路 这个问题可以通过图遍历算法来解决具体来说可以使用深度优先搜索DFS或者广度优先搜索BFS来遍历网格。这里的主要步骤如下 初始化使用一个二维布尔数组 visited 来标记每个位置是否已被访问。 遍历网格对于网格中的每一个位置如果该位置是陆地1并且没有被访问过那么它是一个新的岛屿的起点。 DFS/BFS 遍历 从当前陆地位置开始使用 DFS 或 BFS 遍历所有与之相连的陆地标记这些位置为已访问以确保不重复计数在 DFS 或 BFS 中访问相邻的四个方向上、下、左、右如果相邻的位置也是陆地并且没有被访问过则继续递归或入队。 计数每当发现一个新的岛屿的起点时将岛屿数量加一。 复杂度分析 时间复杂度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 islandnumIslands;// Perform DFS to mark all connected landdfs(grid, visited, i, j);}}}return numIslands;}private void dfs(char[][] grid, boolean[][] visited, int row, int col) {// Base casesif (row 0 || row grid.length || col 0 || col grid[0].length || grid[row][col] 0 || visited[row][col]) {return;}// Mark this cell as visitedvisited[row][col] true;// Visit all adjacent cells (up, down, left, right)dfs(grid, visited, row - 1, col); // updfs(grid, visited, row 1, col); // downdfs(grid, visited, row, col - 1); // leftdfs(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: 1char[][] 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.lengthn grid[i].length1 m, n 10grid[i][j] 仅为 0、1 或 2 解题思路 这个问题可以通过广度优先搜索BFS来解决。使用 BFS 可以很好地模拟橘子的腐烂过程因为 BFS 会逐层扩展确保每分钟橘子的腐烂过程都被正确地模拟。 初始化使用一个队列 queue 来存储所有初始腐烂橘子的坐标使用一个变量 minutes 来记录所需的时间分钟数。 遍历网格遍历网格找到所有初始的腐烂橘子并将它们的坐标加入队列。 BFS 扩展每次从队列中取出一个腐烂橘子尝试将它周围的四个方向的相邻新鲜橘子腐烂如果发现新鲜橘子腐烂了将它们加入队列并更新分钟数。 检查结果在 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;// 用于保存新鲜橘子的位置Queueint[] 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;}}}// 如果没有新鲜橘子直接返回0if (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;}}// 如果还有新鲜橘子未腐烂返回-1return freshCount 0 ? minutes : -1;}public static void main(String[] args) {OrangesRottingSolution solution new OrangesRottingSolution();// 示例 1int[][] grid1 {{2, 1, 1},{1, 1, 0},{0, 1, 1}};System.out.println(solution.orangesRotting(grid1)); // 输出: 4// 示例 2int[][] grid2 {{2, 1, 1},{0, 1, 1},{1, 0, 1}};System.out.println(solution.orangesRotting(grid2)); // 输出: -1// 示例 3int[][] 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 20000 prerequisites.length 5000prerequisites[i].length 20 ai, bi numCoursesprerequisites[i] 中的所有课程对 互不相同 解题思路 要解决这个问题我们可以使用图的概念将课程作为图中的节点先修课程关系作为有向边。问题转化为判断图中是否存在环。如果图中存在环则无法完成所有课程因为存在循环依赖否则课程可以完成。 具体来说解决这个问题可以使用拓扑排序算法常用的方法包括 Kahns Algorithm基于入度的拓扑排序和深度优先搜索DFS来检测图中的环。 解题思路 构建图使用邻接表表示图记录每个课程的入度指向该课程的边数。 拓扑排序 使用 Kahns Algorithm基于入度 记录处理过的节点数。如果邻接节点的入度变为 0将其加入队列。从队列中取出节点减少其邻接节点的入度。初始化一个队列将所有入度为 0 的节点加入队列。 判断是否存在环如果处理过的节点数等于课程总数则返回 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) {// 创建图的邻接表和入度数组ListInteger[] 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];}// 使用队列进行拓扑排序QueueInteger 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();// 示例 1int numCourses1 2;int[][] prerequisites1 {{1, 0}};System.out.println(solution.canFinish(numCourses1, prerequisites1)); // 输出: true// 示例 2int 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 2000word 和 prefix 仅由小写英文字母组成insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次 解题思路 Trie前缀树是一种高效的树形数据结构专门用于处理字符串的前缀匹配问题。实现 Trie 需要以下几个基本操作 插入字符串insert从根节点开始逐字符遍历字符串。如果字符对应的子节点不存在则创建该子节点插入完成后可以在最后一个字符节点上标记该字符串的结束。 搜索字符串search从根节点开始逐字符遍历字符串。如果某个字符的子节点不存在则说明该字符串不在 Trie 中遍历完所有字符后检查最后一个字符节点是否标记了该字符串的结束。 检查前缀startsWith从根节点开始逐字符遍历前缀。如果某个字符的子节点不存在则说明没有以该前缀开头的字符串遍历完所有字符后只要路径存在即返回 true。 Trie 数据结构 Trie 数据结构的基本组成包括 TrieNode 类表示 Trie 树的节点包含一个 children 字典子节点和一个布尔值 isEndOfWord标记是否为一个单词的结束。Trie 类包含插入、搜索和前缀检查操作的方法。 复杂度分析 时间复杂度insert 和 search 操作的时间复杂度为 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 {// 子节点映射MapCharacter, 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)); // 输出: trueSystem.out.println(trie.search(app)); // 输出: falseSystem.out.println(trie.startsWith(app)); // 输出: truetrie.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] 10nums 中的所有整数 互不相同 解题思路 要生成给定数组 nums 的所有可能的全排列可以使用递归回溯算法来探索所有的排列组合。全排列问题的解决可以通过以下步骤 递归回溯 基准情况当所有的数字都被使用时将当前排列加入结果集中。递归情况选择一个数字作为当前排列的一部分递归地生成剩余数字的全排列并在递归调用完成后撤销选择以探索其他可能的排列。 交换元素 使用交换操作来生成不同的排列。每次选择一个数字作为当前排列的一部分然后递归处理剩余的数字。 复杂度分析 时间复杂度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 ListListInteger permute(int[] nums) {ListListInteger result new ArrayList();backtrack(nums, new ArrayList(), result, new boolean[nums.length]);return result;}private void backtrack(int[] nums, ListInteger current, ListListInteger 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 elementsused[i] true; // Mark as usedcurrent.add(nums[i]); // Add current number to the permutationbacktrack(nums, current, result, used); // Recurcurrent.remove(current.size() - 1); // Backtrackused[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] 10nums 中的所有元素 互不相同 解题思路 要生成给定数组 nums 的所有可能子集即幂集可以使用递归回溯算法。由于题目中的元素互不相同这意味着我们不需要处理重复的子集。 递归回溯 基准情况当处理完所有元素时将当前子集加入结果集中。递归情况撤销选择即回溯以探索其他可能的子集选择当前元素将其加入子集并递归处理下一个元素。 子集生成递归生成所有包含当前元素的子集和所有不包含当前元素的子集。 复杂度分析 时间复杂度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 ListListInteger subsets(int[] nums) {ListListInteger result new ArrayList();backtrack(nums, 0, new ArrayList(), result);return result;}private void backtrack(int[] nums, int start, ListInteger current, ListListInteger 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 4digits[i] 是范围 [2, 9] 的一个数字。 解题思路 要解决这个问题我们可以使用递归回溯算法来生成所有可能的字母组合。每个数字2-9对应着一定的字母这些字母可以用于生成字母组合。我们需要根据输入的数字字符串来生成所有可能的组合。 映射关系创建一个映射将每个数字2-9映射到其对应的字母列表。递归回溯基准情况---当处理完所有数字时将当前生成的字母组合加入结果列表递归情况---对于当前数字对应的每个字母递归生成剩余数字的所有可能的组合。 复杂度分析 时间复杂度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, // 1abc, // 2def, // 3ghi, // 4jkl, // 5mno, // 6pqrs,// 7tuv, // 8wxyz // 9};public ListString letterCombinations(String digits) {ListString result new ArrayList();if (digits null || digits.length() 0) {return result;}backtrack(result, new StringBuilder(), digits, 0);return result;}private void backtrack(ListString 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 302 candidates[i] 40candidates 的所有元素 互不相同1 target 40 解题思路 为了找出数组 candidates 中所有和为目标整数 target 的不同组合我们可以使用回溯算法: 从候选数组中选择一个元素将其加入当前组合。减去该元素的值递归地继续寻找目标值。当目标值为 0 时当前组合符合条件添加到结果中。当目标值小于 0 时当前组合无效返回上一步尝试其他可能性。由于可以重复使用元素因此在递归时不移动到下一个元素而是继续使用当前元素。 复杂度分析 时间复杂度回溯算法的时间复杂度较高最坏情况下可能需要遍历所有可能的组合。时间复杂度大致为 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 ListListInteger combinationSum(int[] candidates, int target) {ListListInteger result new ArrayList();backtrack(result, new ArrayList(), candidates, target, 0);return result;}private void backtrack(ListListInteger result, ListInteger 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。括号组合的有效性一个有效的括号组合必须满足在任何时刻左括号的数量不能少于右括号的数量。 从一个空字符串开始构建括号组合。使用两个计数器一个跟踪左括号的数量leftCount另一个跟踪右括号的数量rightCount。当左括号数量少于 n 时添加一个左括号并递归调用。当右括号数量少于左括号数量时添加一个右括号并递归调用。当左括号和右括号都用完时将当前组合加入结果列表。 复杂度分析 时间复杂度生成所有有效括号组合的时间复杂度为 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 ListString generateParenthesis(int n) {ListString result new ArrayList();backtrack(result, , 0, 0, n);return result;}private void backtrack(ListString 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.lengthn board[i].length1 m, n 61 word.length 15board 和 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)); // truechar[][] board2 {{A,B,C,E},{S,F,C,S},{A,D,E,E}};System.out.println(solution.exist(board2, SEE)); // truechar[][] 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 16s 仅由小写英文字母组成 解题思路 要解决这个问题我们需要生成字符串 s 的所有可能的回文分割方案。回文分割的核心在于每个分割出来的子串都是一个回文串可以通过回溯算法来解决这个问题 回溯算法使用回溯算法来尝试不同的分割方式从当前位置开始尝试将子串切分为回文串并递归地继续分割剩下的字符串。 回文判断在每次切分子串时需要判断当前子串是否是回文串。 回溯步骤 从字符串的起始位置开始尝试所有可能的子串。如果当前子串是回文串则继续递归地处理剩余的字符串。递归到字符串的末尾时记录当前的分割方案。 复杂度分析 时间复杂度回溯算法的时间复杂度在最坏情况下是 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 ListListString partition(String s) {ListListString result new ArrayList();ListString currentList new ArrayList();backtrack(s, 0, currentList, result);return result;}private void backtrack(String s, int start, ListString currentList, ListListString 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();// 示例 1String s1 aab;System.out.println(solution.partition(s1));// 输出: [[a,a,b],[aa,b]]// 示例 2String 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 皇后问题可以使用回溯算法来尝试所有可能的皇后放置方案并且确保每个皇后都满足不攻击其他皇后的条件。 回溯算法从棋盘的第一行开始尝试将皇后放置在该行的每一个位置对每个位置进行尝试时递归地处理下一行需要保证当前放置的皇后不与已经放置的皇后相互攻击即不在同一列、同一行或同一斜线上。 攻击检查使用三个集合来记录已被攻击的列、主对角线和副对角线主对角线和副对角线可以通过索引计算得出分别为 row - col 和 row col。 回溯步骤在每行尝试将皇后放置在每个位置若成功则递归处理下一行如果成功放置了所有皇后则将棋盘状态加入结果列表在回溯阶段需要将当前放置的皇后移除恢复状态以便尝试其他位置。 复杂度分析 时间复杂度最坏情况下回溯算法的时间复杂度是 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 ListListString solveNQueens(int n) {ListListString 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(ListListString 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 ListString construct(char[][] board) {ListString result new ArrayList();for (char[] row : board) {result.add(new String(row));}return result;}public static void main(String[] args) {SolveNQueensSolution solution new SolveNQueensSolution();// 示例 1int n1 4;System.out.println(solution.solveNQueens(n1));// 输出: [[.Q..,...Q,Q...,..Q.],[..Q.,Q...,...Q,.Q..]]// 示例 2int 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] 104nums 为 无重复元素 的 升序 排列数组-104 target 104 解题思路 二分查找算法 通过将查找范围不断减半快速定位目标值的位置或确定目标值的插入位置。如果目标值存在于数组中返回其索引。如果目标值不存在于数组中返回其应该插入的位置以保持数组的排序。 步骤 使用两个指针 left 和 right 来定义当前的查找范围。计算中间位置 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();// 示例 1int[] nums1 {1, 3, 5, 6};int target1 5;System.out.println(solution.searchInsert(nums1, target1)); // 输出: 2// 示例 2int[] nums2 {1, 3, 5, 6};int target2 2;System.out.println(solution.searchInsert(nums2, target2)); // 输出: 1// 示例 3int[] 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.lengthn matrix[i].length1 m, n 100-104 matrix[i][j], target 104 解题思路 要在满足条件的 m×nm \times nm×n 整数矩阵中查找目标值 target可以利用矩阵的递增特性进行高效查找。从矩阵的右上角开始查找 从矩阵的右上角开始查找。如果当前元素等于目标值返回 true。如果当前元素大于目标值向左移动因为当前元素过大。如果当前元素小于目标值向下移动因为当前元素过小。如果超出矩阵的边界返回 false 复杂度分析 时间复杂度 O(mn)O(m n)O(mn)。空间复杂度 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();// 示例 1int[][] matrix1 {{1, 3, 5, 7},{10, 11, 16, 20},{23, 30, 34, 60}};int target1 3;System.out.println(solution.searchMatrix(matrix1, target1)); // 输出: true// 示例 2int[][] 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]  109nums 是一个非递减数组-109  target  109 解题思路 为了在一个按照非递减顺序排列的整数数组 nums 中找出目标值 target 的开始位置和结束位置可以利用二分查找来实现 O(log⁡n) 的时间复杂度,二分查找能有效地定位到目标值的左边界和右边界。 二分查找的两个变种 找左边界找到目标值 target 在数组中的第一个出现位置找右边界找到目标值 target 在数组中的最后一个出现位置。 实现步骤 首先使用二分查找找出目标值 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();// 示例 1int[] 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]// 示例 2int[] 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]// 示例 3int[] 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[k1], ..., 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] 104nums 中的每个值都 独一无二题目数据保证 nums 在预先未知的某个下标上进行了旋转-104 target 104 解题思路 数组特点数组被旋转后分成两个有序的部分一个部分是递增的另一个部分也是递增的但通常在旋转后前后不连贯。 二分查找的改进使用二分查找来决定在旋转后的哪一部分继续查找比较中间值与目标值和边界值以确定当前搜索区间的有序性从而决定在旋转数组的哪一部分进行进一步的搜索。 实现步骤 初始化设定 left 为数组的起始索引right 为数组的结束索引。 二分查找计算中间索引 mid判断 mid 的值和目标值之间的关系根据 mid 所在的部分左半部分或右半部分来决定搜索区间的更新。 搜索区间的更新判断当前区间是否是有序的根据目标值是否在有序部分内来决定更新哪一部分的区间。 复杂度分析 时间复杂度 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();// 示例 1int[] nums1 {4, 5, 6, 7, 0, 1, 2};int target1 0;System.out.println(Output: solution.search(nums1, target1)); // 输出: 4// 示例 2int[] nums2 {4, 5, 6, 7, 0, 1, 2};int target2 3;System.out.println(Output: solution.search(nums2, target2)); // 输出: -1// 示例 3int[] 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.length1 n 5000-5000 nums[i] 5000nums 中的所有整数 互不相同nums 原来是一个升序排序的数组并进行了 1 至 n 次旋转 解题思路 旋转排序数组是一个被旋转过的升序数组因此可以利用旋转的性质来优化查找过程。 旋转数组的特点 数组被旋转后原来的升序数组被分成两个部分其中一个部分是递增的而另一个部分也是递增的旋转后的数组中最小元素总是出现在两个递增部分的交界处。 二分查找的改进使用二分查找来决定在哪一部分继续查找最小元素比较中间值与 left 和 right 值判断中间值和目标值的关系来更新查找范围。 实现步骤 初始化设定 left 为数组的起始索引right 为数组的结束索引。 二分查找计算中间索引 mid比较 mid 的值与 left 和 right 的值以决定搜索区间的更新。 判断旋转位置如果 nums[mid] 是最小值则直接返回 nums[mid]根据 nums[left] 和 nums[mid] 的值判断旋转的部分从而决定更新 left 和 right。 复杂度分析 时间复杂度 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();// 示例 1int[] nums1 {3, 4, 5, 1, 2};System.out.println(Output: solution.findMin(nums1)); // 输出: 1// 示例 2int[] nums2 {4, 5, 6, 7, 0, 1, 2};System.out.println(Output: solution.findMin(nums2)); // 输出: 0// 示例 3int[] nums3 {11, 13, 15, 17};System.out.println(Output: solution.findMin(nums3)); // 输出: 11} }68.寻找两个正序数组的中位数 困难 题目描述 给定两个大小分别为 m 和 n 的正序从小到大数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (mn)) 。 示例 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 mnums2.length n0 m 10000 n 10001 m n 2000-106 nums1[i], nums2[i] 106 解题思路 要找到两个正序数组的中位数并且确保算法的时间复杂度为 O(log⁡(mn))可以使用基于二分查找利用了两个数组的有序特性通过在两个数组中二分查找来定位中位数。 解题思路 定义问题假设有两个数组 nums1 和 nums2长度分别为 m 和 n。我们需要找到这两个数组合并后的中位数。 二分查找的思路使用二分查找在较短的数组上查找来确定两个数组的分割位置。通过调整分割位置使得左半部分的最大值小于右半部分的最小值。 步骤 确保 nums1 是较短的数组。如果不是交换 nums1 和 nums2。使用二分查找确定 nums1 的分割位置 i以及 nums2 的分割位置 j。调整 i 和 j 直到找到合适的分割使得左半部分的最大值小于右半部分的最小值。计算中位数。 算法步骤 初始化设定 i_min 和 i_max 为 nums1 的分割范围计算 j 为 nums1 分割位置的对应值。 二分查找计算中间位置 i计算 j 使得 i j 为合适的分割比较分割边界值来调整 i 和 j。 计算中位数如果总元素数是偶数中位数是两个中间值的平均值如果总元素数是奇数中位数是左半部分的最大值。 复杂度分析 时间复杂度 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 arrayif (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 smalliMin i 1;} else if (i 0 nums1[i - 1] nums2[j]) {// i is too bigiMax i - 1;} else {// i is perfectint 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();// 示例 1int[] nums1 {1, 3};int[] nums2 {2};System.out.println(Output: solution.findMedianSortedArrays(nums1, nums2)); // 输出: 2.0// 示例 2int[] nums3 {1, 2};int[] nums4 {3, 4};System.out.println(Output: solution.findMedianSortedArrays(nums3, nums4)); // 输出: 2.5} }十二、栈 69.有效的括号简单 题目描述 给定一个只包括 (){}[] 的字符串 s 判断字符串是否有效。 有效字符串需满足 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。 示例 1输入s () 输出true 示例 2输入s ()[]{} 输出true 示例 3输入s (] 输出false 提示 1 s.length 104s 仅由括号 ()[]{} 组成 解题思路 要判断给定的字符串 s 中的括号是否有效可以使用栈Stack数据结构来解决。栈非常适合处理括号匹配问题因为它遵循“后进先出”LIFO的原则这与括号的匹配逻辑相符。 使用栈遇到左括号(, {, [时将其压入栈中遇到右括号), }, ]时检查栈顶元素是否匹配 如果匹配将栈顶元素弹出。如果栈为空或栈顶元素不匹配则字符串无效。遍历结束后栈应为空即所有的左括号都有对应的右括号。 匹配逻辑使用一个哈希表来存储左括号和右括号的对应关系[ 对应 ]{ 对应 }( 对应 ) 边界条件空字符串应返回 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) {// 哈希表存储括号对MapCharacter, Character bracketMap new HashMap();bracketMap.put(), ();bracketMap.put(}, {);bracketMap.put(], [);// 使用栈来存储左括号StackCharacter 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();// 示例 1String s1 ();System.out.println(Output: solution.isValid(s1)); // 输出: true// 示例 2String s2 ()[]{};System.out.println(Output: solution.isValid(s2)); // 输出: true// 示例 3String 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 - 1pop、top 和 getMin 操作总是在 非空栈 上调用push, pop, top, and getMin最多被调用 3 * 104 次 解题思路 为了实现一个支持 push、pop、top 操作并能在常数时间内检索到最小元素的栈我们可以使用两个栈来解决这个问题 主栈 (stack) 用于存储所有的元素。辅助栈 (minStack) 用于存储当前的最小值。 设计思路 push(int val):将 val 推入主栈 stack对于 minStack如果 minStack 为空或者 val 小于等于 minStack 栈顶元素则将 val 推入 minStack。 pop():从主栈 stack 弹出元素如果弹出的元素等于 minStack 栈顶元素则也从 minStack 弹出元素因为当前最小值已经被移除。 top():返回主栈 stack 的栈顶元素。 getMin():返回 minStack 的栈顶元素这就是当前的最小值。 复杂度分析 时间复杂度所有操作 (push、pop、top 和 getMin) 都是在常数时间内完成的即 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 StackInteger stack;private StackInteger 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 minStackif (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 wellif (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 -3minStack.pop();System.out.println(minStack.top()); // Returns 0System.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 30s 由小写英文字母、数字和方括号 [] 组成s 保证是一个 有效 的输入。s 中所有整数的取值范围为 [1, 300]  解题思路 要解码经过编码的字符串我们可以使用栈来解决问题。我们可以遍历字符串当遇到数字时记录重复的次数当遇到方括号时开始收集需要重复的字符串当遇到闭括号时弹出栈顶的内容并进行解码。 栈的使用 数字栈 countStack用于保存当前的重复次数字符串栈 stringStack用于保存当前处理的字符串当前字符串 currentString用于累积当前字符直到遇到 ]。 遍历字符串 当遇到数字时可能是一个多位数继续读取直到完整数字并入栈。当遇到 [ 时将当前累积的字符串和数字分别压入 stringStack 和 countStack然后重置 currentString 以开始收集新的字符串。当遇到 ] 时弹出栈顶的字符串和数字进行重复并将结果附加到栈顶字符串后继续处理。当遇到普通字符时直接添加到 currentString。 处理完字符串后将结果合并返回。 复杂度分析 时间复杂度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) {StackInteger countStack new Stack();StackStringBuilder 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])); // 输出 aaabcbcSystem.out.println(ds.decodeString(3[a2[c]])); // 输出 accaccaccSystem.out.println(ds.decodeString(2[abc]3[cd]ef)); // 输出 abcabccdcdcdefSystem.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 10530  temperatures[i]  100 解题思路 要解决这个问题我们可以使用 单调栈 来寻找每一天温度之后的第一个更高温度的天数。这个方法能够高效地解决问题并满足时间复杂度的要求。 单调栈的定义我们维护一个栈栈中的元素存储的是温度的下标。栈中的温度是递减的这样当我们遇到一个比栈顶元素大的温度时就可以知道栈顶元素的下一个更高温度出现在当前下标。 遍历温度数组当栈非空且当前温度高于栈顶温度时说明找到了栈顶温度的下一个更高温度。计算距离并将栈顶元素弹出无论如何都将当前温度的下标压入栈中继续处理下一个温度。 结果数组最终得到的 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];StackInteger 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 1050 heights[i] 104 解题思路 要在柱状图中找到最大的矩形面积可以使用 单调栈 来实现。 单调栈的定义维护一个栈栈中存储的是柱子的下标栈中的柱子的高度是单调递增的。当遇到一个柱子高度小于栈顶柱子高度时说明栈顶柱子可以计算面积了。 计算面积当当前柱子高度小于栈顶柱子高度时从栈中弹出栈顶元素计算以该高度为最矮柱子时形成的矩形面积矩形的宽度是当前下标和栈顶下标的差值减一。 处理剩余柱子最后当遍历完整个数组后栈中可能还会剩下柱子的下标需要继续计算面积。 复杂度分析 时间复杂度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) {StackInteger 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)); // 输出: 10System.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 大的元素而不需要对整个数组排序。 选择一个基准元素pivot通常选择数组的最后一个元素。分区操作将数组划分为两部分左边的元素都大于等于基准元素右边的元素都小于基准元素。递归选择检查基准元素的位置是否就是第 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)); // 输出: 5int[] 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 105k 的取值范围是 [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. 统计每个元素出现的频率MapInteger, Integer frequencyMap new HashMap();for (int num : nums) {frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) 1);}// 2. 创建桶数组频率最高为nums.lengthListInteger[] 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 个元素ListInteger 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存储数据流中较大的一半元素堆顶元素是这部分的最小值。 设计思路 addNum 方法首先将新元素添加到最大堆中然后检查最大堆和最小堆的平衡性。如果最大堆的元素大于最小堆的元素那么将最大堆的堆顶元素移动到最小堆中确保两个堆中的元素数量平衡或最大堆的元素数量最多比最小堆多一个。 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 PriorityQueueInteger maxHeap; // 用于存储较小的一半元素private PriorityQueueInteger 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.5medianFinder.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 1050 prices[i] 104 解题思路 要解决这个问题我们可以采用一个一次遍历的方法。核心思想是记录遍历到每一天时之前几天中的最小价格并计算当日卖出时的最大利润。 初始化变量 minPrice 为正无穷大表示到目前为止最小的买入价格。 初始化变量 maxProfit 为 0表示最大利润。 遍历数组 prices 中的每个价格 如果当前价格比 minPrice 小更新 minPrice否则计算当前价格与 minPrice 的差值表示在当前价格卖出股票时能获得的利润。如果该利润大于 maxProfit更新 maxProfit。 遍历结束后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)); // 输出: 5System.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 1040 nums[i] 105 解题思路 要判断是否能到达数组的最后一个下标可以通过维护一个能够到达的最远位置来实现。具体步骤如下 初始化 maxReach 为 0表示当前能够到达的最远位置。遍历数组 nums 中的每个位置 i如果当前位置 i 超过了 maxReach说明不能到达该位置直接返回 false更新 maxReach 为 i nums[i]表示从当前位置出发能够到达的最远位置。如果遍历结束后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大于最远可达位置返回falsereturn false;}// 更新最远可达位置maxReach Math.max(maxReach, i nums[i]);// 如果最远可达位置已经覆盖最后一个下标直接返回trueif (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)); // 输出: trueSystem.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 1040 nums[i] 1000题目保证可以到达 nums[n-1] 解题思路 要解决这个问题我们可以采用贪心算法来计算到达数组最后一个位置所需的最小跳跃次数。主要思想是遍历数组时始终记录当前能到达的最远位置并在达到该位置时增加跳跃次数直到最终到达数组末尾。 初始化变量 jumps 表示跳跃次数currentEnd 表示当前跳跃的结束位置farthest 表示在当前跳跃内可以到达的最远位置。遍历数组不包括最后一个元素在每一步中更新 farthest表示在当前位置 i 可以到达的最远位置如果当前索引 i 达到了 currentEnd说明需要进行一次跳跃因此将 jumps 增加 1并将 currentEnd 更新为 farthest。当遍历结束时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)); // 输出: 2System.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 500s 仅由小写英文字母组成 解题思路 要解决这个问题我们可以使用贪心算法来找到字符串 s 中每个片段的长度使得每个字母最多出现在一个片段中。具体的步骤如下 确定每个字母的最远出现位置我们首先遍历字符串记录每个字母在字符串中最后一次出现的位置。 划分字符串 再次遍历字符串用两个指针 start 和 end 来标记当前片段的开始和结束位置。end 初始化为当前遍历位置字符的最远出现位置如果遍历到的位置等于 end说明当前片段可以结束了我们记录下这个片段的长度并将 start 更新为下一个位置。 返回结果最终记录下的每个片段的长度就是我们要求的结果。 示例说明 对于输入 ababcbacadefegdehijhklij 字符 a 的最远位置为 8b 为 5c 为 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 ListInteger partitionLabels(String s) {// 存储每个字母在字符串中的最后一次出现位置int[] last new int[26];for (int i 0; i s.length(); i) {last[s.charAt(i) - a] i;}ListInteger 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)); // 输出: 2System.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每个内部元素是上一行中两个相邻元素的和即triangle[i][j] triangle[i-1][j-1] triangle[i-1][j]。 动态规划生成杨辉三角的步骤 创建一个二维列表来存储杨辉三角的结果。从第一行开始逐步构建到需要的行数。使用已生成的行来计算当前行的元素。 复杂度分析 时间复杂度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 ListListInteger generate(int numRows) {ListListInteger triangle new ArrayList();// 遍历每一行for (int i 0; i numRows; i) {ListInteger row new ArrayList();// 每一行的第一个和最后一个元素都是 1row.add(1);// 计算中间的元素for (int j 1; j i; j) {// 每个元素等于上一行的两个相邻元素之和row.add(triangle.get(i - 1).get(j - 1) triangle.get(i - 1).get(j));}// 每一行的最后一个元素也是 1if (i 0) {row.add(1);}// 将当前行添加到杨辉三角中triangle.add(row);}return triangle;}public static void main(String[] args) {PascalTriangle pt new PascalTriangle();int numRows 5;ListListInteger 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 1000 nums[i] 400 解题思路 经典的动态规划问题通常称为“打家劫舍”问题我们的目标是计算在不触发警报的情况下能偷窃到的最大金额。 定义状态使用 dp[i] 表示偷窃到第 i 个房屋时能够获得的最大金额。 状态转移方程 如果选择偷窃第 i 个房屋则不能偷窃第 i-1 个房屋最大金额为 dp[i-2] nums[i]如果不选择偷窃第 i 个房屋则最大金额为 dp[i-1]因此状态转移方程为 dp[i]max⁡(dp[i−1],dp[i−2]nums[i]) 初始状态 dp[0] nums[0]只有一个房屋时只能偷窃这个房屋dp[1] \max(nums[0], nums[1])只有两个房屋时选择偷窃金额更大的那个。 最终结果最终的结果是 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)); // 输出: 4int[] nums2 {2, 7, 9, 3, 1};System.out.println(robber.rob(nums2)); // 输出: 12} }84.完全平方数中等 题目描述 给你一个整数 n 返回 和为 n 的完全平方数的最少数量 。 完全平方数 是一个整数其值等于另一个整数的平方换句话说其值等于一个整数自乘的积。例如1、4、9 和 16 都是完全平方数而 3 和 11 不是。 示例 1输入n 12 输出3 解释12 4 4 4 示例 2输入n 13 输出2 解释13 4 9 提示 1 n 104 解题思路 经典的动态规划问题目的是求解和为 n 的最少完全平方数的数量 定义状态使用 dp[i] 表示和为 i 的最少完全平方数的数量。 状态转移方程 对于每个 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 的最少完全平方数的数量。 初始状态dp[0] 0因为和为 0 时不需要任何完全平方数。 最终结果最终的结果是 dp[n]表示和为 n 的最少完全平方数的数量。 复杂度分析 内层循环的复杂度为 O()外层循环的复杂度为 O(n)因此总体的时间复杂度为 。空间复杂度为 O(n)因为我们需要一个大小为 n1 的数组来存储状态。 代码实现 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);// 当 n0 时最少的完全平方数数量为 0dp[0] 0;// 外层循环遍历从 1 到 n 的所有值计算每个 i 的最小完全平方数数量for (int i 1; i n; i) {// 内层循环遍历所有的完全平方数 j*jfor (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 可以表示为 444最少需要 3 个完全平方数System.out.println(ps.numSquares(12));// 输出 2表示 13 可以表示为 49最少需要 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 121 coins[i] 231 - 10 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数组初始化为maxdp[0] 0; // 金额为0时所需硬币数为0// 遍历每个金额从1到amountfor (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说明无法凑成该金额返回-1return 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 3001 wordDict.length 10001 wordDict[i].length 20s 和 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 的长度m 是 wordDict 的长度。在最坏的情况下需要遍历字符串 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, ListString 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个字符中的最后一个匹配wordif (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();ListString wordDict Lists.newArrayList(leet, code);String s leetcode;System.out.println(solution.wordBreak(s, wordDict)); // 输出: truewordDict Lists.newArrayList(apple, pen);s applepenapple;System.out.println(solution.wordBreak(s, wordDict)); // 输出: truewordDict 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] 保存长度为 i1 的递增子序列的最小末尾元素。通过二分查找定位当前元素应放置在 tail 中的位置从而高效更新序列。 解题思路 辅助数组 tail 的定义 使用一个辅助数组 tailtail[i] 保存长度为 i1 的递增子序列的最小末尾元素。tail 数组是递增的但并不一定是最终的最长递增子序列。通过维护 tail我们可以有效地跟踪最长递增子序列的长度。 遍历 nums 数组 对于每个元素 num使用二分查找来确定它应该插入到 tail 数组中的位置如果 num 可以替换掉 tail 中的一个元素即找到一个比 num 大的最小元素则替换之如果 num 大于 tail 中的所有元素那么就将其添加到 tail 的末尾并增加最长子序列的长度。 返回结果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();// 测试用例1int[] nums1 {10, 9, 2, 5, 3, 7, 101, 18};System.out.println(测试用例1结果: lis.lengthOfLIS(nums1)); // 输出4// 测试用例2int[] nums2 {0, 1, 0, 3, 2, 3};System.out.println(测试用例2结果: lis.lengthOfLIS(nums2)); // 输出4// 测试用例3int[] 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] 10nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数 解题思路 要解决这个问题可以使用动态规划来跟踪每个位置的最大和最小乘积。因为乘积可能会由于负数的存在而变化因此我们需要同时记录最大和最小乘积以便在乘以负数时正确计算。 定义状态 maxProduct[i] 表示以 nums[i] 结尾的子数组中的最大乘积minProduct[i] 表示以 nums[i] 结尾的子数组中的最小乘积。 状态转移 对于每个元素 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] 的单独乘积以及与前一个最大和最小乘积相乘的结果。 初始化maxProduct[0] 和 minProduct[0] 都初始化为 nums[0]。 最终结果在遍历过程中记录全局最大乘积并返回。 复杂度分析 时间复杂度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();// 测试用例1int[] nums1 {2, 3, -2, 4};System.out.println(测试用例1结果: mps.maxProduct(nums1)); // 输出6// 测试用例2int[] 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 2001 nums[i] 100 解题思路 要判断一个只包含正整数的非空数组 nums 是否可以分割成两个和相等的子集我们可以使用动态规划来解决这个问题实际上是一个经典的“分割等和子集”问题。 计算数组的总和如果数组的总和是奇数那么无法分割成两个和相等的子集因为两个子集的和必须是总和的一半。 确定子集的目标和设 target 为总和的一半即 target sum(nums) / 2。我们需要找出是否存在一个子集其和为 target。 动态规划 使用一个布尔数组 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();// 测试用例1int[] nums1 {1, 5, 11, 5};System.out.println(测试用例1结果: ps.canPartition(nums1)); // 输出true// 测试用例2int[] 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 * 104s[i] 为 ( 或 ) 解题思路 使用两个指针从左到右和从右到左分别遍历字符串。在左到右遍历时维护计数器 left 和 right。遇到开括号 ( 增加 left遇到闭括号 ) 增加 right。如果 left 和 right 相等更新最长有效子串长度。如果 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();// 测试用例1String s1 (();System.out.println(测试用例1结果: lvp.longestValidParentheses(s1)); // 输出2// 测试用例2String s2 )()());System.out.println(测试用例2结果: lvp.longestValidParentheses(s2)); // 输出4// 测试用例3String 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 解题思路 动态规划 定义状态使用一个二维数组 dp其中 dp[i][j] 表示从起始点到位置 (i, j) 的不同路径数量。 初始化由于机器人只能从上面或左边到达当前位置 (i, j)所以如果 i 0 或 j 0路径数只有一种即沿边缘移动。 状态转移对于每个位置 (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();// 测试用例1System.out.println(测试用例1结果: up.uniquePaths(3, 7)); // 输出28// 测试用例2System.out.println(测试用例2结果: up.uniquePaths(3, 2)); // 输出3// 测试用例3System.out.println(测试用例3结果: up.uniquePaths(7, 3)); // 输出28// 测试用例4System.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.lengthn grid[i].length1 m, n 2000 grid[i][j] 200 解题思路 要解决这个问题可以使用动态规划方法。目标是找到从网格的左上角到右下角的路径使得路径上的数字总和最小。每次只能向下或向右移动一步。 定义状态使用一个二维数组 dp其中 dp[i][j] 表示到达位置 (i, j) 的最小路径和。 初始化dp[0][0] 的值等于 grid[0][0]即起点的值。 状态转移 对于每个位置 (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] 目标返回 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();// 测试用例1int[][] grid1 {{1, 3, 1},{1, 5, 1},{4, 2, 1}};System.out.println(测试用例1结果: mps.minPathSum(grid1)); // 输出7// 测试用例2int[][] 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 1000s 仅由数字和英文字母组成 解题思路 使用二维数组 dp其中 dp[i][j] 表示子串 s[i:j1] 是否是回文。状态转移dp[i][j] true 当且仅当 s[i] s[j] 且 dp[i1][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();// 测试用例1String s1 babad;System.out.println(测试用例1结果: lps.longestPalindrome(s1)); // 输出bab 或 aba// 测试用例2String 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 1000text1 和 text2 仅由小写英文字符组成。 解题思路 要解决找到两个字符串的最长公共子序列 (LCS) 的问题可以使用动态规划方法: 定义状态使用一个二维数组 dp其中 dp[i][j] 表示字符串 text1 的前 i 个字符和字符串 text2 的前 j 个字符的最长公共子序列的长度。 初始化dp[0][j] 和 dp[i][0] 都为 0因为任何一个字符串和空字符串的公共子序列长度为 0。 状态转移 如果 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])即去掉一个字符后计算最长公共子序列长度。 目标返回 dp[m][n]即两个字符串的最长公共子序列的长度。 复杂度分析 时间复杂度O(m * n)其中 m 和 n 分别是 text1 和 text2 的长度。空间复杂度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();// 测试用例1String text1_1 abcde;String text2_1 ace;System.out.println(测试用例1结果: lcs.longestCommonSubsequence(text1_1, text2_1)); // 输出3// 测试用例2String text1_2 abc;String text2_2 abc;System.out.println(测试用例2结果: lcs.longestCommonSubsequence(text1_2, text2_2)); // 输出3// 测试用例3String 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 500word1 和 word2 由小写英文字母组成 解题思路 要解决将一个单词 word1 转换成另一个单词 word2 的最少操作数问题可以使用动态规划算法来计算最小编辑距离Levenshtein Distance: 定义状态使用一个二维数组 dp其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数。 初始化 dp[0][0] 为 0因为两个空字符串之间的转换成本为 0。dp[i][0] 表示将 word1 的前 i 个字符转换为空字符串所需的操作数即删除所有字符dp[i][0] i。dp[0][j] 表示将空字符串转换为 word2 的前 j 个字符所需的操作数即插入所有字符dp[0][j] j。 状态转移 如果 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)。 目标返回 dp[m][n]即将 word1 转换为 word2 所需的最少操作数其中 m 和 n 分别是 word1 和 word2 的长度。 复杂度分析 时间复杂度O(m * n)其中 m 和 n 分别是 word1 和 word2 的长度。空间复杂度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();// 测试用例1String word1_1 horse;String word2_1 ros;System.out.println(测试用例1结果: ed.minDistance(word1_1, word2_1)); // 输出3// 测试用例2String 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除了某个元素只出现一次以外其余每个元素均出现两次。 解题思路 为了找出一个整数数组中只出现一次的那个元素而其他每个元素均出现两次我们可以使用 异或操作 的特性来实现异或操作的特性 自反性x⊕x0。同一个数与自己异或的结果是 0。结合律x⊕(y⊕z)(x⊕y)⊕z。异或操作可以任意组合。单位元x⊕0x。任何数与 0 异或的结果是它本身。 解题思路 初始化使用一个变量 result 来存储异或的结果初始化为 0。 遍历数组遍历数组中的每个元素并将其与 result 进行异或操作。 结果最后 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();// 测试用例1int[] nums1 {2, 2, 1};System.out.println(测试用例1结果: sn.singleNumber(nums1)); // 输出1// 测试用例2int[] nums2 {4, 1, 2, 1, 2};System.out.println(测试用例2结果: sn.singleNumber(nums2)); // 输出4// 测试用例3int[] 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.length1 n 5 * 104-109 nums[i] 109 进阶尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。 解题思路 要在数组中找到多数元素出现次数大于 ⌊n/2⌋\lfloor n/2 \rfloor⌊n/2⌋ 的元素可以使用 Boyer-Moore 投票算法: Boyer-Moore 投票算法是一种用于找出数组中出现次数最多的元素的高效算法。算法的基本思想是通过两个变量来跟踪候选多数元素和其计数。 初始化使用 candidate 变量来存储当前的候选多数元素使用 count 变量来记录 candidate 的计数。 遍历数组 遇到 count 为 0 时将当前元素设置为 candidate。如果当前元素等于 candidate增加 count。如果当前元素不等于 candidate减少 count。 结果遍历完成后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();// 测试用例1int[] nums1 {3, 2, 3};System.out.println(测试用例1结果: me.majorityElement(nums1)); // 输出3// 测试用例2int[] 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.length1 n 300nums[i] 为 0、1 或 2 进阶你能想出一个仅使用常数空间的一趟扫描算法吗 解题思路 要对一个只包含红色0、白色1和蓝色2的数组进行排序可以使用 荷兰国旗问题 的解决方案。这个算法通过一次扫描和常量空间来将数组中的元素排序为红色、白色和蓝色。 荷兰国旗问题是一个经典的数组排序问题它的核心是通过三个指针来分别处理不同的颜色从而达到排序的效果。 算法步骤 定义指针low表示当前红色区域的结束位置mid表示当前正在处理的位置high表示当前蓝色区域的开始位置。 初始化low 和 mid 都指向数组的起始位置high 指向数组的末尾。 处理元素 如果 nums[mid] 是 0红色将其与 nums[low] 交换然后 low 和 mid 都向右移动一位。如果 nums[mid] 是 1白色直接将 mid 向右移动一位。如果 nums[mid] 是 2蓝色将其与 nums[high] 交换然后 high 向左移动一位mid 不变因为交换后的元素还需要检查。 结束条件当 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();// 测试用例1int[] 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]// 测试用例2int[] 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 1000 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();// 测试用例1int[] nums1 {1, 2, 3};np.nextPermutation(nums1);System.out.println(测试用例1结果: java.util.Arrays.toString(nums1)); // 输出[1, 3, 2]// 测试用例2int[] nums2 {3, 2, 1};np.nextPermutation(nums2);System.out.println(测试用例2结果: java.util.Arrays.toString(nums2)); // 输出[1, 2, 3]// 测试用例3int[] 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 105nums.length n 11 nums[i] nnums 中 只有一个整数 出现 两次或多次 其余整数均只出现 一次 进阶 如何证明 nums 中至少存在一个重复的数字?你可以设计一个线性级时间复杂度 O(n) 的解决方案吗 解题思路 要找到数组中唯一的重复数字并且不修改数组且只用常量空间可以利用 Floyd 的龟兔赛跑算法Tortoise and Hare Algorithm这是一种常用于检测循环的算法 构建问题模型 由于数组 nums 中的每个元素都在 [1, n] 范围内且数组长度为 n 1可以将数组视为一个链表其中每个数字 nums[i] 表示链表中的下一节点的索引数组中至少存在一个重复数字意味着在这个链表中会存在环。 使用 Floyd 的算法检测循环 阶段 1找到环的相遇点。 使用两个指针slow 和 fast。slow 每次移动一步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();// 测试用例1int[] nums1 {1, 3, 4, 2, 2};System.out.println(测试用例1结果: fd.findDuplicate(nums1)); // 输出: 2// 测试用例2int[] nums2 {3, 1, 3, 4, 2};System.out.println(测试用例2结果: fd.findDuplicate(nums2)); // 输出: 3// 测试用例3int[] nums3 {3, 3, 3, 3, 3};System.out.println(测试用例3结果: fd.findDuplicate(nums3)); // 输出: 3} }
http://www.hkea.cn/news/14300156/

相关文章:

  • 一个网站建设的目标网站建设叫什么软件
  • 物流网站系统php源码办公室设计公司
  • 为网站做电影花絮档案互动网站建设
  • 做优化网站注意什么杭州网站建设优化案例
  • 网站免费建站2沧州网站seo
  • 潍坊 开发区网站建设济南工程网站建设
  • 佛山h5模板建站中达世联网站建设
  • 什么网站有题目做网站建设的重点
  • c语言做网站后台服务全国封城名单
  • 户县住房和城乡建设局官方网站广告推广策划
  • 网站技术架构图如何仿制wordpress主题
  • psd简单的网站首页wordpress手机如何登陆
  • wordpress 主题漏洞大庆网站建设优化
  • 高端网站建设软件开发windows优化大师官方免费
  • 刷网站排名 优帮云ai网站大全
  • 周口网站关键词优化网络营销对于个人而言有什么作用
  • 利用帝国软件如何做网站目前最火的大型网络游戏
  • 室内设计网站参考网站 搜索 关键字 description
  • 生活做爰网站自建本地网站服务器wordpress
  • 广州网站建设推广易尚开发公司人事行政经理工作总结及计划
  • 中国做网站知名的公司中国芯片三巨头
  • 网站已改版网站开发和商城的科目
  • django商城网站开发的功能海口网站制作
  • 广东网站建设电话咨询下一页word
  • 霸州网站制作wordpress主题图标乱码
  • 公司官方网站怎么做电子商务网站建设商城网站
  • 网站开发软件 连接SQL数据库无锡建设工程服务中心
  • 网站建设基础考试高端品牌护肤品
  • 汽车最全的网站都江堰市网站建设
  • 上海 网站工作室php网站开发方案