diff --git a/algorithm/AddTwoList2.java b/algorithm/AddTwoList2.java new file mode 100644 index 0000000000000..3fb1e769b112f --- /dev/null +++ b/algorithm/AddTwoList2.java @@ -0,0 +1,215 @@ +package com.funian.algorithm.algorithm; + +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 两数相加(LeetCode 2) + * + * 时间复杂度:O(max(m, n)) + * - m 是第一个链表的长度,n 是第二个链表的长度 + * - 需要遍历两个链表,直到两个链表都遍历完且无进位 + * - 总时间复杂度为两个链表中较长者的长度 + * + * 空间复杂度:O(max(m, n)) + * - 结果链表的长度最多为 max(m, n) + 1 + * - 需要创建新节点存储结果 + */ +public class AddTwoList2 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode(int x) { val = x; } + } + + /** + * 主函数:处理用户输入并输出两数相加的结果 + * + * 算法流程: + * 1. 读取用户输入的两个链表 + * 2. 调用addTwoNumbers方法计算两数之和 + * 3. 输出结果链表 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入第一个链表 + System.out.println("输入第一个链表(用空格分隔每个节点的值):"); + ListNode l1 = createList(scanner); + + // 输入第二个链表 + System.out.println("输入第二个链表(用空格分隔每个节点的值):"); + ListNode l2 = createList(scanner); + + // 调用addTwoNumbers方法计算两数之和 + ListNode result = addTwoNumbers(l1, l2); + System.out.println("结果链表:"); + printList(result); + } + + /** + * 创建链表的辅助方法 + * + * 算法思路: + * 使用哑节点简化链表创建过程 + * 依次将输入的数值转换为链表节点 + * + * 时间复杂度分析: + * - 遍历输入数组:O(k),其中k为输入数字个数 + * + * 空间复杂度分析: + * - 创建链表节点:O(k) + * + * @param scanner Scanner对象用于读取输入 + * @return 创建的链表头节点 + */ + private static ListNode createList(Scanner scanner) { + // 读取一行输入 + String line = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] strArray = line.split(" "); + // 创建哑节点 + ListNode dummy = new ListNode(0); + // 当前节点指针,初始指向哑节点 + ListNode current = dummy; + + // 遍历字符串数组 + for (String s : strArray) { + // 创建新节点并连接到链表中 + current.next = new ListNode(Integer.parseInt(s)); + // 移动当前节点指针 + current = current.next; + } + // 返回链表的头节点(哑节点的下一个节点) + return dummy.next; + + } + + /** + * 打印链表的辅助方法 + * + * 算法思路: + * 从头节点开始依次遍历并打印每个节点的值 + * + * 时间复杂度分析: + * - 遍历链表:O(m),其中m为链表长度 + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param head 链表的头节点 + */ + private static void printList(ListNode head) { + // 遍历链表直到末尾 + while (head != null) { + // 打印当前节点的值 + System.out.print(head.val + " "); + // 移动到下一个节点 + head = head.next; + } + // 换行 + System.out.println(); + } + + /** + * 两数相加的核心方法 + * + * 算法思路: + * 模拟手工加法过程,从低位到高位依次相加 + * 处理进位情况,直到两个链表都遍历完且无进位 + * + * 执行过程分析(以 l1=[2,4,3], l2=[5,6,4] 为例,表示 342 + 465 = 807): + * + * 初始状态: + * l1: 2 -> 4 -> 3 -> null (表示数字 342) + * l2: 5 -> 6 -> 4 -> null (表示数字 465) + * dummy -> null + * current -> dummy + * carry = 0 + * + * 第1位相加(2 + 5): + * sum = 0 + 2 + 5 = 7 + * carry = 7 / 10 = 0 + * 创建节点:new ListNode(7 % 10) = new ListNode(7) + * dummy -> 7 -> null + * current -> 7 + * l1: 4 -> 3 -> null + * l2: 6 -> 4 -> null + * + * 第2位相加(4 + 6): + * sum = 0 + 4 + 6 = 10 + * carry = 10 / 10 = 1 + * 创建节点:new ListNode(10 % 10) = new ListNode(0) + * dummy -> 7 -> 0 -> null + * current -> 0 + * l1: 3 -> null + * l2: 4 -> null + * + * 第3位相加(3 + 4): + * sum = 1 + 3 + 4 = 8 + * carry = 8 / 10 = 0 + * 创建节点:new ListNode(8 % 10) = new ListNode(8) + * dummy -> 7 -> 0 -> 8 -> null + * current -> 8 + * l1: null + * l2: null + * + * 循环结束(l1和l2都为null,且carry为0) + * + * 返回 dummy.next,即 7 -> 0 -> 8 -> null(表示数字 807) + * + * 时间复杂度分析: + * - 遍历两个链表:O(max(m, n)),其中m为第一个链表长度,n为第二个链表长度 + * - 每次循环执行常数时间操作 + * + * 空间复杂度分析: + * - 结果链表节点数:O(max(m, n)) + * - 常数额外变量:O(1) + * + * @param l1 第一个数的链表表示(逆序存储) + * @param l2 第二个数的链表表示(逆序存储) + * @return 两数之和的链表表示(逆序存储) + */ + public static ListNode addTwoNumbers(ListNode l1, ListNode l2) { + // 哑节点,用于简化链表操作 + ListNode dummy = new ListNode(0); + // 当前节点指针,初始指向哑节点 + ListNode current = dummy; + // 进位值,初始为0 + int carry = 0; + + // 遍历两个链表,直到两个链表都遍历完且无进位 + while (l1 != null || l2 != null || carry != 0) { + // 初始化和为进位 + int sum = carry; + + // 如果 l1 不为空,则加上 l1 的值 + if (l1 != null) { + sum += l1.val; + l1 = l1.next; + } + + // 如果 l2 不为空,则加上 l2 的值 + if (l2 != null) { + sum += l2.val; + l2 = l2.next; + } + + // 计算新的进位 + carry = sum / 10; + // 创建新节点,保存当前位的值 + current.next = new ListNode(sum % 10); + // 移动到下一个节点 + current = current.next; + } + + // 返回结果链表(哑节点的下一个节点) + return dummy.next; + } + +} diff --git a/algorithm/BuildTree105.java b/algorithm/BuildTree105.java new file mode 100644 index 0000000000000..d5d2da8ae1977 --- /dev/null +++ b/algorithm/BuildTree105.java @@ -0,0 +1,377 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 从前序与中序遍历序列构造二叉树(LeetCode 105) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(n) + * - hashmap存储中序遍历的索引映射 + * - 递归调用栈的深度最多为n + */ +public class BuildTree105 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + // 用于存储中序遍历中节点值到索引的映射 + private Map inorderMap; + // 前序遍历的索引指针 + private int preorderIndex; + + /** + * 根据前序遍历和中序遍历构建二叉树 + * + * 算法思路: + * 1. 前序遍历的第一个元素是根节点 + * 2. 在中序遍历中找到根节点的位置,将中序遍历分为左右两部分 + * 3. 递归地构建左子树和右子树 + * + * 执行过程分析(以前序遍历[3,9,20,15,7],中序遍历[9,3,15,20,7]为例): + * + * 前序遍历: [3, 9, 20, 15, 7] + * root + * + * 中序遍历: [9, 3, 15, 20, 7] + * 左子树 root 右子树 + * + * 递归构建过程: + * buildTree([3,9,20,15,7], [9,3,15,20,7]) + * ├─ 根节点为3,中序遍历中3的索引为1 + * ├─ 左子树: 前序[9], 中序[9] -> 构建节点9 + * └─ 右子树: 前序[20,15,7], 中序[15,20,7] + * ├─ 根节点为20,中序遍历中20的索引为3 + * ├─ 左子树: 前序[15], 中序[15] -> 构建节点15 + * └─ 右子树: 前序[7], 中序[7] -> 构建节点7 + * + * 构建的二叉树: + * 3 + * / \ + * 9 20 + * / \ + * 15 7 + * + * 时间复杂度分析: + * - 构建hashmap:O(n) + * - 递归构建树:O(n),每个节点访问一次 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - hashmap存储:O(n) + * - 递归调用栈:O(h),h为树的高度,最坏情况O(n) + * - 总空间复杂度:O(n) + * + * @param preorder 前序遍历序列 + * @param inorder 中序遍历序列 + * @return 构建的二叉树根节点 + */ + public TreeNode buildTree(int[] preorder, int[] inorder) { + // 初始化中序遍历的索引映射 + inorderMap = new HashMap<>(); + preorderIndex = 0; + + // 构建中序遍历中节点值到索引的映射 + for (int i = 0; i < inorder.length; i++) { + inorderMap.put(inorder[i], i); + } + + // 调用递归辅助方法构建二叉树 + return buildTreeHelper(0, inorder.length - 1, preorder); + } + + /** + * 递归辅助方法 + * + * 算法思路: + * 1. 如果左边界大于右边界,返回null + * 2. 取前序遍历当前索引的值作为根节点 + * 3. 在中序遍历中找到根节点位置,分割左右子树 + * 4. 递归构建左右子树 + * + * 时间复杂度分析: + * - 每个节点处理一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(h),h为树的高度 + * + * @param left 中序遍历的左边界 + * @param right 中序遍历的右边界 + * @param preorder 前序遍历序列 + * @return 构建的子树根节点 + */ + private TreeNode buildTreeHelper(int left, int right, int[] preorder) { + // 基础情况:如果左边界大于右边界,返回null + if (left > right) { + return null; + } + + // 取前序遍历当前索引的值作为根节点 + int rootVal = preorder[preorderIndex++]; + TreeNode root = new TreeNode(rootVal); + + // 在中序遍历中找到根节点位置 + int rootIndex = inorderMap.get(rootVal); + + // 递归构建左子树:范围是[left, rootIndex - 1] + root.left = buildTreeHelper(left, rootIndex - 1, preorder); + + // 递归构建右子树:范围是[rootIndex + 1, right] + root.right = buildTreeHelper(rootIndex + 1, right, preorder); + + return root; + } + + /** + * 另一种实现方式:不使用全局变量 + * + * 时间复杂度分析: + * - 与主方法相同:O(n) + * + * 空间复杂度分析: + * - 与主方法相同:O(n) + * + * @param preorder 前序遍历序列 + * @param inorder 中序遍历序列 + * @return 构建的二叉树根节点 + */ + public TreeNode buildTreeAlternative(int[] preorder, int[] inorder) { + // 创建中序遍历的索引映射 + Map map = new HashMap<>(); + for (int i = 0; i < inorder.length; i++) { + map.put(inorder[i], i); + } + + // 调用递归辅助方法 + return buildTreeHelperAlternative(preorder, 0, inorder, 0, inorder.length - 1, map); + } + + /** + * 不使用全局变量的递归辅助方法 + * + * 算法思路: + * 通过传递索引参数而非使用全局变量来跟踪构建进度 + * + * 时间复杂度分析: + * - 每个节点处理一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(h),h为树的高度 + * + * @param preorder 前序遍历序列 + * @param preStart 前序遍历的起始索引 + * @param inorder 中序遍历序列 + * @param inStart 中序遍历的起始索引 + * @param inEnd 中序遍历的结束索引 + * @param map 中序遍历的索引映射 + * @return 构建的子树根节点 + */ + private TreeNode buildTreeHelperAlternative(int[] preorder, int preStart, int[] inorder, + int inStart, int inEnd, Map map) { + // 基础情况:如果索引越界,返回null + if (preStart > preorder.length - 1 || inStart > inEnd) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(preorder[preStart]); + + // 在中序遍历中找到根节点位置 + int rootIndex = map.get(root.val); + + // 递归构建左子树 + // 左子树的前序遍历范围:[preStart + 1, preStart + rootIndex - inStart] + // 左子树的中序遍历范围:[inStart, rootIndex - 1] + root.left = buildTreeHelperAlternative(preorder, preStart + 1, inorder, + inStart, rootIndex - 1, map); + + // 递归构建右子树 + // 右子树的前序遍历范围:[preStart + rootIndex - inStart + 1, preEnd] + // 右子树的中序遍历范围:[rootIndex + 1, inEnd] + root.right = buildTreeHelperAlternative(preorder, preStart + rootIndex - inStart + 1, + inorder, rootIndex + 1, inEnd, map); + + return root; + } + + /** + * 辅助方法:读取用户输入的遍历序列 + * + * 算法思路: + * 1. 提示用户输入遍历序列 + * 2. 按空格分割输入字符串 + * 3. 将字符串数组转换为整数数组 + * + * 时间复杂度分析: + * - 处理输入字符串:O(m),m为输入字符数 + * - 转换为整数:O(n),n为元素个数 + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * + * @param prompt 提示信息 + * @return 用户输入的整数数组 + */ + private static int[] readTraversalSequence(String prompt) { + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入 + System.out.println(prompt); + // 读取一行输入 + String input = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] strArray = input.split(" "); + + // 创建整数数组 + int[] nums = new int[strArray.length]; + // for (int i = 0; i < strArray.length; i++) 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // nums[i] = Integer.parseInt(strArray[i]) 将字符串转换为整数 + nums[i] = Integer.parseInt(strArray[i]); + } + + // return nums 返回整数数组 + return nums; + } + + /** + * 辅助方法:前序遍历打印树结构 + * + * 算法思路: + * 按照根-左-右的顺序遍历二叉树并打印节点值 + * + * 时间复杂度分析: + * - 访问每个节点一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(h) + */ + public static void printPreorder(TreeNode root) { + if (root != null) { + System.out.print(root.val + " "); + printPreorder(root.left); + printPreorder(root.right); + } + } + + /** + * 辅助方法:中序遍历打印树结构 + * + * 算法思路: + * 按照左-根-右的顺序遍历二叉树并打印节点值 + * + * 时间复杂度分析: + * - 访问每个节点一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(h) + */ + public static void printInorder(TreeNode root) { + if (root != null) { + printInorder(root.left); + System.out.print(root.val + " "); + printInorder(root.right); + } + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + * + * 时间复杂度分析: + * - 访问每个节点一次:O(n) + * + * 空间复杂度分析: + * - 队列存储节点:O(w),w为树的最大宽度 + */ + public static void printLevelOrder(TreeNode root) { + if (root == null) { + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并演示从前序与中序遍历序列构造二叉树 + * + * 程序执行流程: + * 1. 提示用户输入前序遍历序列 + * 2. 提示用户输入中序遍历序列 + * 3. 调用两种方法构建二叉树 + * 4. 打印构建的二叉树 + */ + public static void main(String[] args) { + System.out.println("从前序与中序遍历序列构造二叉树"); + + // 读取前序遍历序列 + int[] preorder = readTraversalSequence("请输入前序遍历序列,以空格分隔:"); + + // 读取中序遍历序列 + int[] inorder = readTraversalSequence("请输入中序遍历序列,以空格分隔:"); + + // 打印输入的遍历序列 + System.out.println("输入的前序遍历序列: " + Arrays.toString(preorder)); + System.out.println("输入的中序遍历序列: " + Arrays.toString(inorder)); + + // 构建二叉树 + // 创建解决方案实例 + BuildTree105 solution = new BuildTree105(); + TreeNode root1 = solution.buildTree(preorder, inorder); + TreeNode root2 = solution.buildTreeAlternative(preorder, inorder); + + // 打印结果 + System.out.println("\n方法1构建的二叉树:"); + printLevelOrder(root1); + System.out.print("前序遍历验证: "); + printPreorder(root1); + System.out.println(); + System.out.print("中序遍历验证: "); + printInorder(root1); + System.out.println(); + + System.out.println("\n方法2构建的二叉树:"); + printLevelOrder(root2); + System.out.print("前序遍历验证: "); + printPreorder(root2); + System.out.println(); + System.out.print("中序遍历验证: "); + printInorder(root2); + System.out.println(); + } +} diff --git a/algorithm/CanFinish207.java b/algorithm/CanFinish207.java new file mode 100644 index 0000000000000..cf63f96a2eb45 --- /dev/null +++ b/algorithm/CanFinish207.java @@ -0,0 +1,270 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Queue; +import java.util.LinkedList; + +/** + * 课程表(LeetCode 207) + * + * 时间复杂度:O(V + E) + * - V是课程数量(顶点数),E是先决条件数量(边数) + * - 需要遍历所有顶点和边 + * + * 空间复杂度:O(V + E) + * - 邻接表存储图需要O(E)空间 + * - 入度数组和队列需要O(V)空间 + */ +public class CanFinish207 { + + /** + * 判断是否可以完成所有课程 + * + * 算法思路: + * 使用拓扑排序(Kahn算法)检测有向图中是否存在环 + * 1. 构建邻接表表示课程依赖关系 + * 2. 计算每门课程的入度(先修课程数量) + * 3. 将入度为0的课程加入队列(可以立即学习的课程) + * 4. 不断从队列中取出课程,将其后继课程的入度减1 + * 5. 如果后继课程入度变为0,加入队列 + * 6. 最后检查处理的课程数量是否等于总课程数 + * + * 执行过程分析(以numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]为例): + * + * 构建邻接表和入度数组: + * 邻接表:0->[1,2], 1->[3], 2->[3], 3->[] + * 入度数组:[0,1,1,2](课程0入度0,课程1入度1,课程2入度1,课程3入度2) + * + * 拓扑排序过程: + * 1. 初始队列:[0](入度为0的课程) + * 处理课程0:入度数组变为[0,0,0,2],队列变为[1,2] + * + * 2. 队列:[1,2] + * 处理课程1:入度数组变为[0,0,0,1],队列变为[2,3] + * 处理课程2:入度数组变为[0,0,0,0],队列变为[3] + * + * 3. 队列:[3] + * 处理课程3:入度数组变为[0,0,0,0],队列变为[] + * + * 4. 队列为空,处理了4门课程 = 总课程数4 + * 返回true(可以完成所有课程) + * + * @param numCourses 课程总数 + * @param prerequisites 先决条件数组,prerequisites[i] = [ai, bi]表示学习课程ai前必须完成课程bi + * @return 如果可以完成所有课程返回true,否则返回false + */ + public boolean canFinish(int numCourses, int[][] prerequisites) { + // 边界条件检查:没有课程或没有先决条件 + if (numCourses <= 0) { + return true; + } + + // 构建邻接表:graph[i]表示课程i的后续课程列表 + List> graph = new ArrayList<>(); + for (int i = 0; i < numCourses; i++) { + graph.add(new ArrayList<>()); + } + + // 计算每门课程的入度(先修课程数量) + int[] inDegree = new int[numCourses]; + + // 遍历所有先决条件,构建图和入度数组 + for (int[] prerequisite : prerequisites) { + int course = prerequisite[0]; // 当前课程 + int prerequisiteCourse = prerequisite[1]; // 先修课程 + + // 添加边:先修课程 -> 当前课程 + graph.get(prerequisiteCourse).add(course); + + // 增加当前课程的入度 + inDegree[course]++; + } + + // 创建队列,存储入度为0的课程(可以立即学习的课程) + Queue queue = new LinkedList<>(); + + // 将所有入度为0的课程加入队列 + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) { + queue.offer(i); + } + } + + // 记录已处理的课程数量 + int processedCourses = 0; + + // 拓扑排序过程 + while (!queue.isEmpty()) { + // 从队列中取出一个可以学习的课程 + int currentCourse = queue.poll(); + + // 增加已处理课程数量 + processedCourses++; + + // 遍历当前课程的所有后续课程 + for (int nextCourse : graph.get(currentCourse)) { + // 减少后续课程的入度(相当于完成了一门先修课程) + inDegree[nextCourse]--; + + // 如果后续课程的入度变为0,说明可以学习了 + if (inDegree[nextCourse] == 0) { + queue.offer(nextCourse); + } + } + } + + // 如果处理的课程数量等于总课程数,说明可以完成所有课程 + // 否则说明存在循环依赖,无法完成所有课程 + return processedCourses == numCourses; + } + + /** + * 方法2:使用深度优先搜索(DFS)检测环 + * + * 算法思路: + * 使用三色标记法检测有向图中的环 + * 0: 未访问 + * 1: 正在访问(在当前DFS路径中) + * 2: 已完成访问 + * + * @param numCourses 课程总数 + * @param prerequisites 先决条件数组 + * @return 如果可以完成所有课程返回true,否则返回false + */ + public boolean canFinishDFS(int numCourses, int[][] prerequisites) { + // 边界条件检查 + if (numCourses <= 0) { + return true; + } + + // 构建邻接表 + List> graph = new ArrayList<>(); + for (int i = 0; i < numCourses; i++) { + graph.add(new ArrayList<>()); + } + + // 填充邻接表 + for (int[] prerequisite : prerequisites) { + graph.get(prerequisite[1]).add(prerequisite[0]); + } + + // 0: 未访问, 1: 正在访问, 2: 已完成访问 + int[] visited = new int[numCourses]; + + // 对每个未访问的课程进行DFS + for (int i = 0; i < numCourses; i++) { + if (visited[i] == 0) { + if (hasCycle(graph, visited, i)) { + return false; // 发现环,无法完成所有课程 + } + } + } + + return true; // 没有发现环,可以完成所有课程 + } + + /** + * DFS辅助方法:检测是否存在环 + * + * @param graph 邻接表 + * @param visited 访问状态数组 + * @param course 当前课程 + * @return 如果存在环返回true,否则返回false + */ + private boolean hasCycle(List> graph, int[] visited, int course) { + // 如果当前课程正在访问中,说明发现了环 + if (visited[course] == 1) { + return true; + } + + // 如果当前课程已完成访问,说明之前已检查过,无环 + if (visited[course] == 2) { + return false; + } + + // 标记当前课程为正在访问 + visited[course] = 1; + + // 递归访问所有后续课程 + for (int nextCourse : graph.get(course)) { + if (hasCycle(graph, visited, nextCourse)) { + return true; + } + } + + // 标记当前课程为已完成访问 + visited[course] = 2; + + return false; + } + + /** + * 辅助方法:读取用户输入的课程数和先决条件 + * + * @return 包含课程数和先决条件数组的数组 + */ + public static Object[] readInput() { + Scanner scanner = new Scanner(System.in); + + // 读取课程总数 + System.out.print("请输入课程总数: "); + int numCourses = scanner.nextInt(); + + // 读取先决条件数量 + System.out.print("请输入先决条件数量: "); + int preCount = scanner.nextInt(); + + // 读取先决条件 + int[][] prerequisites = new int[preCount][2]; + System.out.println("请输入先决条件(格式:课程编号 先修课程编号):"); + for (int i = 0; i < preCount; i++) { + prerequisites[i][0] = scanner.nextInt(); // 课程编号 + prerequisites[i][1] = scanner.nextInt(); // 先修课程编号 + } + + return new Object[]{numCourses, prerequisites}; + } + + /** + * 辅助方法:打印先决条件 + * + * @param prerequisites 先决条件数组 + */ + public static void printPrerequisites(int[][] prerequisites) { + System.out.println("先决条件:"); + for (int[] pre : prerequisites) { + System.out.println("学习课程 " + pre[0] + " 前必须完成课程 " + pre[1]); + } + } + + /** + * 主函数:处理用户输入并判断是否可以完成所有课程 + */ + public static void main(String[] args) { + System.out.println("课程表问题"); + + // 读取用户输入 + Object[] input = readInput(); + int numCourses = (int) input[0]; + int[][] prerequisites = (int[][]) input[1]; + + // 打印输入信息 + System.out.println("课程总数: " + numCourses); + printPrerequisites(prerequisites); + + // 创建解决方案对象 + CanFinish207 solution = new CanFinish207(); + + // 使用BFS方法判断 + boolean result1 = solution.canFinish(numCourses, prerequisites); + + // 使用DFS方法判断 + boolean result2 = solution.canFinishDFS(numCourses, prerequisites); + + // 输出结果 + System.out.println("BFS方法结果: " + (result1 ? "可以完成所有课程" : "无法完成所有课程")); + System.out.println("DFS方法结果: " + (result2 ? "可以完成所有课程" : "无法完成所有课程")); + } +} diff --git a/algorithm/CanPartition416.java b/algorithm/CanPartition416.java new file mode 100644 index 0000000000000..1aca5cbeecc79 --- /dev/null +++ b/algorithm/CanPartition416.java @@ -0,0 +1,289 @@ +package com.funian.algorithm.algorithm; + +/** + * 分割等和子集(LeetCode 416)- 动态规划(0-1背包问题) + * + * 时间复杂度:O(n * sum) + * - n是数组长度,sum是数组元素和的一半 + * - 需要遍历每个元素,并对每个元素更新dp数组 + * + * 空间复杂度:O(sum) + * - 只需要大小为sum+1的DP数组 + */ +import java.util.Scanner; +import java.util.Arrays; + +public class CanPartition416 { + + /** + * 主函数:处理用户输入并判断数组是否可以分割成两个和相等的子集 + * + * 算法流程: + * 1. 读取用户输入的正整数数组 + * 2. 调用 [canPartition](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/CanPartition416.java#L113-L154)方法判断是否可以分割 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入正整数数组 + System.out.print("请输入正整数数组(用空格分隔):"); + // 读取输入并以空格分割成字符串数组 + String[] input = scanner.nextLine().split(" "); + + // 将字符串数组转换为整数数组 + int[] nums = new int[input.length]; + for (int i = 0; i < input.length; i++) { + nums[i] = Integer.parseInt(input[i]); // 转换字符串为整数 + } + + // 调用 canPartition 方法判断是否可以分割 + boolean result = canPartition(nums); + // 输出结果 + System.out.println("是否可以分割成两个和相等的子集:" + result); + } + + /** + * 判断数组是否可以分割成两个和相等的子集 + * + * 算法思路: + * 这是一个0-1背包问题的变种 + * 问题转化为:能否从数组中选出一些元素,使其和等于总和的一半 + * + * 状态定义: + * `dp[i]` 表示是否能选出一些元素使其和等于 i + * + * 状态转移: + * 对于每个元素`num`,更新`dp`数组: + * `dp[j] = dp[j] || dp[j - num]` + * (要么不选`num`仍能达到和`j`,要么选`num`从和`j-num`达到和`j`) + * + * 执行过程分析(以`nums=[1,5,11,5]`为例): + * + * 第一步:计算总和 + * totalSum = 1+5+11+5 = 22 + * target = 22/2 = 11 + * + * 第二步:初始化DP数组 + * dp = [T, F, F, F, F, F, F, F, F, F, F, F] + * 索引: 0 1 2 3 4 5 6 7 8 9 10 11 + * + * 第三步:遍历每个元素更新DP数组 + * + * 处理num=1: + * j=11: dp[11] = dp[11] || dp[10] = F || F = F + * ... + * j=1: dp[1] = dp[1] || dp[0] = F || T = T + * dp = [T, T, F, F, F, F, F, F, F, F, F, F] + * + * 处理num=5: + * j=11: dp[11] = dp[11] || dp[6] = F || F = F + * ... + * j=6: dp[6] = dp[6] || dp[1] = F || T = T + * j=5: dp[5] = dp[5] || dp[0] = F || T = T + * dp = [T, T, F, F, F, T, T, F, F, F, F, F] + * + * 处理num=11: + * j=11: dp[11] = dp[11] || dp[0] = F || T = T + * dp = [T, T, F, F, F, T, T, F, F, F, F, T] + * + * 处理num=5: + * j=11: dp[11] = dp[11] || dp[6] = T || T = T + * ... + * + * 最终结果:dp[11] = T,可以分割 + * 一个可能的分割方案:[1,5,5] 和 [11],和都为11 + * + * 执行过程分析(以`nums=[1,2,3,5]`为例): + * + * 第一步:计算总和 + * totalSum = 1+2+3+5 = 11(奇数) + * 直接返回false + * + * 时间复杂度分析: + * - 计算总和:O(n) + * - 更新DP数组:O(n * sum) + * - 总时间复杂度:O(n * sum) + * + * 空间复杂度分析: + * - DP数组存储空间:O(sum) + * + * @param nums 输入的正整数数组 + * @return 如果可以分割成两个和相等的子集返回true,否则返回false + */ + public static boolean canPartition(int[] nums) { + int totalSum = 0; // 初始化总和 + + // 计算数组中所有元素的总和 + for (int num : nums) { + totalSum += num; // 累加元素 + } + + // 如果总和为奇数,无法分割成两个相等的部分 + // 这是一个重要的边界条件 + if (totalSum % 2 != 0) { + return false; // 直接返回 false + } + + // 目标子集和(总和的一半) + int target = totalSum / 2; + + // 创建动态规划数组 + // dp[i] 表示是否能选出一些元素使其和等于 i + boolean[] dp = new boolean[target + 1]; + + // 0 可以通过不选择任何元素得到(基础情况) + dp[0] = true; + + // 遍历每个元素 + for (int num : nums) { + // 从目标值开始倒序更新 dp 数组 + // 倒序是为了避免重复使用同一个元素 + // 如果正序更新,dp[j-num]可能已经在本轮中被更新过 + for (int j = target; j >= num; j--) { + // 更新 dp[j],表示是否可以通过当前元素达到和 j + // dp[j] = dp[j] || dp[j - num] + // 要么不选num仍能达到和j,要么选num从和j-num达到和j + dp[j] = dp[j] || dp[j - num]; + } + } + + // 返回 dp[target],表示是否能够通过选择元素得到目标子集和 + return dp[target]; + } + + /** + * 方法2:二维DP解法(更直观的理解) + * + * 算法思路: + * `dp[i][j]` 表示前i个元素是否能选出一些使其和等于j + * + * 时间复杂度分析: + * - 初始化DP表:O(n) + * - 填充DP表:O(n * sum) + * - 总时间复杂度:O(n * sum) + * + * 空间复杂度分析: + * - 二维DP表存储空间:O(n * sum) + * + * @param nums 输入的正整数数组 + * @return 如果可以分割成两个和相等的子集返回true,否则返回false + */ + public boolean canPartition2D(int[] nums) { + int totalSum = 0; + for (int num : nums) { + totalSum += num; + } + + if (totalSum % 2 != 0) { + return false; + } + + int target = totalSum / 2; + int n = nums.length; + + // dp[i][j] 表示前i个元素是否能选出一些使其和等于j + boolean[][] dp = new boolean[n + 1][target + 1]; + + // 基础情况:不选任何元素可以得到和0 + for (int i = 0; i <= n; i++) { + dp[i][0] = true; + } + + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= target; j++) { + // 不选第i个元素 + dp[i][j] = dp[i - 1][j]; + // 选第i个元素(如果可能) + if (j >= nums[i - 1]) { + dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]]; + } + } + } + + return dp[n][target]; + } + + /** + * 扩展方法:返回实际的分割方案 + * + * 时间复杂度分析: + * - 计算总和:O(n) + * - 更新DP数组:O(n * sum) + * - 重构解:O(n) + * - 总时间复杂度:O(n * sum) + * + * 空间复杂度分析: + * - DP数组存储空间:O(sum) + * - 路径记录数组:O(n * sum) + * - 子集存储空间:O(n) + * + * @param nums 输入的正整数数组 + * @return 如果可以分割则返回两个子集,否则返回null + */ + public int[][] findPartition(int[] nums) { + int totalSum = 0; + for (int num : nums) { + totalSum += num; + } + + if (totalSum % 2 != 0) { + return null; + } + + int target = totalSum / 2; + boolean[] dp = new boolean[target + 1]; + dp[0] = true; + + // 记录路径 + boolean[][] path = new boolean[nums.length][target + 1]; + + for (int i = 0; i < nums.length; i++) { + for (int j = target; j >= nums[i]; j--) { + if (dp[j - nums[i]]) { + dp[j] = true; + path[i][j] = true; + } + } + } + + if (!dp[target]) { + return null; + } + + // 重构解 + int[][] result = new int[2][]; + java.util.List subset1 = new java.util.ArrayList<>(); + int currentSum = target; + + for (int i = nums.length - 1; i >= 0; i--) { + if (path[i][currentSum]) { + subset1.add(nums[i]); + currentSum -= nums[i]; + } + } + + // 构建另一个子集 + java.util.List subset2 = new java.util.ArrayList<>(); + boolean[] used = new boolean[nums.length]; + for (int i = 0; i < nums.length; i++) { + if (subset1.contains(nums[i])) { + used[i] = true; + subset1.remove(Integer.valueOf(nums[i])); + } + } + + for (int i = 0; i < nums.length; i++) { + if (!used[i]) { + subset2.add(nums[i]); + } + } + + result[0] = subset1.stream().mapToInt(i -> i).toArray(); + result[1] = subset2.stream().mapToInt(i -> i).toArray(); + + return result; + } +} diff --git a/algorithm/CanSkip55.java b/algorithm/CanSkip55.java new file mode 100644 index 0000000000000..d8c3870fd3b22 --- /dev/null +++ b/algorithm/CanSkip55.java @@ -0,0 +1,213 @@ +package com.funian.algorithm.algorithm; + +/** + * 跳跃游戏(LeetCode 55) + * + * 时间复杂度:O(n) + * - n是数组长度 + * - 只需要遍历数组一次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +import java.util.Scanner; +import java.util.Arrays; + +public class CanSkip55 { + + /** + * 主函数:处理用户输入并判断是否能跳跃到最后一个下标 + * + * 算法流程: + * 1. 读取用户输入的跳跃数组 + * 2. 调用 [canJump](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/CanSkip55.java#L97-L127)方法判断是否能到达最后一个下标 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入跳跃数组(以空格分隔):"); + String line = scanner.nextLine(); + String[] strNums = line.split(" "); + int n = strNums.length; + int[] nums = new int[n]; + + // 将输入的字符串转换为整数数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(strNums[i]); + } + + boolean canReach = canJump(nums); + System.out.println("是否能够到达最后一个下标:" + canReach); + } + + /** + * 判断是否能跳跃到最后一个下标 + * + * 算法思路: + * 贪心算法,维护一个变量`maxReachable`表示当前能到达的最远位置 + * 遍历数组,对于每个位置i: + * 1. 如果i > maxReachable,说明无法到达位置i,返回false + * 2. 更新maxReachable = max(maxReachable, i + nums[i]) + * 3. 如果maxReachable >= n-1,说明已经能到达最后一个下标,返回true + * + * 执行过程分析(以nums=[2,3,1,1,4]为例): + * + * 初始状态: + * maxReachable = 0 + * n = 5, 最后下标 = 4 + * + * 遍历过程: + * i=0, nums[0]=2: + * i=0 <= maxReachable=0,可以到达 + * maxReachable = max(0, 0+2) = 2 + * maxReachable=2 < 4,继续遍历 + * + * i=1, nums[1]=3: + * i=1 <= maxReachable=2,可以到达 + * maxReachable = max(2, 1+3) = 4 + * maxReachable=4 >= 4,可以到达最后下标,返回true + * + * 执行过程分析(以nums=[3,2,1,0,4]为例): + * + * 初始状态: + * maxReachable = 0 + * n = 5, 最后下标 = 4 + * + * 遍历过程: + * i=0, nums[0]=3: + * i=0 <= maxReachable=0,可以到达 + * maxReachable = max(0, 0+3) = 3 + * maxReachable=3 < 4,继续遍历 + * + * i=1, nums[1]=2: + * i=1 <= maxReachable=3,可以到达 + * maxReachable = max(3, 1+2) = 3 + * maxReachable=3 < 4,继续遍历 + * + * i=2, nums[2]=1: + * i=2 <= maxReachable=3,可以到达 + * maxReachable = max(3, 2+1) = 3 + * maxReachable=3 < 4,继续遍历 + * + * i=3, nums[3]=0: + * i=3 <= maxReachable=3,可以到达 + * maxReachable = max(3, 3+0) = 3 + * maxReachable=3 < 4,继续遍历 + * + * i=4, nums[4]=4: + * i=4 > maxReachable=3,无法到达位置4,返回false + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 跳跃数组,nums[i]表示在下标i处可以跳跃的最大长度 + * @return 如果能到达最后一个下标返回true,否则返回false + */ + public static boolean canJump(int[] nums) { + // 最远可达位置,初始为0(起始位置) + int maxReachable = 0; + int n = nums.length; + + // 遍历数组中的每个位置 + for (int i = 0; i < n; i++) { + // 如果当前位置超过最远可达位置,说明无法到达 + // 这是关键的边界条件判断 + if (i > maxReachable) { + return false; + } + + // 更新最远可达位置 + // 从位置i最远可以跳到 i + nums[i] + maxReachable = Math.max(maxReachable, i + nums[i]); + + // 如果已经可以到达最后一个下标,直接返回 true + // 这是一个优化的边界条件,提前结束 + if (maxReachable >= n - 1) { + return true; + } + } + + // 遍历完仍未到达最后一个下标(理论上不会执行到这里,因为循环中会返回) + return false; + } + + + /** + * 方法2:从后往前的贪心算法 + * + * 算法思路: + * 从最后一个位置开始,向前寻找能到达当前目标位置的最近位置 + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 跳跃数组 + * @return 如果能到达最后一个下标返回true,否则返回false + */ + public boolean canJumpReverse(int[] nums) { + if (nums == null || nums.length <= 1) return true; + + int target = nums.length - 1; // 目标位置 + + // 从倒数第二个位置开始向前遍历 + for (int i = nums.length - 2; i >= 0; i--) { + // 如果从位置i能跳到目标位置,则更新目标位置 + if (i + nums[i] >= target) { + target = i; + } + } + + // 如果目标位置能更新到起始位置,说明可以到达 + return target == 0; + } + + /** + * 方法3:动态规划解法 + * + * 算法思路: + * dp[i]表示是否能到达位置i + * + * 时间复杂度分析: + * - 外层循环:O(n) + * - 内层循环:O(nums[i]) + * - 最坏情况总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - dp数组:O(n) + * + * @param nums 跳跃数组 + * @return 如果能到达最后一个下标返回true,否则返回false + */ + public boolean canJumpDP(int[] nums) { + if (nums == null || nums.length <= 1) return true; + + boolean[] dp = new boolean[nums.length]; + dp[0] = true; // 起始位置总是可达的 + + for (int i = 0; i < nums.length; i++) { + if (!dp[i]) continue; // 如果位置i不可达,跳过 + + // 从位置i可以跳到的所有位置都标记为可达 + for (int j = 1; j <= nums[i] && i + j < nums.length; j++) { + dp[i + j] = true; + } + + // 如果最后一个位置可达,提前返回 + if (dp[nums.length - 1]) { + return true; + } + } + + return dp[nums.length - 1]; + } +} diff --git a/algorithm/ClimbStairs70.java b/algorithm/ClimbStairs70.java new file mode 100644 index 0000000000000..81e6f60e55872 --- /dev/null +++ b/algorithm/ClimbStairs70.java @@ -0,0 +1,181 @@ +package com.funian.algorithm.algorithm; + +/** + * 爬楼梯(LeetCode 70)- 动态规划/斐波那契数列 + * + * 时间复杂度:O(n) + * - 需要计算从第3阶到第n阶 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +import java.util.HashMap; +import java.util.Scanner; +import java.util.Arrays; + +public class ClimbStairs70 { + + /** + * 主函数:处理用户输入并计算爬楼梯的方法数 + * + * 算法流程: + * 1. 读取用户输入的楼梯阶数 + * 2. 调用 [climbStairs](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/ClimbStairs70.java#L75-L92)方法计算到达楼顶的方法数 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入楼梯的阶数 + System.out.print("输入楼梯的阶数 n: "); + int n = scanner.nextInt(); + + // 计算到达楼顶的方法数 + int ways = climbStairs(n); + System.out.println("到达楼顶的方法数是: " + ways); + + // 演示其他解法 + ClimbStairs70 solution = new ClimbStairs70(); + int ways2 = solution.climbStairsDP(n); + int ways3 = solution.climbStairsMemo(n); + System.out.println("动态规划版本结果: " + ways2); + System.out.println("记忆化递归版本结果: " + ways3); + } + + /** + * 计算爬楼梯的不同方法数 + * + * 算法思路: + * 这是一个经典的斐波那契数列问题 + * 定义f(n)表示爬到第n阶楼梯的方法数 + * 状态转移方程: + * f(n) = f(n-1) + f(n-2) + * (到达第n阶可以从第n-1阶爬1步,或从第n-2阶爬2步) + * + * 边界条件: + * f(1) = 1(1阶楼梯只有1种方法) + * f(2) = 2(2阶楼梯有2种方法:1+1或2) + * + * 执行过程分析(以n=5为例): + * + * f(1) = 1 + * f(2) = 2 + * f(3) = f(2) + f(1) = 2 + 1 = 3 + * f(4) = f(3) + f(2) = 3 + 2 = 5 + * f(5) = f(4) + f(3) = 5 + 3 = 8 + * + * 详细路径分析(n=3): + * 方法1: 1 + 1 + 1(每次爬1阶) + * 方法2: 1 + 2(先爬1阶,再爬2阶) + * 方法3: 2 + 1(先爬2阶,再爬1阶) + * 总计:3种方法 + * + * 时间复杂度分析: + * - 循环计算:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param n 楼梯的阶数 + * @return 到达楼顶的方法数 + */ + public static int climbStairs(int n) { + // 边界情况:1阶和2阶的特例 + if (n <= 2) return n; + + // 使用两个变量优化空间复杂度 + int first = 1; // 到达第1阶的方法数 + int second = 2; // 到达第2阶的方法数 + int current = 0; + + // 动态规划,从第3阶开始计算 + for (int i = 3; i <= n; i++) { + current = first + second; // 当前阶数的方法数 + first = second; // 更新前一个值 + second = current; // 更新当前值 + } + + return second; // 返回到达第n阶的方法数 + } + + /** + * 方法2:标准动态规划解法 + * + * 算法思路: + * 使用动态规划数组存储每阶楼梯的方法数 + * + * 时间复杂度分析: + * - 初始化DP数组:O(1) + * - 填充DP数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * + * @param n 楼梯的阶数 + * @return 到达楼顶的方法数 + */ + public int climbStairsDP(int n) { + if (n <= 2) return n; + + // 创建DP数组 + int[] dp = new int[n + 1]; + + // 初始化边界条件 + dp[1] = 1; + dp[2] = 2; + + // 填充DP数组 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + + return dp[n]; + } + + + /** + * 方法3:记忆化递归解法 + * + * 算法思路: + * 使用递归方式计算方法数,并通过记忆化数组避免重复计算 + * + * 时间复杂度分析: + * - 每个子问题只计算一次:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(n) + * - 记忆化数组存储空间:O(n) + * - 总空间复杂度:O(n) + * + * @param n 楼梯的阶数 + * @return 到达楼顶的方法数 + */ + public int climbStairsMemo(int n) { + int[] memo = new int[n + 1]; + return climbStairsHelper(n, memo); + } + + /** + * 记忆化递归辅助方法 + * + * 算法思路: + * 递归计算爬楼梯的方法数,并使用记忆化数组缓存已计算的结果 + * + * @param n 楼梯的阶数 + * @param memo 记忆化数组 + * @return 到达楼顶的方法数 + */ + private int climbStairsHelper(int n, int[] memo) { + if (n <= 2) return n; + + if (memo[n] != 0) { + return memo[n]; + } + + memo[n] = climbStairsHelper(n - 1, memo) + climbStairsHelper(n - 2, memo); + return memo[n]; + } +} diff --git a/algorithm/CoinChange322.java b/algorithm/CoinChange322.java new file mode 100644 index 0000000000000..c5f4c80f163d8 --- /dev/null +++ b/algorithm/CoinChange322.java @@ -0,0 +1,253 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 零钱兑换(LeetCode 322)- 动态规划 + * + * 时间复杂度:O(amount * coins.length) + * - 外层循环运行amount次 + * - 内层循环运行coins.length次 + * + * 空间复杂度:O(amount) + * - 需要长度为amount+1的DP数组 + */ +public class CoinChange322 { + + /** + * 计算凑成总金额所需的最少硬币个数 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示凑成金额i所需的最少硬币个数 + * 状态转移方程: + * `dp[i] = min(dp[i - coin] + 1)` for all coin in coins where coin <= i + * + * 执行过程分析(以`coins=[1,3,4]`, `amount=6`为例): + * + * 初始化DP数组(amount=6): + * dp = [0, MAX, MAX, MAX, MAX, MAX, MAX] + * + * 填充DP数组: + * dp[1] = min(dp[0] + 1) = 1 (使用硬币1) + * dp[2] = min(dp[1] + 1) = 2 (使用两个硬币1) + * dp[3] = min(dp[2] + 1, dp[0] + 1) = 1 (使用硬币3) + * dp[4] = min(dp[3] + 1, dp[1] + 1, dp[0] + 1) = 1 (使用硬币4) + * dp[5] = min(dp[4] + 1, dp[2] + 1, dp[1] + 1) = 2 (使用硬币4+1) + * dp[6] = min(dp[5] + 1, dp[3] + 1, dp[2] + 1) = 2 (使用硬币3+3) + * + * 最终结果:dp[6] = 2(使用两个硬币3) + * + * 时间复杂度分析: + * - 初始化DP数组:O(amount) + * - 填充DP数组:O(amount * coins.length) + * - 总时间复杂度:O(amount * coins.length) + * + * 空间复杂度分析: + * - DP数组存储空间:O(amount) + * + * @param coins 不同面额的硬币数组 + * @param amount 总金额 + * @return 凑成总金额所需的最少硬币个数,如果无法凑成返回-1 + */ + public int coinChange(int[] coins, int amount) { + // 边界情况:金额为0时不需要硬币 + if (amount == 0) return 0; + + // dp[i] 表示凑成金额i所需的最少硬币个数 + int[] dp = new int[amount + 1]; + + // 初始化DP数组为一个大值(表示无法凑成) + Arrays.fill(dp, amount + 1); + dp[0] = 0; // 凑成金额0需要0个硬币 + + // 动态规划填充DP数组 + for (int i = 1; i <= amount; i++) { + // 尝试使用每种硬币 + for (int coin : coins) { + if (coin <= i) { + // 状态转移方程: + // 凑成金额i的最少硬币数 = min(凑成金额(i-coin)的最少硬币数 + 1) + dp[i] = Math.min(dp[i], dp[i - coin] + 1); + } + } + } + + // 如果dp[amount]仍为初始值,说明无法凑成 + return dp[amount] > amount ? -1 : dp[amount]; + } + + + /** + * 方法2:BFS解法 + * + * 算法思路: + * 将问题看作图论问题,每个金额是一个节点 + * 如果两个金额相差一个硬币面额,则它们之间有边 + * 使用BFS找到从0到amount的最短路径 + * + * 时间复杂度分析: + * - BFS遍历:O(amount * coins.length) + * - 队列操作:O(1) + * - 总时间复杂度:O(amount * coins.length) + * + * 空间复杂度分析: + * - 队列存储空间:O(amount) + * - visited数组存储空间:O(amount) + * + * @param coins 不同面额的硬币数组 + * @param amount 总金额 + * @return 凑成总金额所需的最少硬币个数,如果无法凑成返回-1 + */ + public int coinChangeBFS(int[] coins, int amount) { + if (amount == 0) return 0; + + // 使用BFS寻找最短路径 + java.util.Queue queue = new java.util.LinkedList<>(); + boolean[] visited = new boolean[amount + 1]; + + queue.offer(0); + visited[0] = true; + + int level = 0; + + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + + for (int i = 0; i < size; i++) { + int current = queue.poll(); + + // 尝试添加每种硬币 + for (int coin : coins) { + int next = current + coin; + + if (next == amount) { + return level; + } + + if (next < amount && !visited[next]) { + queue.offer(next); + visited[next] = true; + } + } + } + } + + return -1; + } + + /** + * 方法3:记忆化递归解法 + * + * 算法思路: + * 使用递归方式计算凑成指定金额所需的最少硬币数, + * 并通过记忆化数组避免重复计算相同子问题 + * + * 时间复杂度分析: + * - 每个子问题只计算一次:O(amount) + * - 每个子问题需要尝试所有硬币:O(coins.length) + * - 总时间复杂度:O(amount * coins.length) + * + * 空间复杂度分析: + * - 递归调用栈:O(amount) + * - 记忆化数组存储空间:O(amount) + * + * @param coins 不同面额的硬币数组 + * @param amount 总金额 + * @return 凑成总金额所需的最少硬币个数,如果无法凑成返回-1 + */ + public int coinChangeMemo(int[] coins, int amount) { + if (amount == 0) return 0; + + int[] memo = new int[amount + 1]; + Arrays.fill(memo, -2); // -2表示未计算,-1表示无法凑成 + + return coinChangeHelper(coins, amount, memo); + } + + /** + * 记忆化递归辅助方法 + * + * 算法思路: + * 递归地计算凑成指定金额所需的最少硬币数 + * + * @param coins 不同面额的硬币数组 + * @param amount 剩余金额 + * @param memo 记忆化数组 + * @return 凑成剩余金额所需的最少硬币个数 + */ + private int coinChangeHelper(int[] coins, int amount, int[] memo) { + if (amount == 0) return 0; + if (amount < 0) return -1; + + if (memo[amount] != -2) { + return memo[amount]; + } + + int minCoins = Integer.MAX_VALUE; + + // 尝试使用每种硬币 + for (int coin : coins) { + int result = coinChangeHelper(coins, amount - coin, memo); + if (result != -1) { + minCoins = Math.min(minCoins, result + 1); + } + } + + memo[amount] = (minCoins == Integer.MAX_VALUE) ? -1 : minCoins; + return memo[amount]; + } + + /** + * 辅助方法:读取用户输入的硬币数组 + * + * 时间复杂度分析: + * - 读取和处理输入:O(n),n为硬币数量 + * + * 空间复杂度分析: + * - 存储硬币数组:O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readCoins() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入硬币面额(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] coins = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + coins[i] = Integer.parseInt(strArray[i]); + } + + return coins; + } + + /** + * 主函数:处理用户输入并计算最少硬币个数 + */ + public static void main(String[] args) { + System.out.println("零钱兑换问题"); + + // 读取用户输入的硬币数组 + int[] coins = readCoins(); + System.out.println("硬币面额: " + Arrays.toString(coins)); + + // 读取总金额 + Scanner scanner = new Scanner(System.in); + System.out.print("请输入总金额: "); + int amount = scanner.nextInt(); + + // 计算最少硬币个数 + CoinChange322 solution = new CoinChange322(); + int result1 = solution.coinChange(coins, amount); + int result2 = solution.coinChangeBFS(coins, amount); + int result3 = solution.coinChangeMemo(coins, amount); + + // 输出结果 + System.out.println("动态规划方法结果: " + result1); + System.out.println("BFS方法结果: " + result2); + System.out.println("记忆化递归方法结果: " + result3); + } +} diff --git a/algorithm/CombinationSum39.java b/algorithm/CombinationSum39.java new file mode 100644 index 0000000000000..dda4069c16c23 --- /dev/null +++ b/algorithm/CombinationSum39.java @@ -0,0 +1,210 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.Arrays; + +/** + * 组合总和(LeetCode 39) + * + * 时间复杂度:O(N^(T/M)) + * - N是数组长度,T是目标值,M是数组中最小值 + * - 最坏情况下递归深度为T/M,每层有N个选择 + * + * 空间复杂度:O(T/M) + * - 递归调用栈深度最坏为T/M + * - 需要额外的列表存储当前路径 + */ +public class CombinationSum39 { + + public static void main(String[] args) { + // 初始化输入读取器 + Scanner scanner = new Scanner(System.in); + + // 获取用户输入的候选数字数组 + System.out.print("请输入候选数字(以空格分隔):"); + String line = scanner.nextLine(); + String[] str = line.split(" "); + int[] candidates = new int[str.length]; + for (int i = 0; i < str.length; i++) { + candidates[i] = Integer.parseInt(str[i]); + } + + // 获取用户输入的目标值 + System.out.print("请输入目标值 target:"); + int target = scanner.nextInt(); + + // 调用核心算法计算所有可能的组合 + List> result = combinationSum(candidates, target); + + // 输出所有找到的组合结果 + System.out.println("所有可能的组合:"); + for (List combination : result) { + System.out.println(combination); + } + } + + /** + * 获取可以使和为 target 的所有组合 + * + * 算法思路: + * 使用回溯算法,通过递归和状态重置生成所有可能的组合 + * 1. 数组中的数字可以无限制重复使用 + * 2. 递归时从当前位置开始(允许重复选择),避免重复组合 + * 3. 当剩余目标值为0时找到一个有效组合 + * 4. 当剩余目标值小于0时剪枝 + * + * 执行过程分析(以`candidates=[2,3,6,7]`, `target=7`为例): + * + * 递归树: + * [] + * / / \ \ + * [2] [3] [6] [7] + * / | | | + * [2,2] [2,3] [3,3] [6] (剪枝) + * / | | + * [2,2,2] [2,3] [3,3] (剪枝) + * | | + * [2,2,2] [2,3,2] (剪枝) + * X X + * (remain<0) (remain<0) + * + * 详细执行过程: + * + * 1. `start=0`, `remain=7`: 尝试`candidates[0]=2` + * - choose(2): `tempList=[2]`, `remain=5`, `start=0`(允许重复) + * - choose(2): `tempList=[2,2]`, `remain=3`, `start=0` + * - choose(2): `tempList=[2,2,2]`, `remain=1`, `start=0` + * - choose(2): `tempList=[2,2,2,2]`, `remain=-1`, `start=0` (剪枝) + * - choose(3): `tempList=[2,2,2,3]`, `remain=-2`, `start=1` (剪枝) + * - ... + * - choose(3): `tempList=[2,2,3]`, `remain=0`, `start=1` + * - `remain=0`,找到解[2,2,3] + * - choose(3): `tempList=[2,3]`, `remain=2`, `start=1` + * - choose(3): `tempList=[2,3,3]`, `remain=-1`, `start=1` (剪枝) + * - ... + * - choose(3): `tempList=[3]`, `remain=4`, `start=1` + * - choose(3): `tempList=[3,3]`, `remain=1`, `start=1` + * - choose(3): `tempList=[3,3,3]`, `remain=-2`, `start=1` (剪枝) + * - ... + * - choose(6): `tempList=[6]`, `remain=1`, `start=2` + * - choose(6): `tempList=[6,6]`, `remain=-5`, `start=2` (剪枝) + * - ... + * - choose(7): `tempList=[7]`, `remain=0`, `start=3` + * - `remain=0`,找到解[7] + * + * 最终结果:[[2,2,3], [7]] + * + * @param candidates 无重复元素的整数数组 + * @param target 目标值 + * @return 所有和为目标值的组合 + */ + public static List> combinationSum(int[] candidates, int target) { + // 初始化结果集,用于存储所有满足条件的组合 + List> result = new ArrayList<>(); + // 启动回溯算法寻找所有可能组合 + backtrack(result, new ArrayList<>(), candidates, target, 0); + return result; + } + + /** + * 递归回溯方法 + * + * 算法思路: + * 递归地尝试所有可能的数字组合,通过回溯生成所有有效组合 + * + * @param result 存储所有有效组合的结果列表 + * @param tempList 当前构建的组合 + * @param candidates 候选数组 + * @param remain 剩余目标值 + * @param start 当前处理的起始位置 + */ + private static void backtrack(List> result, List tempList, + int[] candidates, int remain, int start) { + // 基础情况1:找到一个有效组合 + if (remain == 0) { + // 将当前路径添加到结果集中,使用新列表避免引用问题 + result.add(new ArrayList<>(tempList)); + return; + } + // 基础情况2:当前路径和已经超过目标值,剪枝 + if (remain < 0) { + return; + } + // 遍历候选数组,尝试添加每个数字到组合中 + for (int i = start; i < candidates.length; i++) { + // 选择:将当前候选数字添加到临时路径中 + tempList.add(candidates[i]); + // 递归:继续寻找组合,允许重复使用当前数字(i),更新剩余目标值 + backtrack(result, tempList, candidates, remain - candidates[i], i); + // 回溯:移除刚才添加的数字,尝试其他可能 + tempList.remove(tempList.size() - 1); + } + } + + + /** + * 方法2:优化版(排序后剪枝) + * + * 算法思路: + * 先对候选数组排序,然后在回溯过程中进行剪枝优化 + * + * @param candidates 无重复元素的整数数组 + * @param target 目标值 + * @return 所有和为目标值的组合 + */ + public List> combinationSumOptimized(int[] candidates, int target) { + // 初始化结果集和当前路径 + List> result = new ArrayList<>(); + List path = new ArrayList<>(); + + // 对候选数组进行排序,为剪枝优化做准备 + Arrays.sort(candidates); + + // 启动优化版回溯算法 + backtrackOptimized(candidates, target, 0, path, result); + + return result; + } + + /** + * 优化版回溯辅助方法 + * + * 算法思路: + * 在排序后的数组上进行回溯,利用有序性进行剪枝优化 + * + * @param candidates 排序后的候选数组 + * @param target 剩余目标值 + * @param start 当前处理的起始位置 + * @param path 当前构建的组合 + * @param result 存储所有有效组合的结果列表 + */ + private void backtrackOptimized(int[] candidates, int target, int start, + List path, List> result) { + // 基础情况:找到一个有效组合 + if (target == 0) { + // 将当前路径添加到结果集中 + result.add(new ArrayList<>(path)); + return; + } + + // 遍历候选数组,尝试添加每个数字到组合中 + for (int i = start; i < candidates.length; i++) { + // 优化剪枝:如果当前数字已经大于剩余目标值, + // 由于数组已排序,后续数字只会更大,可以直接跳出循环 + if (candidates[i] > target) { + break; + } + + // 选择:将当前候选数字添加到路径中 + path.add(candidates[i]); + + // 递归:继续寻找组合,允许重复使用当前数字(i),更新剩余目标值 + backtrackOptimized(candidates, target - candidates[i], i, path, result); + + // 回溯:移除刚才添加的数字,尝试其他可能 + path.remove(path.size() - 1); + } + } +} diff --git a/algorithm/CommonAncester236.java b/algorithm/CommonAncester236.java new file mode 100644 index 0000000000000..911dee7418cc0 --- /dev/null +++ b/algorithm/CommonAncester236.java @@ -0,0 +1,183 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.LinkedList; +import java.util.Queue; + +/** + * 二叉树的最近公共祖先(LeetCode 236) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 最坏情况下需要遍历所有节点 + * + * 空间复杂度:O(h) + * - h是二叉树的高度 + * - 递归调用栈的深度 + */ +class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int x) { val = x; } +} + +public class CommonAncester236 { + + public static void main(String[] args) { + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 输入二叉树节点和p, q的值 + // 读取二叉树节点数据 + String nodeList = scanner.nextLine(); + // 读取节点p的值 + int pVal = scanner.nextInt(); + // 读取节点q的值 + int qVal = scanner.nextInt(); + + // 构造二叉树 + TreeNode root = buildTree(nodeList); + TreeNode p = findNode(root, pVal); + TreeNode q = findNode(root, qVal); + + // 查找最近公共祖先 + TreeNode lca = lowestCommonAncestor(root, p, q); + System.out.println((lca != null ? lca.val : "null")); + } + + /** + * 解析输入的字符串并构建二叉树 + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + * + * 时间复杂度分析: + * - 遍历所有节点一次:O(n),其中n为节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 存储节点值数组:O(n) + * - 总空间复杂度:O(n) + * + * @param data 二叉树节点数据字符串 + * @return 构建完成的二叉树根节点 + */ + private static TreeNode buildTree(String data) { + if (data == null || data.isEmpty()) { + return null; + } + + // 去掉输入的括号和空格,并分割成数组 + String[] values = data.split(" "); + if (values[0].equals("null")) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0].trim())); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + queue.add(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + while (i < values.length) { + // 从队列中取出一个节点 + TreeNode current = queue.poll(); + + // 构造左子树 + if (!values[i].trim().equals("null")) { + current.left = new TreeNode(Integer.parseInt(values[i].trim())); + queue.add(current.left); + } + i++; + + // 构造右子树 + if (i < values.length && !values[i].trim().equals("null")) { + current.right = new TreeNode(Integer.parseInt(values[i].trim())); + queue.add(current.right); + } + i++; + } + return root; + } + + /** + * 在二叉树中查找指定值的节点 + * + * 算法思路: + * 使用递归方式在二叉树中查找指定值的节点 + * + * 时间复杂度分析: + * - 最坏情况下需要遍历所有节点:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param root 二叉树根节点 + * @param val 目标节点值 + * @return 找到的节点,未找到返回null + */ + private static TreeNode findNode(TreeNode root, int val) { + if (root == null) return null; + if (root.val == val) return root; + + TreeNode left = findNode(root.left, val); + if (left != null) return left; + + return findNode(root.right, val); + } + + /** + * 查找二叉树中两个节点的最近公共祖先 + * + * 算法思路: + * 使用递归方式查找最近公共祖先 + * 对于每个节点,如果它是p或q,则返回该节点 + * 如果左子树和右子树分别包含p和q,则当前节点是最近公共祖先 + * 如果只有左子树或右子树包含p或q,则返回包含的那个子树的结果 + * + * 执行过程分析(以二叉树 [3,5,1,6,2,0,8,null,null,7,4],p=5, q=1 为例): + * + * 3 + * / \ + * 5 1 + * / \ / \ + * 6 2 0 8 + * / \ + * 7 4 + * + * 查找过程: + * 1. 从根节点3开始,p=5在左子树,q=1在右子树 + * 2. 左子树返回5,右子树返回1 + * 3. 因为左右子树都返回非空结果,所以3是最近公共祖先 + * + * 时间复杂度分析: + * - 每个节点最多访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param root 二叉树根节点 + * @param p 目标节点p + * @param q 目标节点q + * @return 最近公共祖先节点 + */ + public static 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); + + // 如果左右子树都找到节点 + if (left != null && right != null) return root; + + // 返回非空的结果 + return left != null ? left : right; + } +} diff --git a/algorithm/CopyRandomList138.java b/algorithm/CopyRandomList138.java new file mode 100644 index 0000000000000..3c3ad3cf8a2db --- /dev/null +++ b/algorithm/CopyRandomList138.java @@ -0,0 +1,244 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashMap; +import java.util.Map; + +/** + * 复制带随机指针的链表(LeetCode 138) + * + * 时间复杂度:O(n) + * - 方法1(哈希表):需要遍历链表两次,O(2n) = O(n) + * - 方法2(原地复制):需要遍历链表三次,O(3n) = O(n) + * + * 空间复杂度: + * - 方法1(哈希表):O(n) + * 需要哈希表存储所有节点的映射关系 + * - 方法2(原地复制):O(1) + * 只使用常数个额外变量,不使用额外的数据结构 + */ +public class CopyRandomList138 { + + /** + * 节点定义:带随机指针的链表节点 + */ + static class Node { + int val; + Node next; + Node random; + + public Node(int val) { + this.val = val; + this.next = null; + this.random = null; + } + } + + /** + * 方法1:使用哈希表复制链表 + * + * 算法思路: + * 1. 第一次遍历:创建所有节点的副本,并建立原节点到新节点的映射 + * 2. 第二次遍历:根据映射关系设置新节点的next和random指针 + * + * 执行过程分析(以链表 A(1)->B(2)->C(3),其中A.random=C, B.random=A 为例): + * + * 第一次遍历:创建节点副本 + * 原链表:A(1)->B(2)->C(3) + * 哈希表:{A->A', B->B', C->C'} + * + * 第二次遍历:设置指针关系 + * A'.next = 哈希表[A.next] = 哈希表[B] = B' + * A'.random = 哈希表[A.random] = 哈希表[C] = C' + * B'.next = 哈希表[B.next] = 哈希表[C] = C' + * B'.random = 哈希表[B.random] = 哈希表[A] = A' + * C'.next = 哈希表[C.next] = 哈希表[null] = null + * C'.random = 哈希表[C.random] = 哈希表[null] = null + * + * 结果链表:A'(1)->B'(2)->C'(3),其中A'.random=C', B'.random=A' + * + * 时间复杂度分析: + * - 第一次遍历创建节点:O(n),其中n为链表长度 + * - 第二次遍历设置指针:O(n) + * - 哈希表操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 哈希表存储映射关系:O(n) + * - 新链表节点:O(n) + * - 总空间复杂度:O(n) + * + * @param head 原链表的头节点 + * @return 复制后的链表头节点 + */ + public Node copyRandomListWithHashMap(Node head) { + if (head == null) return null; + + // 创建哈希表存储原节点到新节点的映射 + Map map = new HashMap<>(); + + // 第一次遍历:创建所有节点的副本 + Node current = head; + while (current != null) { + map.put(current, new Node(current.val)); + current = current.next; + } + + // 第二次遍历:设置新节点的next和random指针 + current = head; + while (current != null) { + // 设置next指针 + map.get(current).next = map.get(current.next); + + // 设置random指针 + map.get(current).random = map.get(current.random); + + current = current.next; + } + + // 返回复制链表的头节点 + return map.get(head); + } + + /** + * 方法2:原地复制法(三步法) + * + * 算法思路: + * 1. 第一遍遍历:将每个节点的副本插入到原节点后面 + * 2. 第二遍遍历:设置副本节点的random指针 + * 3. 第三遍遍历:将副本节点从原链表中分离出来 + * + * 执行过程分析(以链表 A(1)->B(2)->C(3),其中A.random=C, B.random=A 为例): + * + * 初始状态: + * A(1) -> B(2) -> C(3) -> null + * | | | + * random | random + * --------+-------- + * | + * random + * + * 第一遍遍历后(复制节点并插入): + * A(1) -> A'(1) -> B(2) -> B'(2) -> C(3) -> C'(3) -> null + * + * 第二遍遍历后(设置random指针): + * 对于A':A'.random = A.random.next = C' (因为C'紧跟在C后面) + * 对于B':B'.random = B.random.next = A' (因为A'紧跟在A后面) + * 对于C':C'.random = C.random.next = null + * + * 第三遍遍历后(分离链表): + * 原链表:A(1) -> B(2) -> C(3) -> null + * 新链表:A'(1) -> B'(2) -> C'(3) -> null + * 其中:A'.random = C', B'.random = A' + * + * 时间复杂度分析: + * - 第一遍遍历复制节点:O(n) + * - 第二遍遍历设置random指针:O(n) + * - 第三遍遍历分离链表:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * - 新链表节点:O(n) + * - 总空间复杂度:O(1)(不考虑输出空间) + * + * @param head 原链表的头节点 + * @return 复制后的链表头节点 + */ + public Node copyRandomList(Node head) { + if (head == null) return null; + + // 第一遍遍历:复制每个节点并插入到原节点后面 + Node current = head; + while (current != null) { + Node newNode = new Node(current.val); + newNode.next = current.next; + current.next = newNode; + current = newNode.next; + } + + // 第二遍遍历:设置新节点的 random 指针 + current = head; + while (current != null) { + // 如果原节点的random不为空,则新节点的random指向原节点random的下一个节点 + // (因为原节点random的下一个节点就是原节点random的副本) + if (current.random != null) { + current.next.random = current.random.next; + } + current = current.next.next; + } + + // 第三遍遍历:将新节点从原节点中分离出来 + Node newHead = head.next; + current = head; + while (current != null) { + Node newNode = current.next; + + // 恢复原链表的next指针 + current.next = newNode.next; + + // 设置新链表的next指针 + if (newNode.next != null) { + newNode.next = newNode.next.next; + } + + current = current.next; + } + + // 返回新链表的头节点 + return newHead; + } + + /** + * 辅助方法:创建带随机指针的链表(用于测试) + */ + public Node createList(int[] values, int[] randomIndices) { + if (values.length == 0) return null; + + // 创建所有节点 + Node[] nodes = new Node[values.length]; + for (int i = 0; i < values.length; i++) { + nodes[i] = new Node(values[i]); + } + + // 设置next指针 + for (int i = 0; i < values.length - 1; i++) { + nodes[i].next = nodes[i + 1]; + } + + // 设置random指针 + for (int i = 0; i < values.length; i++) { + if (randomIndices[i] != -1) { + nodes[i].random = nodes[randomIndices[i]]; + } + } + + // 返回链表头节点 + return nodes[0]; + } + + /** + * 辅助方法:打印链表(用于测试) + */ + public void printList(Node head) { + // 先建立节点到索引的映射 + Map nodeToIndex = new HashMap<>(); + Node current = head; + int index = 0; + while (current != null) { + nodeToIndex.put(current, index++); + current = current.next; + } + + // 打印节点信息 + current = head; + index = 0; + while (current != null) { + // randomIndex random指针指向节点的索引 + int randomIndex = (current.random != null) ? nodeToIndex.get(current.random) : -1; + System.out.println("Node " + index + ": val=" + current.val + + ", random->Node " + (randomIndex == -1 ? "null" : randomIndex)); + current = current.next; + index++; + } + } +} diff --git a/algorithm/DailyTemperature739.java b/algorithm/DailyTemperature739.java new file mode 100644 index 0000000000000..fa24b4ed55796 --- /dev/null +++ b/algorithm/DailyTemperature739.java @@ -0,0 +1,157 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Stack; +import java.util.Arrays; + +/** + * 每日温度(LeetCode 739)- 单调栈 + * + * 时间复杂度:O(n) + * - 每个元素最多入栈和出栈一次 + * + * 空间复杂度:O(n) + * - 栈最多存储n个元素 + * - 结果数组需要n个空间 + */ +public class DailyTemperature739 { + + /** + * 主函数:处理用户输入并计算每日温度问题 + * + * 算法流程: + * 1. 读取用户输入的温度数组 + * 2. 调用dailyTemperatures方法计算结果 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入每天的温度(以空格分隔):"); + + String line = scanner.nextLine(); + String[] strTemps = line.split(" "); + int[] temperatures = new int[strTemps.length]; + + for (int i = 0; i < strTemps.length; i++) { + temperatures[i] = Integer.parseInt(strTemps[i]); + } + + int[] answer = dailyTemperatures(temperatures); + System.out.println("下一个更高温度出现在几天后:" + Arrays.toString(answer)); + } + + /** + * 计算每天需要等待多少天才能遇到更高的温度 + * + * 算法思路: + * 使用单调递减栈存储温度的索引 + * 1. 遍历温度数组 + * 2. 当当前温度大于栈顶索引对应的温度时,说明找到了更高温度 + * 3. 弹出栈顶元素并计算天数差 + * 4. 将当前索引入栈 + * + * @param temperatures 温度数组 + * @return 等待更高温度的天数数组 + */ + public static int[] dailyTemperatures(int[] temperatures) { + int n = temperatures.length; + int[] answer = new int[n]; + Stack 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; + } + + /** + * 方法2:从右到左的解法 + * + * 算法思路: + * 从右到左遍历,利用已计算的结果跳过不必要的比较 + * + * @param temperatures 温度数组 + * @return 等待更高温度的天数数组 + */ + public int[] dailyTemperaturesReverse(int[] temperatures) { + int n = temperatures.length; + int[] answer = new int[n]; + + for (int i = n - 2; i >= 0; i--) { + int j = i + 1; + + while (j < n && temperatures[j] <= temperatures[i]) { + if (answer[j] > 0) { + j += answer[j]; + } else { + j = n; + } + } + + if (j < n) { + answer[i] = j - i; + } + } + + return answer; + } + + /** + * 方法3:暴力解法(仅供对比) + * + * 算法思路: + * 对于每个温度,向后遍历寻找第一个更高温度 + * + * 时间复杂度:O(n²) + * 空间复杂度:O(1) + * + * @param temperatures 温度数组 + * @return 等待更高温度的天数数组 + */ + public int[] dailyTemperaturesBruteForce(int[] temperatures) { + int n = temperatures.length; + int[] answer = new int[n]; + + for (int i = 0; i < n; i++) { + for (int j = i + 1; j < n; j++) { + if (temperatures[j] > temperatures[i]) { + answer[i] = j - i; + break; + } + } + } + + return answer; + } + + /** + * 方法4:使用数组代替栈(优化空间) + * + * 算法思路: + * 使用数组模拟栈操作,避免使用Stack类的开销 + * + * @param temperatures 温度数组 + * @return 等待更高温度的天数数组 + */ + public int[] dailyTemperaturesArrayStack(int[] temperatures) { + int n = temperatures.length; + int[] answer = new int[n]; + int[] stack = new int[n]; + int top = -1; + + for (int i = 0; i < n; i++) { + while (top >= 0 && temperatures[i] > temperatures[stack[top]]) { + int idx = stack[top--]; + answer[idx] = i - idx; + } + stack[++top] = i; + } + + return answer; + } +} diff --git a/algorithm/DecodeString394.java b/algorithm/DecodeString394.java new file mode 100644 index 0000000000000..f1f0f3763848e --- /dev/null +++ b/algorithm/DecodeString394.java @@ -0,0 +1,224 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Stack; + +/** + * 字符串解码(LeetCode 394) + * + * 时间复杂度:O(n) + * - n是解码后字符串的长度 + * - 每个字符最多被处理常数次 + * + * 空间复杂度:O(n) + * - 栈空间和StringBuilder空间 + * - 最坏情况下存储解码后的整个字符串 + */ +public class DecodeString394 { + + /** + * 主函数:处理用户输入并解码字符串 + * + * 算法流程: + * 1. 读取用户输入的编码字符串 + * 2. 调用decodeString方法进行解码 + * 3. 输出解码结果 + */ + public static void main(String[] args) { + // 创建Scanner对象用于读取输入 + Scanner scanner = new Scanner(System.in); + // 打印输入提示 + System.out.println("请输入经过编码的字符串(如: 3[a2[bc]]):"); + // 读取用户输入的编码字符串 + String s = scanner.nextLine(); + + // 调用decodeString方法进行解码 + String decodedString = decodeString(s); + // 打印解码结果 + System.out.println("解码后的字符串: " + decodedString); + } + + /** + * 解码字符串 + * + * 算法思路: + * 使用两个栈分别存储重复次数和字符串 + * 1. 遇到数字时,计算完整的重复次数 + * 2. 遇到'['时,将当前重复次数和字符串入栈,并重置 + * 3. 遇到']'时,弹出栈顶元素进行解码操作 + * 4. 遇到普通字符时,直接添加到当前字符串 + * + * @param s 输入的编码字符串 + * @return 解码后的字符串 + */ + public static String decodeString(String s) { + // 使用栈来处理字符和重复次数 + + // 存储重复次数的栈 + Stack countStack = new Stack<>(); + + // 存储字符串的栈 + Stack stringStack = new Stack<>(); + + // 当前正在构建的字符串 + StringBuilder currentString = new StringBuilder(); + + // 当前的重复次数 + int k = 0; + + // 遍历字符串中的每一个字符 + for (char ch : s.toCharArray()) { + // 如果是数字字符 + if (Character.isDigit(ch)) { + // 更新重复次数 k + // 处理多位数的情况,如"123[abc]" + k = k * 10 + (ch - '0'); + } + // 如果遇到 '[' + else if (ch == '[') { + // 将当前字符串和重复次数入栈 + countStack.push(k); + stringStack.push(currentString.toString()); + + // 重置 k 和 currentString + // 开始处理新的嵌套层级 + k = 0; + currentString = new StringBuilder(); + } + // 如果遇到 ']' + else if (ch == ']') { + // 弹出栈顶的重复次数和字符串 + StringBuilder decodedString = new StringBuilder(stringStack.pop()); + int repeatTimes = countStack.pop(); + + // 重复当前字符串并拼接 + // 将currentString重复repeatTimes次并添加到decodedString后面 + for (int i = 0; i < repeatTimes; i++) { + decodedString.append(currentString); + } + + // 更新当前字符串为解码后的结果 + currentString = decodedString; + } + // 其他字符(字母) + else { + // 其他字符直接添加到 currentString + currentString.append(ch); + } + } + + // 返回解码后的字符串 + return currentString.toString(); + } + + /** + * 方法2:递归解法 + * + * 算法思路: + * 使用递归处理嵌套结构 + * + * @param s 输入的编码字符串 + * @return 解码后的字符串 + */ + private int index = 0; + + public String decodeStringRecursive(String s) { + // 重置索引 + index = 0; + // 调用递归辅助方法 + return decodeStringHelper(s); + } + + private String decodeStringHelper(String s) { + // 创建结果构建器 + StringBuilder result = new StringBuilder(); + // 初始化重复次数 + int k = 0; + + // 当索引未超出字符串长度时循环 + while (index < s.length()) { + // 获取当前字符 + char ch = s.charAt(index); + // 索引递增 + index++; + + // 判断字符是否为数字 + if (Character.isDigit(ch)) { + // 计算多位数 + k = k * 10 + (ch - '0'); + } else if (ch == '[') { + // 递归处理括号内的内容 + String nested = decodeStringHelper(s); + // 重复k次并添加到结果中 + while (k > 0) { + // 将嵌套内容追加到结果 + result.append(nested); + // 递减重复次数 + k--; + } + } else if (ch == ']') { + // 遇到右括号,返回当前层级的结果 + break; + } else { + // 普通字符直接添加 + result.append(ch); + } + } + + // 返回解码结果 + return result.toString(); + } + + /** + * 方法3:使用单个栈优化版本 + * + * 算法思路: + * 使用单个栈存储数字和字符串,交替存储 + * + * @param s 输入的编码字符串 + * @return 解码后的字符串 + */ + public String decodeStringOptimized(String s) { + // 创建单个栈 + Stack stack = 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 == '[') { + // 将数字和字符串都压入同一个栈 + stack.push(String.valueOf(k)); + stack.push(currentString.toString()); + // 重置重复次数 + k = 0; + // 重置当前字符串 + currentString = new StringBuilder(); + } else if (ch == ']') { + // 弹出字符串和数字 + String prevString = stack.pop(); + int repeatTimes = Integer.parseInt(stack.pop()); + + // 构造重复字符串 + StringBuilder temp = new StringBuilder(prevString); + for (int i = 0; i < repeatTimes; i++) { + temp.append(currentString); + } + // 更新当前字符串 + currentString = temp; + } else { + // 将字符追加到当前字符串 + currentString.append(ch); + } + } + + // 返回解码后的字符串 + return currentString.toString(); + } +} diff --git a/algorithm/DetectCycleList142.java b/algorithm/DetectCycleList142.java new file mode 100644 index 0000000000000..ca5805490891b --- /dev/null +++ b/algorithm/DetectCycleList142.java @@ -0,0 +1,251 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashSet; +import java.util.Set; + +/** + * 环形链表II - 找到环的起始节点(LeetCode 142) + * + * 时间复杂度: + * - 方法1(哈希表):O(n) + * 需要遍历链表,最坏情况下遍历所有节点 + * - 方法2(快慢指针):O(n) + * 第一阶段寻找相遇点:O(n) + * 第二阶段寻找环入口:O(n) + * 总体时间复杂度:O(n) + * + * 空间复杂度: + * - 方法1(哈希表):O(n) + * 需要存储所有访问过的节点 + * - 方法2(快慢指针):O(1) + * 只使用常数个额外变量 + */ +public class DetectCycleList142 { + + // 定义链表节点类 + static class ListNode { + int val; + ListNode next; + ListNode(int x) { + val = x; + next = null; + } + } + + /** + * 方法1:使用哈希表找到环的起始节点 + * + * 算法思路: + * 遍历链表,将访问过的节点存储在哈希表中 + * 如果遇到已经存在于哈希表中的节点,该节点就是环的起始节点 + * 如果遍历到null,说明无环,返回null + * + * 执行过程分析(以链表 1->2->3->4->2 为例,4指向2形成环): + * 1. 访问节点1,哈希表:{1} + * 2. 访问节点2,哈希表:{1,2} + * 3. 访问节点3,哈希表:{1,2,3} + * 4. 访问节点4,哈希表:{1,2,3,4} + * 5. 再次访问节点2,发现已存在于哈希表中,返回节点2 + * + * 时间复杂度分析: + * - 遍历链表:O(n),其中n为链表节点数 + * - HashSet操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashSet存储节点:O(n) + * + * @param head 链表的头节点 + * @return 环的起始节点,如果无环返回null + */ + public ListNode detectCycleWithHashSet(ListNode head) { + // 使用HashSet存储访问过的节点 + Set visitedNodes = new HashSet<>(); + + // 从头节点开始遍历 + ListNode current = head; + while (current != null) { + // 如果当前节点已在集合中,说明找到了环的起始节点 + if (visitedNodes.contains(current)) { + // 返回环的起始节点 + return current; + } + + // 将当前节点加入集合 + visitedNodes.add(current); + + // 移动到下一个节点 + current = current.next; + } + + // 遍历完成未发现环,返回null + return null; + } + + + /** + * 方法2:使用快慢指针找到环的起始节点(Floyd算法扩展) + * + * 算法思路: + * 1. 使用快慢指针判断是否有环,并找到相遇点 + * 2. 将一个指针重新指向头节点,另一个指针保持在相遇点 + * 3. 两个指针同时以相同速度移动,相遇点即为环的起始节点 + * + * 数学原理: + * 假设链表头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c + * 环的长度为 b+c + * + * 当快慢指针相遇时: + * - 慢指针走过的距离:a + b + * - 快指针走过的距离:a + b + c + b = a + 2b + c + * - 快指针走过的距离是慢指针的2倍:2(a + b) = a + 2b + c + * - 化简得:a = c + * + * 因此,从头节点到环入口的距离等于从相遇点到环入口的距离 + * + * 执行过程分析(以链表 1->2->3->4->2 为例,4指向2形成环): + * 第一阶段:寻找相遇点 + * - 初始:slow=1, fast=1 + * - 第1步:slow=2, fast=3 + * - 第2步:slow=3, fast=2 (环中) + * - 第3步:slow=4, fast=4 (相遇) + * + * 第二阶段:寻找环入口 + * - ptr从头节点开始,slow保持在相遇点 + * - ptr=1, slow=4 + * - ptr=2, slow=2 (相遇,返回节点2) + * + * 时间复杂度分析: + * - 第一阶段寻找相遇点:O(n) + * - 第二阶段寻找环入口:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * + * @param head 链表的头节点 + * @return 环的起始节点,如果无环返回null + */ + public ListNode detectCycleWithTwoPointers(ListNode head) { + // 边界情况:空链表或只有一个节点 + if (head == null || head.next == null) { + return null; + } + + // 第一阶段:判断是否有环并找到相遇点 + ListNode slow = head; + ListNode fast = head; + + // 寻找相遇点 + while (fast != null && fast.next != null) { + // 慢指针前进一步 + slow = slow.next; + // 快指针前进两步 + fast = fast.next.next; + + // 如果快慢指针相遇,说明有环 + if (slow == fast) { + break; + } + } + + // 如果没有环,返回null + if (fast == null || fast.next == null) { + return null; + } + + // 第二阶段:找到环的起始节点 + ListNode ptr = head; + while (ptr != slow) { + ptr = ptr.next; + slow = slow.next; + } + + // 返回环的起始节点 + return ptr; + } + + /** + * 辅助方法:打印链表(用于测试,有环时需要限制打印节点数) + */ + public void printList(ListNode head, int maxNodes) { + ListNode current = head; + int count = 0; + while (current != null && count < maxNodes) { + System.out.print(current.val + " "); + current = current.next; + count++; + } + System.out.println(); + } + + /** + * 测试方法和使用示例 + */ + public static void main(String[] args) { + // 创建解决方案实例 + DetectCycleList142 solution = new DetectCycleList142(); + + // 创建无环链表: 1->2->3->null + ListNode head1 = new ListNode(1); + head1.next = new ListNode(2); + head1.next.next = new ListNode(3); + + // 创建有环链表: 1->2->3->4->2 (4指向2) + ListNode head2 = new ListNode(1); + head2.next = new ListNode(2); + head2.next.next = new ListNode(3); + head2.next.next.next = new ListNode(4); + head2.next.next.next.next = head2.next; + + // 测试无环链表 + ListNode result1 = solution.detectCycleWithHashSet(head1); + System.out.println("无环链表检测结果(哈希表): " + (result1 == null ? "无环" : "环起始节点值: " + result1.val)); + + result1 = solution.detectCycleWithTwoPointers(head1); + System.out.println("无环链表检测结果(快慢指针): " + (result1 == null ? "无环" : "环起始节点值: " + result1.val)); + + // 测试有环链表 + ListNode result2 = solution.detectCycleWithHashSet(head2); + System.out.println("有环链表检测结果(哈希表): " + (result2 == null ? "无环" : "环起始节点值: " + result2.val)); + + result2 = solution.detectCycleWithTwoPointers(head2); + System.out.println("有环链表检测结果(快慢指针): " + (result2 == null ? "无环" : "环起始节点值: " + result2.val)); + } + + /** + * 方法3:使用异常处理检测环(投机取巧) + * + * 算法思路: + * 利用链表有环时toString()方法会产生StackOverflowError的特性 + * + * 注意:这种方法不推荐在生产环境中使用 + * + * 执行过程分析: + * 1. 对于无环链表:toString()方法正常执行,返回null + * 2. 对于有环链表:toString()方法递归调用导致栈溢出,捕获异常返回head + * + * 时间复杂度分析: + * - 无环情况:O(n),其中n为链表节点数 + * - 有环情况:O(1),发生栈溢出立即返回 + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * - 有环时递归调用栈:O(n) + * + * @param head 链表的头节点 + * @return 环的起始节点,如果无环返回null + */ + public ListNode detectCycleByToString(ListNode head) { + try { + // 尝试调用toString方法,有环时会抛出StackOverflowError + head.toString(); + return null; // 无环 + } catch (StackOverflowError e) { + // 捕获到栈溢出错误,说明有环 + // 这里只是检测是否有环,但无法确定环的起始节点 + // 实际应用中需要结合其他方法 + return head; // 简化处理,实际应返回环的起始节点 + } + } +} diff --git a/algorithm/DiameterBinaryTree543.java b/algorithm/DiameterBinaryTree543.java new file mode 100644 index 0000000000000..32398b7f27ce8 --- /dev/null +++ b/algorithm/DiameterBinaryTree543.java @@ -0,0 +1,346 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Queue; +import java.util.LinkedList; + +/** + * 二叉树的直径(LeetCode 543) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(h) + * - h是二叉树的高度,递归调用栈的深度 + * - 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + */ +public class DiameterBinaryTree543 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + // 全局变量,用于记录二叉树的最大直径 + private int maxDiameter = 0; + + /** + * 计算二叉树的直径 + * + * 算法思路: + * 二叉树的直径是任意两个节点之间的最长路径长度(边数) + * 这条路径可能经过根节点,也可能不经过根节点 + * 对于每个节点,经过该节点的最长路径等于其左子树的最大深度加上右子树的最大深度 + * 因此,我们可以在计算每个节点最大深度的同时,更新全局最大直径 + * + * 执行过程分析(以二叉树 [1,2,3,4,5] 为例): + * + * 1 + * / \ + * 2 3 + * / \ + * 4 5 + * + * 递归调用过程: + * diameterOfBinaryTree(1) + * ├─ depth(1) + * │ ├─ depth(2) + * │ │ ├─ depth(4) -> 返回1,更新maxDiameter=max(0,0+0)=0 + * │ │ ├─ depth(5) -> 返回1,更新maxDiameter=max(0,0+0)=0 + * │ │ └─ 返回max(1,1)+1=2 + * │ ├─ depth(3) + * │ │ ├─ depth(null) -> 返回0 + * │ │ ├─ depth(null) -> 返回0 + * │ │ └─ 返回max(0,0)+1=1 + * │ ├─ 更新maxDiameter=max(0,2+1)=3(经过节点2的路径4->2->5最长) + * │ └─ 返回max(2,1)+1=3 + * └─ 返回maxDiameter=3 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 二叉树的直径 + */ + public int diameterOfBinaryTree(TreeNode root) { + // 重置全局变量 + maxDiameter = 0; + + // 计算树的深度,在计算过程中更新最大直径 + depth(root); + + // 返回最大直径 + return maxDiameter; + } + + /** + * 辅助方法:计算以当前节点为根的子树的最大深度 + * 同时更新全局最大直径 + * + * 算法思路: + * 1. 递归计算左右子树的深度 + * 2. 更新经过当前节点的最长路径(左子树深度+右子树深度) + * 3. 返回当前子树的最大深度 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @return 以当前节点为根的子树的最大深度 + */ + private int depth(TreeNode node) { + // 基础情况:如果节点为空,深度为0 + if (node == null) { + return 0; + } + + // 递归计算左子树的最大深度 + int leftDepth = depth(node.left); + + // 递归计算右子树的最大深度 + int rightDepth = depth(node.right); + + // 更新最大直径:经过当前节点的路径长度等于左子树深度加右子树深度 + maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth); + + // 返回以当前节点为根的子树的最大深度 + return Math.max(leftDepth, rightDepth) + 1; + } + + /** + * 另一种实现方式:使用自定义类封装返回值 + * + * 算法思路: + * 对于每个节点,我们需要知道两个信息: + * 1. 以该节点为根的子树的最大深度 + * 2. 以该节点为根的子树中的最大直径 + * 可以用一个类来封装这两个信息 + */ + static class Result { + int depth; // 子树的最大深度 + int diameter; // 子树中的最大直径 + + Result(int depth, int diameter) { + this.depth = depth; + this.diameter = diameter; + } + } + + /** + * 使用自定义类的解法 + * + * 算法思路: + * 通过Result类同时返回子树的深度和最大直径 + * 避免使用全局变量 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * - Result对象创建:O(n) + * + * @param root 二叉树的根节点 + * @return 二叉树的直径 + */ + public int diameterOfBinaryTreeAlternative(TreeNode root) { + return diameterHelper(root).diameter; + } + + /** + * 辅助方法:返回以当前节点为根的子树的深度和最大直径 + * + * 算法思路: + * 1. 递归获取左右子树的深度和直径信息 + * 2. 计算当前子树的深度 + * 3. 计算经过当前节点的路径长度 + * 4. 计算当前子树的最大直径 + * 5. 返回封装了深度和直径的Result对象 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @return 包含深度和直径的Result对象 + */ + private Result diameterHelper(TreeNode node) { + // 基础情况:如果节点为空 + if (node == null) { + return new Result(0, 0); + } + + // 递归获取左右子树的信息 + Result leftResult = diameterHelper(node.left); + Result rightResult = diameterHelper(node.right); + + // 计算当前子树的最大深度 + int currentDepth = Math.max(leftResult.depth, rightResult.depth) + 1; + + // 计算经过当前节点的路径长度 + int diameterThroughCurrent = leftResult.depth + rightResult.depth; + + // 计算当前子树中的最大直径 + int currentDiameter = Math.max(diameterThroughCurrent, + Math.max(leftResult.diameter, rightResult.diameter)); + + return new Result(currentDepth, currentDiameter); + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并计算二叉树的直径 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建的二叉树 + * 4. 调用两种方法计算直径 + * 5. 打印计算结果 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("二叉树直径计算"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空,直径为: 0"); + return; + } + + // 打印创建的二叉树 + System.out.println("创建的二叉树:"); + printLevelOrder(root); + + // 计算直径 + // 创建解决方案实例 + DiameterBinaryTree543 solution = new DiameterBinaryTree543(); + int diameter1 = solution.diameterOfBinaryTree(root); + int diameter2 = solution.diameterOfBinaryTreeAlternative(root); + + // 打印结果 + System.out.println("方法1计算的直径: " + diameter1); + System.out.println("方法2计算的直径: " + diameter2); + } +} diff --git a/algorithm/FindAnagrams438.java b/algorithm/FindAnagrams438.java new file mode 100644 index 0000000000000..83eaac28b8a77 --- /dev/null +++ b/algorithm/FindAnagrams438.java @@ -0,0 +1,329 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * 找到字符串中所有字母异位词(LeetCode 438) + * + * 时间复杂度:O(n) + * - n 是字符串 s 的长度 + * - 初始化 pCount 数组需要 O(m) 时间,m 是字符串 p 的长度 + * - 遍历字符串 s 需要 O(n) 时间 + * - 每次比较两个数组需要 O(26) = O(1) 时间 + * - 总时间复杂度:O(m) + O(n) = O(n)(假设 m <= n) + * + * 空间复杂度:O(1) + * - 使用了两个固定大小的数组 pCount 和 sCount,大小均为 26 + * - 结果列表的空间不计入,因为它是输出的一部分 + */ +public class FindAnagrams438 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入主字符串 s + System.out.println("输入字符串:"); + + // 读取主字符串 s + String s = scanner.nextLine(); + + // 提示用户输入模式字符串 p + System.out.println("输入字符串:"); + + // 读取模式字符串 p + String p = scanner.nextLine(); + + // 调用 findAnagrams 方法查找所有字母异位词的起始位置 + List result = findAnagrams(s, p); + + // 输出结果 + System.out.println("结果:" + result); + } + + /** + * 找出字符串 s 中所有与字符串 p 互为字母异位词的子串的起始索引 + * 字母异位词指字母相同,但排列不同的字符串 + * + * 算法思路: + * 1. 使用滑动窗口技术,窗口大小固定为字符串 p 的长度 + * 2. 统计字符串 p 中每个字符的出现次数 + * 3. 使用滑动窗口遍历字符串 s,维护窗口内字符的出现次数 + * 4. 比较窗口内字符统计与 p 的字符统计是否相同 + * 5. 如果相同,则窗口起始位置是一个有效的字母异位词 + * + * 示例过程(以 s="cbaebabacd", p="abc" 为例): + * p的字符统计: a:1, b:1, c:1 + * + * 窗口位置 窗口内容 字符统计 是否匹配 结果 + * [0,2] cba a:1,b:1,c:1 是 [0] + * [1,3] bae a:1,b:1,e:1 否 [0] + * [2,4] aeb a:1,b:1,e:1 否 [0] + * [3,5] eba a:1,b:1,e:1 否 [0] + * [4,6] bab a:1,b:2 否 [0] + * [5,7] aba a:2,b:1 否 [0] + * [6,8] bac a:1,b:1,c:1 是 [0,6] + * [7,9] acd a:1,c:1,d:1 否 [0,6] + * + * 最终结果:[0,6] + * + * 时间复杂度分析: + * - 统计字符串p的字符:O(m),m为p的长度 + * - 滑动窗口遍历字符串s:O(n),n为s的长度 + * - 比较字符统计数组:O(26) = O(1) + * - 总时间复杂度:O(n),其中n为输入字符串`s`的长度 + * + * 空间复杂度分析: + * - 两个字符统计数组:O(26) = O(1) + * - 结果列表:O(k),k为结果数量 + * - 总空间复杂度:O(1) + * + * @param s 主字符串 + * @param p 模式字符串 + * @return 所有字母异位词的起始索引列表 + */ + public static List findAnagrams(String s, String p) { + // 存储结果的列表 + List result = new ArrayList<>(); + + // 边界条件检查:如果 s 为空或长度小于 p,直接返回空列表 + if (s == null || s.length() < p.length()) { + return result; + } + + // 使用数组统计字符出现次数(假设只包含小写字母 a-z) + int[] pCount = new int[26]; + int[] sCount = new int[26]; + + // 获取字符串 p 和 s 的长度 + int pp = p.length(); + int ss = s.length(); + + // 统计字符串 p 中每个字符的出现次数 + for (int i = 0; i < pp; i++) { + pCount[p.charAt(i) - 'a']++; + } + + // 使用滑动窗口遍历字符串 s + for (int i = 0; i < ss; i++) { + // 将当前字符加入窗口统计 + sCount[s.charAt(i) - 'a']++; + + // 如果窗口大小超过 p 的长度,需要移除窗口左侧的字符 + if (i >= pp) { + // 移除窗口左侧的字符(i - pp 位置的字符) + sCount[s.charAt(i - pp) - 'a']--; + } + + // 检查当前窗口是否与字符串 p 构成字母异位词 + if (isEqual(pCount, sCount)) { + // 如果是字母异位词,将起始位置添加到结果列表中 + result.add(i - pp + 1); + } + } + + // 返回所有字母异位词的起始索引 + return result; + } + + + /** + * 判断两个字符统计数组是否相等 + * 比较两个数组中每个字符的出现次数是否完全相同 + * + * 时间复杂度分析: + * - 遍历26个字符:O(26) = O(1) + * - 总时间复杂度:O(1) + * + * 空间复杂度分析: + * - 只使用了循环变量:O(1) + * - 总空间复杂度:O(1) + * + * @param pCount 字符串 p 的字符统计数组 + * @param sCount 滑动窗口的字符统计数组 + * @return 如果两个数组相等返回 true,否则返回 false + */ + public static boolean isEqual(int[] pCount, int[] sCount) { + // 遍历数组比较每个字符的出现次数 + for (int i = 0; i < 26; i++) { + // 如果有任何字符的出现次数不相等,则两个数组不相等 + if (pCount[i] != sCount[i]) { + return false; + } + } + // 所有字符的出现次数都相等,两个数组相等 + return true; + } + + /** + * 方法2:优化版本(使用差异计数) + * + * 算法思路: + * 使用一个变量记录两个数组之间的差异,避免每次都比较整个数组 + * + * 示例过程(以 s="cbaebabacd", p="abc" 为例): + * + * 初始化: + * count数组统计初始窗口与p的差异: + * s[0:2]="cba", p="abc" + * count['c'] = 1-0 = 1, count['b'] = 1-0 = 1, count['a'] = 1-0 = 1 + * count['a'] = 1-1 = 0, count['b'] = 1-1 = 0, count['c'] = 1-1 = 0 + * 最终count数组全为0,diff=0,匹配,添加索引0 + * + * 滑动过程: + * i=3, 移入'e', 移出'c': + * count['e']从0变为1,diff++;count['c']从0变为-1,diff++ + * diff=2≠0,不匹配 + * + * i=4, 移入'a', 移出'b': + * count['a']从1变为0,diff--;count['b']从0变为-1,diff++ + * diff=1≠0,不匹配 + * + * ...继续滑动直到i=6时重新匹配 + * + * 时间复杂度分析: + * - 初始化:O(m),m为p的长度 + * - 滑动窗口:O(n),n为s的长度 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 字符计数数组:O(26) = O(1) + * - 其他变量:O(1) + * - 总空间复杂度:O(1) + * + * @param s 主字符串 + * @param p 模式字符串 + * @return 所有字母异位词的起始索引列表 + */ + public static List findAnagramsOptimized(String s, String p) { + List result = new ArrayList<>(); + + if (s == null || s.length() < p.length()) { + return result; + } + + int[] count = new int[26]; + int pLen = p.length(); + + // 初始化字符计数差异数组 + for (int i = 0; i < pLen; i++) { + count[s.charAt(i) - 'a']++; + count[p.charAt(i) - 'a']--; + } + + // 计算初始差异 + int diff = 0; + for (int i = 0; i < 26; i++) { + if (count[i] != 0) { + diff++; + } + } + + if (diff == 0) { + result.add(0); + } + + // 滑动窗口 + for (int i = pLen; i < s.length(); i++) { + // 添加新字符 + int rightChar = s.charAt(i) - 'a'; + if (count[rightChar] == 0) { + diff++; + } + count[rightChar]++; + if (count[rightChar] == 0) { + diff--; + } + + // 移除旧字符 + int leftChar = s.charAt(i - pLen) - 'a'; + if (count[leftChar] == 0) { + diff++; + } + count[leftChar]--; + if (count[leftChar] == 0) { + diff--; + } + + // 检查是否匹配 + if (diff == 0) { + result.add(i - pLen + 1); + } + } + + return result; + } + + /** + * 方法3:使用HashMap实现 + * + * 算法思路: + * 使用HashMap统计字符出现次数,避免数组大小限制 + * + * 示例过程(以 s="cbaebabacd", p="abc" 为例): + * + * pMap = {'a':1, 'b':1, 'c':1} + * + * i=0: windowMap = {'c':1} + * i=1: windowMap = {'c':1, 'b':1} + * i=2: windowMap = {'c':1, 'b':1, 'a':1} == pMap,添加索引0 + * i=3: 移入'e',移出'c',windowMap = {'b':1, 'a':1, 'e':1} ≠ pMap + * i=4: 移入'a',移出'b',windowMap = {'a':2, 'e':1} ≠ pMap + * ... + * i=6: windowMap = {'b':1, 'a':1, 'c':1} == pMap,添加索引6 + * + * 时间复杂度分析: + * - 滑动窗口遍历:O(n),n为s的长度 + * - HashMap操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashMap存储字符:O(k),k为不同字符数 + * - 结果列表:O(m) + * - 总空间复杂度:O(k+m) + * + * @param s 主字符串 + * @param p 模式字符串 + * @return 所有字母异位词的起始索引列表 + */ + public static List findAnagramsHashMap(String s, String p) { + List result = new ArrayList<>(); + + if (s == null || s.length() < p.length()) { + return result; + } + + java.util.Map pMap = new java.util.HashMap<>(); + java.util.Map windowMap = new java.util.HashMap<>(); + + // 统计p中字符出现次数 + for (char c : p.toCharArray()) { + pMap.put(c, pMap.getOrDefault(c, 0) + 1); + } + + int pLen = p.length(); + + // 滑动窗口 + for (int i = 0; i < s.length(); i++) { + char rightChar = s.charAt(i); + windowMap.put(rightChar, windowMap.getOrDefault(rightChar, 0) + 1); + + // 如果窗口大小超过p的长度,移除左侧字符 + if (i >= pLen) { + char leftChar = s.charAt(i - pLen); + windowMap.put(leftChar, windowMap.get(leftChar) - 1); + if (windowMap.get(leftChar) == 0) { + windowMap.remove(leftChar); + } + } + + // 检查是否匹配 + if (windowMap.equals(pMap)) { + result.add(i - pLen + 1); + } + } + + return result; + } +} diff --git a/algorithm/FindDuplicate287.java b/algorithm/FindDuplicate287.java new file mode 100644 index 0000000000000..87b86e3167e0a --- /dev/null +++ b/algorithm/FindDuplicate287.java @@ -0,0 +1,169 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 寻找重复数(LeetCode 287) + * + * 时间复杂度:O(n) + * - 需要遍历数组常数次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 没有使用额外的数据结构 + */ +public class FindDuplicate287 { + + /** + * 主函数:处理用户输入并找出重复元素 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用findDuplicate方法找出重复元素 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入整数数组 + System.out.print("请输入整数数组(用空格分隔,包含重复元素):"); + String line = scanner.nextLine(); + String[] strArray = line.split(" "); + + // 将字符串数组转换为整数数组 + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); // 转换字符串为整数 + } + + // 调用 findDuplicate 方法找出重复的元素 + int duplicate = findDuplicate(nums); + + // 输出结果 + System.out.println("重复的元素是:" + duplicate); + } + + /** + * 寻找重复数(Floyd判圈算法) + * + * 算法思路: + * 将数组看作链表,数组下标作为节点,数组值作为next指针 + * 由于存在重复元素,必然存在环,使用Floyd判圈算法找到环的入口 + * + * 执行过程分析(以数组 [1,3,4,2,2] 为例): + * + * 数组索引: 0 1 2 3 4 + * 数组值: 1 3 4 2 2 + * + * 构建的链表结构: + * 0 -> 1 -> 3 -> 2 -> 4 -> 2 (形成环) + * + * Floyd算法执行步骤: + * + * 第一阶段:寻找相遇点 + * 初始:slow = nums[0] = 1, fast = nums[0] = 1 + * 第1步:slow = nums[1] = 3, fast = nums[nums[1]] = nums[3] = 2 + * 第2步:slow = nums[3] = 2, fast = nums[nums[2]] = nums[4] = 2 + * 第3步:slow = nums[2] = 4, fast = nums[nums[2]] = nums[4] = 2 + * 第4步:slow = nums[4] = 2, fast = nums[nums[2]] = nums[4] = 2 + * 相遇点:slow = fast = 2 + * + * 第二阶段:寻找环的入口(重复元素) + * slow = nums[0] = 1, fast = 2 (保持相遇点) + * 第1步:slow = nums[1] = 3, fast = nums[2] = 4 + * 第2步:slow = nums[3] = 2, fast = nums[4] = 2 + * 相遇,环的入口为2,即重复元素 + * + * 数学原理: + * 假设链表头到环入口距离为a,环入口到相遇点距离为b,相遇点到环入口距离为c + * 环的长度为 b+c + * + * 当快慢指针相遇时: + * - 慢指针走过的距离:a + b + * - 快指针走过的距离:a + b + c + b = a + 2b + c + * - 快指针走过的距离是慢指针的2倍:2(a + b) = a + 2b + c + * - 化简得:a = c + * + * 因此,从头节点到环入口的距离等于从相遇点到环入口的距离 + * + * 时间复杂度分析: + * - 第一阶段寻找相遇点:O(n) + * - 第二阶段寻找环入口:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了两个指针变量:O(1) + * + * @param nums 包含n+1个整数的数组,数字都在[1,n]范围内 + * @return 重复的数字 + */ + public static int findDuplicate(int[] nums) { + // 步骤 1:使用快慢指针寻找相遇点 + int slow = nums[0]; + int fast = nums[0]; + + // 循环直到快慢指针相遇 + // do-while循环确保至少执行一次 + do { + slow = nums[slow]; // 慢指针每次移动一步 + fast = nums[nums[fast]]; // 快指针每次移动两步 + } while (slow != fast); // 当快慢指针相遇时结束循环 + + // 步骤 2:寻找环的入口 + slow = nums[0]; // 将慢指针放回起始位置 + // 快指针保持在相遇点位置 + while (slow != fast) { // 当快慢指针再次相遇时找到环入口 + slow = nums[slow]; // 每次移动一步 + fast = nums[fast]; // 每次移动一步 + } + + // 返回重复的元素(环的入口) + return slow; // 或者 return fast; + } + + /** + * 方法2:二分查找解法 + * + * 算法思路: + * 利用抽屉原理,对于区间[1,n]中的任意数字k, + * 如果数组中小于等于k的数字个数大于k,则重复数字在[1,k]区间内 + * 否则重复数字在[k+1,n]区间内 + * + * 时间复杂度分析: + * - 二分查找执行次数:O(log n) + * - 每次二分查找需要遍历数组统计:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 只使用了常数个变量(left, right, mid, count):O(1) + * + * @param nums 包含n+1个整数的数组,数字都在[1,n]范围内 + * @return 重复的数字 + */ + public static int findDuplicateBinarySearch(int[] nums) { + int left = 1; + int right = nums.length - 1; + + while (left < right) { + int mid = left + (right - left) / 2; + int count = 0; + + // 统计数组中小于等于mid的数字个数 + for (int num : nums) { + if (num <= mid) { + count++; + } + } + + // 根据抽屉原理判断重复数字所在的区间 + if (count > mid) { + right = mid; // 重复数字在[left, mid]区间内 + } else { + left = mid + 1; // 重复数字在[mid+1, right]区间内 + } + } + + return left; + } +} diff --git a/algorithm/FindKLargest215.java b/algorithm/FindKLargest215.java new file mode 100644 index 0000000000000..c21682d72ce71 --- /dev/null +++ b/algorithm/FindKLargest215.java @@ -0,0 +1,279 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.PriorityQueue; +import java.util.Random; + +/** + * 数组中的第K个最大元素(LeetCode 215) + * + * 时间复杂度: + * - 方法1(排序):O(n log n) + * - 方法2(堆):O(n log k) + * - 方法3(快速选择):O(n) 平均情况,O(n²) 最坏情况 + * + * 空间复杂度: + * - 方法1(排序):O(1) + * - 方法2(堆):O(k) + * - 方法3(快速选择):O(log n) 平均情况,O(n) 最坏情况 + */ +public class FindKLargest215 { + + /** + * 方法1:排序解法 + * + * 算法思路: + * 对数组进行排序,然后直接返回第k个最大元素 + * + * 时间复杂度分析: + * - 排序操作:O(n log n) + * - 访问元素:O(1) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 排序算法所需空间:O(1)(原地排序) + * + * @param nums 整数数组 + * @param k 第k个最大元素 + * @return 第k个最大元素 + */ + public int findKthLargestSort(int[] nums, int k) { + java.util.Arrays.sort(nums); + return nums[nums.length - k]; + } + + /** + * 方法2:最小堆解法 + * + * 算法思路: + * 维护一个大小为k的最小堆 + * 1. 遍历数组,将元素加入堆中 + * 2. 如果堆大小超过k,弹出最小元素 + * 3. 最终堆顶就是第k个最大元素 + * + * 执行过程分析(以`nums=[3,2,1,5,6,4]`, `k=2`为例): + * + * 初始状态: + * heap = [] + * + * 遍历过程: + * num=3: heap=[3] + * num=2: heap=[2,3] + * num=1: heap=[1,2,3],大小超过k,弹出1,heap=[2,3] + * num=5: heap=[2,3,5],大小超过k,弹出2,heap=[3,5] + * num=6: heap=[3,5,6],大小超过k,弹出3,heap=[5,6] + * num=4: heap=[4,5,6],大小超过k,弹出4,heap=[5,6] + * + * 最终结果:堆顶元素5即为第2个最大元素 + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 堆操作:每次O(log k) + * - 总时间复杂度:O(n log k) + * + * 空间复杂度分析: + * - 最小堆存储空间:O(k) + * + * @param nums 整数数组 + * @param k 第k个最大元素 + * @return 第k个最大元素 + */ + public int findKthLargestHeap(int[] nums, int k) { + // 创建最小堆,堆顶是最小元素 + PriorityQueue minHeap = new PriorityQueue<>(); + + // 遍历数组 + for (int num : nums) { + // 将元素加入堆中 + minHeap.offer(num); + + // 如果堆大小超过k,弹出最小元素 + if (minHeap.size() > k) { + minHeap.poll(); + } + } + + // 返回堆顶元素(第k个最大元素) + return minHeap.peek(); + } + + /** + * 方法3:快速选择算法(最优解) + * + * 算法思路: + * 基于快速排序的分区思想 + * 1. 随机选择一个基准元素 + * 2. 进行分区操作,将数组分为大于基准和小于基准的两部分 + * 3. 根据基准元素的位置决定在哪个部分继续查找 + * + * 执行过程分析(以`nums=[3,2,1,5,6,4]`, `k=2`为例): + * + * 目标:找到第2个最大元素(即第5大的元素) + * 转换:在0索引系统中,目标索引 = `nums.length` - k = 6 - 2 = 4 + * + * 第一次分区: + * 选择基准元素`pivot=4`(随机选择) + * 分区后:[3,2,1,4,6,5],基准元素4在索引3位置 + * 3 < 4,说明第4大的元素在右半部分[6,5] + * + * 第二次分区: + * 在[6,5]中选择基准元素`pivot=5` + * 分区后:[5,6],基准元素5在索引4位置 + * 4 == 4,找到目标元素5 + * + * 时间复杂度分析: + * - 平均情况:O(n) + * - 最坏情况:O(n²) + * - 分区操作:O(n) + * + * 空间复杂度分析: + * - 平均情况:O(log n)(递归调用栈) + * - 最坏情况:O(n)(递归调用栈) + * + * @param nums 整数数组 + * @param k 第k个最大元素 + * @return 第k个最大元素 + */ + public int findKthLargest(int[] nums, int k) { + // 目标索引(转换为0索引系统) + int targetIndex = nums.length - k; + + // 使用快速选择算法 + return quickSelect(nums, 0, nums.length - 1, targetIndex); + } + + /** + * 快速选择辅助方法 + * + * 算法思路: + * 实现快速选择算法的核心逻辑,通过递归方式查找第k个元素 + * + * @param nums 数组 + * @param left 左边界 + * @param right 右边界 + * @param k 目标索引 + * @return 第k个元素 + */ + private int quickSelect(int[] nums, int left, int right, int k) { + // 随机选择基准元素以避免最坏情况 + Random random = new Random(); + int pivotIndex = left + random.nextInt(right - left + 1); + + // 进行分区操作 + pivotIndex = partition(nums, left, right, pivotIndex); + + // 根据基准元素位置决定下一步操作 + if (pivotIndex == k) { + // 找到目标元素 + return nums[pivotIndex]; + } else if (pivotIndex < k) { + // 目标在右半部分 + return quickSelect(nums, pivotIndex + 1, right, k); + } else { + // 目标在左半部分 + return quickSelect(nums, left, pivotIndex - 1, k); + } + } + + /** + * 分区辅助方法 + * + * 算法思路: + * 实现快速排序中的分区操作,将数组分为小于基准和大于基准的两部分 + * + * @param nums 数组 + * @param left 左边界 + * @param right 右边界 + * @param pivotIndex 基准元素索引 + * @return 分区后基准元素的最终位置 + */ + private int partition(int[] nums, int left, int right, int pivotIndex) { + int pivotValue = nums[pivotIndex]; + + // 将基准元素移到末尾 + swap(nums, pivotIndex, right); + + int storeIndex = left; + + // 将小于基准的元素移到左边 + for (int i = left; i < right; i++) { + if (nums[i] < pivotValue) { + swap(nums, storeIndex, i); + storeIndex++; + } + } + + // 将基准元素放到正确位置 + swap(nums, storeIndex, right); + + return storeIndex; + } + + /** + * 交换数组中两个元素 + * + * 算法思路: + * 交换数组中指定位置的两个元素 + * + * @param nums 数组 + * @param i 第一个元素索引 + * @param j 第二个元素索引 + */ + private void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 读取和解析输入:O(n) + * + * 空间复杂度分析: + * - 存储数组:O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入整数数组(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 主函数:处理用户输入并找出第K个最大元素 + */ + public static void main(String[] args) { + System.out.println("数组中的第K个最大元素"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("输入数组: " + java.util.Arrays.toString(nums)); + + // 读取k值 + Scanner scanner = new Scanner(System.in); + System.out.print("请输入k值: "); + int k = scanner.nextInt(); + + // 计算第k个最大元素 + FindKLargest215 solution = new FindKLargest215(); + int result1 = solution.findKthLargestSort(nums, k); // 修正:添加k参数 + int result2 = solution.findKthLargestHeap(nums, k); + int result3 = solution.findKthLargest(nums, k); + + // 输出结果 + System.out.println("排序方法结果: " + result1); + System.out.println("堆方法结果: " + result2); + System.out.println("快速选择方法结果: " + result3); + } +} diff --git a/algorithm/FindMediumNumber4.java b/algorithm/FindMediumNumber4.java new file mode 100644 index 0000000000000..bd34fb82cc909 --- /dev/null +++ b/algorithm/FindMediumNumber4.java @@ -0,0 +1,402 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 寻找两个正序数组的中位数(LeetCode 4) + * + * 时间复杂度:O(log(min(m,n))) + * - 使用二分查找在较短的数组上进行搜索 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class FindMediumNumber4 { + + /** + * 寻找两个正序数组的中位数 + * + * 算法思路: + * 使用二分查找,确保较短的数组作为第一个数组 + * 1. 在较短数组上进行二分查找,确定分割点 + * 2. 根据总长度确定另一个数组的分割点 + * 3. 确保左半部分的最大值小于等于右半部分的最小值 + * 4. 根据总长度奇偶性计算中位数 + * + * 执行过程分析(以`nums1=[1,3]`, `nums2=[2]`为例): + * + * 初始状态: + * A = [1,3], B = [2] + * m = 2, n = 1 + * total = 3, half = 1 + * + * 二分查找过程: + * left = 0, right = 2 + * i = 1, j = 1-1-0 = 0 + * + * 分割后: + * A_left = 1, A_right = 3 + * B_left = -∞, B_right = 2 + * + * 检查条件: + * A_left(1) <= B_right(2) ✓ + * B_left(-∞) <= A_right(3) ✓ + * + * 计算中位数: + * 总长度为奇数,中位数 = min(A_right, B_right) = min(3, 2) = 2 + * + * 执行过程分析(以`nums1=[1,2]`, `nums2=[3,4]`为例): + * + * 初始状态: + * A = [1,2], B = [3,4] + * m = 2, n = 2 + * total = 4, half = 2 + * + * 二分查找过程: + * left = 0, right = 2 + * i = 1, j = 2-1-1 = 0 + * + * 分割后: + * A_left = 1, A_right = 2 + * B_left = -∞, B_right = 3 + * + * 检查条件: + * A_left(1) <= B_right(3) ✓ + * B_left(-∞) <= A_right(2) ✓ + * + * 计算中位数: + * 总长度为偶数,中位数 = (max(1,-∞) + min(2,3)) / 2 = (1 + 2) / 2 = 1.5 + * + * 时间复杂度分析: + * - 二分查找:O(log(min(m,n))) + * - 每次迭代操作:O(1) + * - 总时间复杂度:O(log(min(m,n))) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums1 第一个正序数组 + * @param nums2 第二个正序数组 + * @return 两个数组的中位数 + */ + public double findMedianSortedArrays(int[] nums1, int[] nums2) { + // 确保nums1是较短的数组,优化时间复杂度 + // if (nums1.length > nums2.length) 检查nums1是否比nums2长 + if (nums1.length > nums2.length) { + // return findMedianSortedArrays(nums2, nums1) 交换参数递归调用 + return findMedianSortedArrays(nums2, nums1); + } + + // int m = nums1.length 获取nums1的长度 + int m = nums1.length; + // int n = nums2.length 获取nums2的长度 + int n = nums2.length; + + // 总长度和左半部分的长度 + // int total = m + n 计算总长度 + int total = m + n; + // int half = (total + 1) / 2 计算左半部分长度 + int half = (total + 1) / 2; + + // 二分查找的边界 + // int left = 0 初始化左边界 + int left = 0; + // int right = m 初始化右边界 + int right = m; + + // while (left <= right) 二分查找循环 + while (left <= right) { + // nums1的分割点 + // int i = left + (right - left) / 2 计算nums1的分割点 + int i = left + (right - left) / 2; + // nums2的分割点 + // int j = half - i 计算nums2的分割点 + int j = half - i; + + // 分割点左边的元素(如果越界则用极值代替) + // int A_left = (i > 0) ? nums1[i - 1] : Integer.MIN_VALUE 获取A的左半部分最大值 + int A_left = (i > 0) ? nums1[i - 1] : Integer.MIN_VALUE; + // int B_left = (j > 0) ? nums2[j - 1] : Integer.MIN_VALUE 获取B的左半部分最大值 + int B_left = (j > 0) ? nums2[j - 1] : Integer.MIN_VALUE; + + // 分割点右边的元素(如果越界则用极值代替) + // int A_right = (i < m) ? nums1[i] : Integer.MAX_VALUE 获取A的右半部分最小值 + int A_right = (i < m) ? nums1[i] : Integer.MAX_VALUE; + // int B_right = (j < n) ? nums2[j] : Integer.MAX_VALUE 获取B的右半部分最小值 + int B_right = (j < n) ? nums2[j] : Integer.MAX_VALUE; + + // 检查是否找到正确的分割点 + // if (A_left <= B_right && B_left <= A_right) 检查分割点是否正确 + if (A_left <= B_right && B_left <= A_right) { + // 找到正确的分割点 + // if (total % 2 == 1) 检查总长度是否为奇数 + if (total % 2 == 1) { + // 总长度为奇数,返回左半部分的最大值 + // return Math.max(A_left, B_left) 返回中位数 + return Math.max(A_left, B_left); + } else { + // 总长度为偶数,返回左半部分最大值和右半部分最小值的平均值 + // return (Math.max(A_left, B_left) + Math.min(A_right, B_right)) / 2.0 返回中位数 + return (Math.max(A_left, B_left) + Math.min(A_right, B_right)) / 2.0; + } + } else if (A_left > B_right) { + // nums1的左半部分太大,需要向左移动 + // right = i - 1 调整右边界 + right = i - 1; + } else { + // nums1的左半部分太小,需要向右移动 + // left = i + 1 调整左边界 + left = i + 1; + } + } + + // 理论上不会执行到这里 + // return 0.0 返回默认值 + return 0.0; + } + + /** + * 方法2:合并后找中位数(简单但效率较低) + * + * 算法思路: + * 先合并两个有序数组,再找中位数 + * + * 时间复杂度分析: + * - 合并数组:O(m+n) + * - 计算中位数:O(1) + * - 总时间复杂度:O(m+n) + * + * 空间复杂度分析: + * - 合并数组存储空间:O(m+n) + * + * @param nums1 第一个正序数组 + * @param nums2 第二个正序数组 + * @return 两个数组的中位数 + */ + public double findMedianSortedArraysMerge(int[] nums1, int[] nums2) { + // int m = nums1.length 获取nums1的长度 + int m = nums1.length; + // int n = nums2.length 获取nums2的长度 + int n = nums2.length; + // int[] merged = new int[m + n] 创建合并后的数组 + int[] merged = new int[m + n]; + + // int i = 0, j = 0, k = 0 初始化指针 + int i = 0, j = 0, k = 0; + + // 合并两个有序数组 + // while (i < m && j < n) 当两个数组都未遍历完时循环 + while (i < m && j < n) { + // if (nums1[i] <= nums2[j]) 比较两个数组当前元素 + if (nums1[i] <= nums2[j]) { + // merged[k++] = nums1[i++] 将较小元素放入合并数组 + merged[k++] = nums1[i++]; + } else { + // merged[k++] = nums2[j++] 将较小元素放入合并数组 + merged[k++] = nums2[j++]; + } + } + + // 复制剩余元素 + // while (i < m) 复制nums1剩余元素 + while (i < m) { + // merged[k++] = nums1[i++] 复制元素 + merged[k++] = nums1[i++]; + } + + // while (j < n) 复制nums2剩余元素 + while (j < n) { + // merged[k++] = nums2[j++] 复制元素 + merged[k++] = nums2[j++]; + } + + // 计算中位数 + // int total = m + n 计算总长度 + int total = m + n; + // if (total % 2 == 1) 检查总长度是否为奇数 + if (total % 2 == 1) { + // return merged[total / 2] 返回中位数 + return merged[total / 2]; + } else { + // return (merged[total / 2 - 1] + merged[total / 2]) / 2.0 返回中位数 + return (merged[total / 2 - 1] + merged[total / 2]) / 2.0; + } + } + + /** + * 方法3:递归解法(找第k小的元素) + * + * 算法思路: + * 通过递归查找第k小的元素来计算中位数 + * + * 时间复杂度分析: + * - 每次递归排除k/2个元素:O(log(m+n)) + * - 总时间复杂度:O(log(m+n)) + * + * 空间复杂度分析: + * - 递归调用栈:O(log(m+n)) + * + * @param nums1 第一个正序数组 + * @param nums2 第二个正序数组 + * @return 两个数组的中位数 + */ + public double findMedianSortedArraysRecursive(int[] nums1, int[] nums2) { + // int m = nums1.length 获取nums1的长度 + int m = nums1.length; + // int n = nums2.length 获取nums2的长度 + int n = nums2.length; + // int total = m + n 计算总长度 + int total = m + n; + + // if (total % 2 == 1) 检查总长度是否为奇数 + if (total % 2 == 1) { + // return findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2 + 1) 查找第(total/2+1)小的元素 + return findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2 + 1); + } else { + // int mid1 = findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2) 查找第(total/2)小的元素 + int mid1 = findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2); + // int mid2 = findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2 + 1) 查找第(total/2+1)小的元素 + int mid2 = findKthElement(nums1, 0, m - 1, nums2, 0, n - 1, total / 2 + 1); + // return (mid1 + mid2) / 2.0 返回中位数 + return (mid1 + mid2) / 2.0; + } + } + + /** + * 找到两个有序数组中第k小的元素 + * + * 算法思路: + * 通过比较两个数组中第k/2个元素,排除较小的一半 + * + * @param nums1 第一个数组 + * @param start1 nums1的起始索引 + * @param end1 nums1的结束索引 + * @param nums2 第二个数组 + * @param start2 nums2的起始索引 + * @param end2 nums2的结束索引 + * @param k 第k小的元素 + * @return 第k小的元素值 + */ + private int findKthElement(int[] nums1, int start1, int end1, + int[] nums2, int start2, int end2, int k) { + // int len1 = end1 - start1 + 1 计算nums1的有效长度 + int len1 = end1 - start1 + 1; + // int len2 = end2 - start2 + 1 计算nums2的有效长度 + int len2 = end2 - start2 + 1; + + // 确保nums1是较短的数组 + // if (len1 > len2) 检查nums1是否比nums2长 + if (len1 > len2) { + // return findKthElement(nums2, start2, end2, nums1, start1, end1, k) 交换参数递归调用 + return findKthElement(nums2, start2, end2, nums1, start1, end1, k); + } + + // 如果nums1为空,直接在nums2中找 + // if (len1 == 0) 检查nums1是否为空 + if (len1 == 0) { + // return nums2[start2 + k - 1] 返回nums2中第k小的元素 + return nums2[start2 + k - 1]; + } + + // 如果k为1,返回两个数组起始元素的较小值 + // if (k == 1) 检查k是否为1 + if (k == 1) { + // return Math.min(nums1[start1], nums2[start2]) 返回较小值 + return Math.min(nums1[start1], nums2[start2]); + } + + // 在两个数组中分别找第k/2个元素 + // int i = start1 + Math.min(len1, k / 2) - 1 计算nums1中要比较的元素索引 + int i = start1 + Math.min(len1, k / 2) - 1; + // int j = start2 + Math.min(len2, k / 2) - 1 计算nums2中要比较的元素索引 + int j = start2 + Math.min(len2, k / 2) - 1; + + // if (nums1[i] > nums2[j]) 比较两个元素 + if (nums1[i] > nums2[j]) { + // nums2的前k/2个元素可以排除 + // return findKthElement(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)) 递归查找 + return findKthElement(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1)); + } else { + // nums1的前k/2个元素可以排除 + // return findKthElement(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)) 递归查找 + return findKthElement(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1)); + } + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 读取和解析输入:O(n) + * + * 空间复杂度分析: + * - 存储数组:O(n) + * + * @param prompt 提示信息 + * @return 用户输入的整数数组 + */ + public static int[] readArray(String prompt) { + // Scanner scanner = new Scanner(System.in) 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // System.out.println(prompt) 打印提示信息 + System.out.println(prompt); + // String input = scanner.nextLine() 读取输入 + String input = scanner.nextLine(); + // if (input.trim().isEmpty()) 检查输入是否为空 + if (input.trim().isEmpty()) { + // return new int[0] 返回空数组 + return new int[0]; + } + // String[] strArray = input.split(" ") 分割字符串 + String[] strArray = input.split(" "); + + // int[] nums = new int[strArray.length] 创建整数数组 + int[] nums = new int[strArray.length]; + // for (int i = 0; i < strArray.length; i++) 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // nums[i] = Integer.parseInt(strArray[i]) 转换为整数 + nums[i] = Integer.parseInt(strArray[i]); + } + + // return nums 返回整数数组 + return nums; + } + + /** + * 主函数:处理用户输入并计算两个数组的中位数 + */ + public static void main(String[] args) { + // System.out.println("寻找两个正序数组的中位数") 打印程序标题 + System.out.println("寻找两个正序数组的中位数"); + + // 读取用户输入的数组 + // int[] nums1 = readArray("请输入第一个正序数组(用空格分隔):") 读取第一个数组 + int[] nums1 = readArray("请输入第一个正序数组(用空格分隔):"); + // int[] nums2 = readArray("请输入第二个正序数组(用空格分隔):") 读取第二个数组 + int[] nums2 = readArray("请输入第二个正序数组(用空格分隔):"); + + // System.out.println("第一个数组: " + Arrays.toString(nums1)) 打印第一个数组 + System.out.println("第一个数组: " + Arrays.toString(nums1)); + // System.out.println("第二个数组: " + Arrays.toString(nums2)) 打印第二个数组 + System.out.println("第二个数组: " + Arrays.toString(nums2)); + + // 计算中位数 + // FindMediumNumber4 solution = new FindMediumNumber4() 创建解决方案对象 + FindMediumNumber4 solution = new FindMediumNumber4(); + // double result1 = solution.findMedianSortedArrays(nums1, nums2) 调用二分查找方法 + double result1 = solution.findMedianSortedArrays(nums1, nums2); + // double result2 = solution.findMedianSortedArraysMerge(nums1, nums2) 调用合并方法 + double result2 = solution.findMedianSortedArraysMerge(nums1, nums2); + // double result3 = solution.findMedianSortedArraysRecursive(nums1, nums2) 调用递归方法 + double result3 = solution.findMedianSortedArraysRecursive(nums1, nums2); + + // 输出结果 + // System.out.println("二分查找方法结果: " + result1) 打印二分查找方法结果 + System.out.println("二分查找方法结果: " + result1); + // System.out.println("合并方法结果: " + result2) 打印合并方法结果 + System.out.println("合并方法结果: " + result2); + // System.out.println("递归方法结果: " + result3) 打印递归方法结果 + System.out.println("递归方法结果: " + result3); + } +} diff --git a/algorithm/FindMin153.java b/algorithm/FindMin153.java new file mode 100644 index 0000000000000..0a86ce81b12ba --- /dev/null +++ b/algorithm/FindMin153.java @@ -0,0 +1,192 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 寻找旋转排序数组中的最小值(LeetCode 153) + * + * 时间复杂度:O(log n) + * - 使用二分查找,每次将搜索范围减半 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class FindMin153 { + + /** + * 寻找旋转排序数组中的最小值 + * + * 算法思路: + * 使用二分查找,关键在于判断最小值在哪一半 + * 1. 比较中间元素和右边界元素 + * 2. 如果中间元素小于右边界元素,说明右半部分有序,最小值在左半部分(包括中间) + * 3. 如果中间元素大于右边界元素,说明左半部分有序,最小值在右半部分 + * + * @param nums 旋转排序数组(无重复元素) + * @return 数组中的最小值 + */ + public int findMin(int[] nums) { + // 边界情况:空数组 + if (nums == null || nums.length == 0) { + // 抛出异常 + throw new IllegalArgumentException("数组不能为空"); + } + + // 如果数组只有一个元素,直接返回 + if (nums.length == 1) { + // 返回唯一元素 + return nums[0]; + } + + // 左右指针 + int left = 0; + int right = nums.length - 1; + + // 如果数组没有旋转,第一个元素就是最小值 + if (nums[left] < nums[right]) { + // 返回第一个元素 + return nums[left]; + } + + // 二分查找 + while (left < right) { + // 计算中间索引 + int mid = left + (right - left) / 2; + + // 关键判断:比较中间元素和右边界元素 + if (nums[mid] > nums[right]) { + // 中间元素大于右边界元素,说明最小值在右半部分 + left = mid + 1; + } else { + // 中间元素小于等于右边界元素,说明最小值在左半部分(包括中间) + right = mid; + } + } + + // left == right时,指向最小值 + return nums[left]; + } + + /** + * 方法2:比较中间元素和左边界元素的版本 + * + * 算法思路: + * 通过比较中间元素和左边界元素来判断最小值位置 + * + * @param nums 旋转排序数组 + * @return 数组中的最小值 + */ + public int findMinAlternative(int[] nums) { + if (nums == null || nums.length == 0) { + // 抛出异常 + throw new IllegalArgumentException("数组不能为空"); + } + + int left = 0; + int right = nums.length - 1; + + while (left < right) { + int mid = left + (right - left) / 2; + + // 比较中间元素和左边界元素 + if (nums[mid] > nums[left]) { + // 左半部分有序 + if (nums[left] < nums[right]) { + // 整个数组有序,最小值在最左边 + return nums[left]; + } else { + // 最小值在右半部分 + left = mid + 1; + } + } else { + // 右半部分有序,最小值在左半部分(包括中间) + right = mid; + } + } + + return nums[left]; + } + + /** + * 方法3:线性搜索解法(仅供对比) + * + * 算法思路: + * 遍历数组找到最小值 + * + * @param nums 旋转排序数组 + * @return 数组中的最小值 + */ + public int findMinLinear(int[] nums) { + if (nums == null || nums.length == 0) { + // 抛出异常 + throw new IllegalArgumentException("数组不能为空"); + } + + int min = nums[0]; + for (int i = 1; i < nums.length; i++) { + if (nums[i] < min) { + // 更新最小值 + min = nums[i]; + } + } + return min; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + // 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // 打印提示信息 + System.out.println("请输入旋转排序数组(用空格分隔):"); + // 读取输入 + String input = scanner.nextLine(); + // 分割字符串 + String[] strArray = input.split(" "); + + // 创建整数数组 + int[] nums = new int[strArray.length]; + // 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // 转换为整数 + nums[i] = Integer.parseInt(strArray[i]); + } + + // 返回整数数组 + return nums; + } + + /** + * 主函数:处理用户输入并找出数组中的最小值 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("寻找旋转排序数组中的最小值"); + + // 调用readArray方法读取数组 + int[] nums = readArray(); + // 打印输入数组 + System.out.println("输入数组: " + Arrays.toString(nums)); + + // 计算最小值 + FindMin153 solution = new FindMin153(); + // 调用findMin方法 + int result1 = solution.findMin(nums); + // 调用findMinAlternative方法 + int result2 = solution.findMinAlternative(nums); + // 调用findMinLinear方法 + int result3 = solution.findMinLinear(nums); + + // 输出结果 + // 打印二分查找方法结果 + System.out.println("二分查找方法结果: " + result1); + // 打印替代方法结果 + System.out.println("替代方法结果: " + result2); + // 打印线性搜索方法结果 + System.out.println("线性搜索方法结果: " + result3); + } +} diff --git a/algorithm/FirstMissingPositive41.java b/algorithm/FirstMissingPositive41.java new file mode 100644 index 0000000000000..715578fc6f0e7 --- /dev/null +++ b/algorithm/FirstMissingPositive41.java @@ -0,0 +1,262 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 缺失的第一个正数(LeetCode 41) + * + * 时间复杂度:O(n) + * - 第一个循环最多执行 n 次交换操作(每个元素最多被放置到正确位置一次) + * - 第二个循环遍历数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度:O(1) + * - 原地操作,只使用了常数级别的额外空间 + */ +public class FirstMissingPositive41 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 读取输入的数组 + System.out.print("请输入数组元素,以空格分隔:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] str = line.split(" "); + + // 获取数组长度 + int n = str.length; + + // 创建整型数组 + int[] nums = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(str[i]); + } + + // 调用 firstMissingPositive 方法计算缺失的第一个正数 + int result = firstMissingPositive(nums); + + // 输出结果 + System.out.println("数组中缺失的最小正整数是:" + result); + } + + /** + * 找到数组中缺失的第一个正整数 + * + * 算法思路: + * 使用原地哈希的思想,将数组本身作为哈希表 + * 1. 对于长度为 n 的数组,缺失的第一个正整数一定在 [1, n+1] 范围内 + * 2. 将每个数字放到它应该在的位置上(数字 i 放到索引 i-1 的位置) + * 3. 遍历数组找到第一个不在正确位置的数字 + * + * 示例过程(以数组 [3,4,-1,1] 为例): + * 数组: [3, 4, -1, 1] + * 索引: 0 1 2 3 + * + * 步骤1 - 将数字放到正确位置: + * i=0: nums[0]=3, 应该放在索引2, 交换nums[0]和nums[2] + * [-1, 4, 3, 1] + * i=0: nums[0]=-1, 不在[1,4]范围内, 跳过 + * i=1: nums[1]=4, 应该放在索引3, 交换nums[1]和nums[3] + * [-1, 1, 3, 4] + * i=1: nums[1]=1, 应该放在索引0, 交换nums[1]和nums[0] + * [1, -1, 3, 4] + * i=1: nums[1]=-1, 不在[1,4]范围内, 跳过 + * i=2: nums[2]=3, 已经在正确位置, 跳过 + * i=3: nums[3]=4, 已经在正确位置, 跳过 + * + * 最终数组: [1, -1, 3, 4] + * + * 步骤2 - 找到第一个不匹配的位置: + * i=0: nums[0]=1, 匹配 i+1=1 + * i=1: nums[1]=-1, 不匹配 i+1=2, 返回 2 + * + * 结果: 2 + * + * 时间复杂度分析: + * - 放置数字到正确位置:O(n),其中n为输入数组`nums`的长度,每个元素最多被交换一次 + * - 查找第一个不匹配位置:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 原地操作,只使用常数额外空间:O(1) + * + * @param nums 输入的整数数组 + * @return 缺失的第一个正整数 + */ + public static int firstMissingPositive(int[] nums) { + // 获取数组长度 + int n = nums.length; + + // 1. 将数字放到正确的位置 + // 对于每个位置,如果该位置的数字在[1,n]范围内且不在正确位置,则交换到正确位置 + for (int i = 0; i < n; i++) { + // 当前数字在有效范围内(1到n) 且 不在正确位置 且 正确位置上的数字不等于当前数字时进行交换 + while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) { + // 交换 nums[i] 和 nums[nums[i] - 1] + // 将数字 nums[i] 放到它应该在的位置 nums[i]-1 + swap(nums, i, nums[i] - 1); + } + } + + // 2. 找到第一个不匹配的数字 + // 正确情况下,索引i位置应该存放数字i+1 + for (int i = 0; i < n; i++) { + // 如果索引i位置存放的不是i+1,则i+1就是缺失的第一个正整数 + if (nums[i] != i + 1) { + // 返回缺失的最小正整数 + return i + 1; + } + } + + // 3. 如果所有位置都匹配,说明数组包含[1,n]的所有正整数 + // 缺失的第一个正整数就是n+1 + return n + 1; + } + + /** + * 交换数组中两个位置的元素 + * + * 时间复杂度分析: + * - 交换操作:O(1) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param nums 数组 + * @param i 第一个位置 + * @param j 第二个位置 + */ + private static void swap(int[] nums, int i, int j) { + // 临时变量存储第一个位置的值 + int temp = nums[i]; + // 将第二个位置的值赋给第一个位置 + nums[i] = nums[j]; + // 将临时变量的值赋给第二个位置 + nums[j] = temp; + } + + /** + * 方法2:使用布尔数组标记 + * + * 算法思路: + * 1. 创建布尔数组标记[1,n]范围内的数字是否存在 + * 2. 遍历原数组,标记存在的正整数 + * 3. 找到第一个未被标记的位置 + * + * 示例过程(以数组 [3,4,-1,1] 为例): + * + * 1. 初始化: present = [false, false, false, false, false] (长度为n+1=5) + * 2. 标记过程: + * num=3: 0<3≤4, present[3]=true → [false, false, false, true, false] + * num=4: 0<4≤4, present[4]=true → [false, false, false, true, true] + * num=-1: -1≤0, 跳过 + * num=1: 0<1≤4, present[1]=true → [false, true, false, true, true] + * 3. 查找过程: + * i=1: present[1]=true, 继续 + * i=2: present[2]=false, 返回2 + * + * 时间复杂度分析: + * - 标记存在的正整数:O(n),其中n为输入数组`nums`的长度 + * - 查找第一个不存在的正整数:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 布尔数组:O(n) + * - 其他变量:O(1) + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @return 缺失的第一个正整数 + */ + public static int firstMissingPositiveBooleanArray(int[] nums) { + int n = nums.length; + boolean[] present = new boolean[n + 1]; + + // 标记存在的正整数 + for (int num : nums) { + if (num > 0 && num <= n) { + present[num] = true; + } + } + + // 找到第一个不存在的正整数 + for (int i = 1; i <= n; i++) { + if (!present[i]) { + return i; + } + } + + return n + 1; + } + + /** + * 方法3:将负数和超出范围的数标记为n+1 + * + * 算法思路: + * 1. 将所有非正数和大于n的数替换为n+1 + * 2. 使用数组本身作为标记数组,将对应位置的数变为负数 + * 3. 找到第一个正数的位置 + * + * 示例过程(以数组 [3,4,-1,1] 为例): + * + * 1. 初始化: nums = [3, 4, -1, 1], n = 4 + * 2. 替换非正数: + * nums[0]=3: 3>0, 不变 + * nums[1]=4: 4>0, 不变 + * nums[2]=-1: -1≤0, nums[2]=5 → [3, 4, 5, 1] + * nums[3]=1: 1>0, 不变 + * 3. 标记数组: + * i=0, num=abs(3)=3: 3≤4, nums[2]=-abs(nums[2])=-5 → [3, 4, -5, 1] + * i=1, num=abs(4)=4: 4≤4, nums[3]=-abs(nums[3])=-1 → [3, 4, -5, -1] + * i=2, num=abs(-5)=5: 5>4, 跳过 + * i=3, num=abs(-1)=1: 1≤4, nums[0]=-abs(nums[0])=-3 → [-3, 4, -5, -1] + * 4. 查找正数: + * i=0: nums[0]=-3<0, 继续 + * i=1: nums[1]=4>0, 返回1+1=2 + * + * 时间复杂度分析: + * - 替换非正数:O(n),其中n为输入数组`nums`的长度 + * - 标记数组:O(n) + * - 查找正数:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 原地操作:O(1) + * + * @param nums 输入的整数数组 + * @return 缺失的第一个正整数 + */ + public static int firstMissingPositiveMarking(int[] nums) { + int n = nums.length; + + // 将所有非正数替换为n+1 + for (int i = 0; i < n; i++) { + if (nums[i] <= 0) { + nums[i] = n + 1; + } + } + + // 使用数组本身作为标记数组 + for (int i = 0; i < n; i++) { + int num = Math.abs(nums[i]); + if (num <= n) { + nums[num - 1] = -Math.abs(nums[num - 1]); + } + } + + // 找到第一个正数的位置 + for (int i = 0; i < n; i++) { + if (nums[i] > 0) { + return i + 1; + } + } + + return n + 1; + } +} diff --git a/algorithm/Flatten114.java b/algorithm/Flatten114.java new file mode 100644 index 0000000000000..f93aaf1eb580c --- /dev/null +++ b/algorithm/Flatten114.java @@ -0,0 +1,405 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 二叉树展开为链表(LeetCode 114) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(h) + * - h是二叉树的高度 + * - 递归调用栈的深度 + */ +public class Flatten114 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int val) { this.val = val; } + } + + /** + * 将二叉树展开为链表 + * + * 算法思路: + * 使用后序遍历的思想,对于每个节点: + * 1. 递归展开左子树 + * 2. 递归展开右子树 + * 3. 将左子树连接到右子树位置 + * 4. 将原来的右子树连接到当前链表的末尾 + * + * 执行过程分析(以二叉树 [1,2,5,3,4,null,6] 为例): + * + * 1 + * / \ + * 2 5 + * / \ \ + * 3 4 6 + * + * 展开过程: + * 1. 展开节点3:3 + * 2. 展开节点4:4 + * 3. 展开节点2: + * - 左子树2->3->4 + * - 右子树为空 + * - 结果:2->3->4 + * 4. 展开节点6:6 + * 5. 展开节点5: + * - 左子树为空 + * - 右子树5->6 + * - 结果:5->6 + * 6. 展开节点1: + * - 左子树1->2->3->4 + * - 右子树1->2->3->4->5->6 + * - 结果:1->2->3->4->5->6 + * + * 最终链表:1->2->3->4->5->6(右子节点连接,左子节点为null) + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param root 二叉树的根节点 + */ + public void flatten(TreeNode root) { + // 调用递归辅助方法 + flattenHelper(root); + } + + /** + * 递归辅助方法 + * + * 算法思路: + * 1. 递归处理左子树和右子树 + * 2. 将左子树移动到右子树位置 + * 3. 将原来的右子树连接到新链表末尾 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param root 当前节点 + * @return 展开后链表的尾节点 + */ + private TreeNode flattenHelper(TreeNode root) { + // 基础情况:如果节点为空,返回null + if (root == null) { + return null; + } + + // 递归处理左子树和右子树 + TreeNode leftTail = flattenHelper(root.left); + TreeNode rightTail = flattenHelper(root.right); + + // 如果左子树不为空 + if (leftTail != null) { + // 将左子树的尾部连接到原来的右子树 + leftTail.right = root.right; + + // 将左子树移动到右子树位置 + root.right = root.left; + + // 将左子树置为空 + root.left = null; + } + + // 确定并返回链表的尾节点 + if (rightTail != null) { + return rightTail; + } else if (leftTail != null) { + return leftTail; + } else { + return root; + } + } + + /** + * 迭代解法 + * + * 算法思路: + * 使用栈进行前序遍历,在遍历过程中重新连接节点 + * + * 时间复杂度分析: + * - 每个节点入栈和出栈一次:O(n) + * + * 空间复杂度分析: + * - 显式栈最多存储树的高度个节点:O(h) + * + * @param root 二叉树的根节点 + */ + public void flattenIterative(TreeNode root) { + // 如果根节点为空,直接返回 + if (root == null) { + return; + } + + // 创建栈用于迭代遍历 + Stack stack = new Stack<>(); + stack.push(root); + + // 当栈不为空时继续遍历 + while (!stack.isEmpty()) { + // 弹出栈顶节点 + TreeNode current = stack.pop(); + + // 先将右子节点入栈(如果存在) + if (current.right != null) { + stack.push(current.right); + } + + // 再将左子节点入栈(如果存在) + if (current.left != null) { + stack.push(current.left); + } + + // 如果栈不为空,将当前节点的右指针指向栈顶节点 + if (!stack.isEmpty()) { + current.right = stack.peek(); + } + + // 将当前节点的左指针置为空 + current.left = null; + } + } + + /** + * 原地解法(O(1)空间复杂度) + * + * 算法思路: + * 对于每个节点,如果存在左子树: + * 1. 找到左子树的最右节点 + * 2. 将原右子树连接到该节点的右子树 + * 3. 将左子树移动到右子树位置 + * 4. 将左子树置为空 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * - 寻找前驱节点的总时间复杂度也是O(n) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param root 二叉树的根节点 + */ + public void flattenInPlace(TreeNode root) { + TreeNode current = root; + + // 当当前节点不为空时继续 + while (current != null) { + // 如果当前节点存在左子树 + if (current.left != null) { + // 找到左子树的最右节点 + TreeNode predecessor = current.left; + while (predecessor.right != null) { + predecessor = predecessor.right; + } + + // 将原右子树连接到前驱节点的右子树 + predecessor.right = current.right; + + // 将左子树移动到右子树位置 + current.right = current.left; + + // 将左子树置为空 + current.left = null; + } + + // 移动到下一个节点 + current = current.right; + } + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:打印展开后的链表 + * + * 算法思路: + * 按照右子节点连接顺序打印节点值 + */ + public static void printFlattenedList(TreeNode root) { + System.out.print("展开后的链表: "); + TreeNode current = root; + + // 当当前节点不为空时继续遍历 + while (current != null) { + System.out.print(current.val); + if (current.right != null) { + System.out.print(" -> "); + } + current = current.right; + } + System.out.println(); + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并演示二叉树展开为链表 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建的二叉树 + * 4. 调用三种方法展开二叉树 + * 5. 打印展开后的链表 + */ + public static void main(String[] args) { + System.out.println("二叉树展开为链表"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空"); + return; + } + + // 打印创建的二叉树 + System.out.println("原始二叉树:"); + printLevelOrder(root); + + // 方法1:递归解法 + // 创建解决方案实例 + Flatten114 solution = new Flatten114(); + // 保存原始树的副本用于后续方法测试 + TreeNode rootCopy1 = copyTree(root); + TreeNode rootCopy2 = copyTree(root); + + solution.flatten(root); + // 打印结果 + System.out.println("\n递归方法展开后:"); + printFlattenedList(root); + + // 方法2:迭代解法 + solution.flattenIterative(rootCopy1); + // 打印结果 + System.out.println("\n迭代方法展开后:"); + printFlattenedList(rootCopy1); + + // 方法3:原地解法 + solution.flattenInPlace(rootCopy2); + // 打印结果 + System.out.println("\n原地方法展开后:"); + printFlattenedList(rootCopy2); + } + + /** + * 辅助方法:复制二叉树 + */ + private static TreeNode copyTree(TreeNode root) { + if (root == null) return null; + TreeNode newRoot = new TreeNode(root.val); + newRoot.left = copyTree(root.left); + newRoot.right = copyTree(root.right); + return newRoot; + } +} diff --git a/algorithm/Generate22.java b/algorithm/Generate22.java new file mode 100644 index 0000000000000..aa5d0b3a96f49 --- /dev/null +++ b/algorithm/Generate22.java @@ -0,0 +1,114 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * 括号生成(LeetCode 22) + * + * 时间复杂度:O(4^n / √n) + * 空间复杂度:O(4^n / √n) + */ +public class Generate22 { + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入括号对数 n:"); + int n = scanner.nextInt(); + + List result = generateParenthesis(n); + + System.out.println("所有有效的括号组合:"); + for (String s : result) { + System.out.println(s); + } + } + + /** + * 生成有效括号组合的方法 + * + * 算法思路: + * 使用回溯算法,通过递归生成所有有效的括号组合 + * 1. 维护已使用的左括号(open)和右括号(close)数量 + * 2. 只有当左括号数量大于右括号数量时才能添加右括号 + * 3. 当当前字符串长度达到2*n时得到一个有效组合 + * 4. 剪枝:左括号不能超过n个,右括号不能超过左括号数量 + * + * @param n 括号对数 + * @return 所有可能的有效括号组合 + */ + public static List generateParenthesis(int n) { + List result = new ArrayList<>(); + backtrack(result, "", 0, 0, n); + return result; + } + + /** + * 回溯方法 + * + * @param result 存储所有有效括号组合的结果列表 + * @param current 当前构建的括号序列 + * @param open 已使用的左括号数量 + * @param close 已使用的右括号数量 + * @param max 括号对数 + */ + private static void backtrack(List result, String current, int open, int close, int max) { + // 如果当前组合的长度达到最大长度(2*n),则添加到结果中 + if (current.length() == 2 * max) { + result.add(current); + return; + } + + // 如果还可以添加左括号(左括号数量小于n) + if (open < max) { + backtrack(result, current + "(", open + 1, close, max); + } + + // 如果可以添加右括号(右括号数量小于左括号数量) + if (close < open) { + backtrack(result, current + ")", open, close + 1, max); + } + } + + /** + * 方法2:使用StringBuilder优化字符串操作 + * + * @param n 括号对数 + * @return 所有可能的有效括号组合 + */ + public List generateParenthesisOptimized(int n) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + backtrackOptimized(result, current, 0, 0, n); + return result; + } + + /** + * 优化版回溯方法 + * + * @param result 存储所有有效括号组合的结果列表 + * @param current 当前构建的括号序列 + * @param open 已使用的左括号数量 + * @param close 已使用的右括号数量 + * @param max 括号对数 + */ + private void backtrackOptimized(List result, StringBuilder current, int open, int close, int max) { + if (current.length() == 2 * max) { + result.add(current.toString()); + return; + } + + if (open < max) { + current.append('('); + backtrackOptimized(result, current, open + 1, close, max); + current.deleteCharAt(current.length() - 1); + } + + if (close < open) { + current.append(')'); + backtrackOptimized(result, current, open, close + 1, max); + current.deleteCharAt(current.length() - 1); + } + } +} diff --git a/algorithm/GenerateTriangle118.java b/algorithm/GenerateTriangle118.java new file mode 100644 index 0000000000000..fe9d466801ab9 --- /dev/null +++ b/algorithm/GenerateTriangle118.java @@ -0,0 +1,199 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * 生成杨辉三角(LeetCode 118) + * + * 时间复杂度:O(n²) + * - n是行数 + * - 需要生成n行,第i行有i个元素,总共约n²/2个元素 + * + * 空间复杂度:O(n²) + * - 需要存储杨辉三角的所有元素 + */ +public class GenerateTriangle118 { + + /** + * 主函数:处理用户输入并生成杨辉三角 + * + * 算法流程: + * 1. 读取用户输入的行数 + * 2. 调用 [generate](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/GenerateTriangle118.java#L77-L107)方法生成杨辉三角 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入杨辉三角的行数:"); + int numRows = scanner.nextInt(); + + List> triangle = generate(numRows); + System.out.println("杨辉三角的前 " + numRows + " 行:"); + for (List row : triangle) { + System.out.println(row); + } + } + + /** + * 生成杨辉三角的前numRows行 + * + * 算法思路: + * 杨辉三角的性质: + * 1. 每行的第一个和最后一个元素都是1 + * 2. 中间的元素等于其上方两个元素之和 + * + * 执行过程分析(以numRows=5为例): + * + * 第1行:1 + * 第2行:1 1 + * 第3行:1 2 1 + * 第4行:1 3 3 1 + * 第5行:1 4 6 4 1 + * + * 构造过程详解: + * i=0: row=[1] (j=0,边界情况) + * triangle=[[1]] + * + * i=1: row=[1,1] (j=0和j=1,都是边界情况) + * triangle=[[1], [1,1]] + * + * i=2: + * j=0: row=[1] (边界情况) + * j=1: row=[1,2] (triangle[1][0]+triangle[1][1] = 1+1 = 2) + * j=2: row=[1,2,1] (边界情况) + * triangle=[[1], [1,1], [1,2,1]] + * + * i=3: + * j=0: row=[1] (边界情况) + * j=1: row=[1,3] (triangle[2][0]+triangle[2][1] = 1+2 = 3) + * j=2: row=[1,3,3] (triangle[2][1]+triangle[2][2] = 2+1 = 3) + * j=3: row=[1,3,3,1] (边界情况) + * triangle=[[1], [1,1], [1,2,1], [1,3,3,1]] + * + * i=4: + * j=0: row=[1] (边界情况) + * j=1: row=[1,4] (triangle[3][0]+triangle[3][1] = 1+3 = 4) + * j=2: row=[1,4,6] (triangle[3][1]+triangle[3][2] = 3+3 = 6) + * j=3: row=[1,4,6,4] (triangle[3][2]+triangle[3][3] = 3+1 = 4) + * j=4: row=[1,4,6,4,1] (边界情况) + * triangle=[[1], [1,1], [1,2,1], [1,3,3,1], [1,4,6,4,1]] + * + * 时间复杂度分析: + * - 外层循环:O(numRows) + * - 内层循环:O(i)(第i行有i+1个元素) + * - 总时间复杂度:O(numRows²) + * + * 空间复杂度分析: + * - 存储杨辉三角:O(numRows²) + * + * @param numRows 杨辉三角的行数 + * @return 杨辉三角的前numRows行 + */ + public static List> generate(int numRows) { + // 存储整个杨辉三角 + List> triangle = new ArrayList<>(); + + // 逐行生成杨辉三角 + for (int i = 0; i < numRows; i++) { + // 存储当前行的元素 + List row = new ArrayList<>(); + + // 生成当前行的元素 + for (int j = 0; j <= i; j++) { + // 每行的第一个和最后一个元素都是 1 + if (j == 0 || j == i) { + row.add(1); + } else { + // 当前元素为上方两个元素之和 + // triangle.get(i-1).get(j-1) 是左上方元素 + // triangle.get(i-1).get(j) 是右上方元素 + int value = triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j); + row.add(value); + } + } + + // 添加当前行到三角形中 + triangle.add(row); + } + + return triangle; + } + + /** + * 扩展方法:生成杨辉三角的第rowIndex行(从0开始) + * + * 算法思路: + * 利用组合数公式:第n行第k个元素是C(n,k) = n!/(k!(n-k)!) + * + * 时间复杂度分析: + * - 计算每行元素:O(rowIndex) + * - 总时间复杂度:O(rowIndex²) + * + * 空间复杂度分析: + * - 存储一行元素:O(rowIndex) + * + * @param rowIndex 行索引(从0开始) + * @return 杨辉三角的第rowIndex行 + */ + public List getRow(int rowIndex) { + List row = new ArrayList<>(); + + // 第rowIndex行有rowIndex+1个元素 + for (int i = 0; i <= rowIndex; i++) { + // 计算组合数C(rowIndex, i) + long value = 1; + for (int j = 0; j < i; j++) { + value = value * (rowIndex - j) / (j + 1); + } + row.add((int) value); + } + + return row; + } + + /** + * 扩展方法:优化空间复杂度的生成方法 + * + * 算法思路: + * 每次只保留前一行的数据,而不是存储所有行的数据 + * + * 时间复杂度分析: + * - 生成每行:O(i)(第i行有i+1个元素) + * - 总时间复杂度:O(numRows²) + * + * 空间复杂度分析: + * - 存储杨辉三角:O(numRows²) + * + * @param numRows 杨辉三角的行数 + * @return 杨辉三角的前numRows行 + */ + public List> generateOptimized(int numRows) { + List> triangle = new ArrayList<>(); + + if (numRows == 0) return triangle; + + // 第一行 + List firstRow = new ArrayList<>(); + firstRow.add(1); + triangle.add(firstRow); + + // 生成其余行 + for (int i = 1; i < numRows; i++) { + List prevRow = triangle.get(i - 1); + List newRow = new ArrayList<>(); + newRow.add(1); // 每行第一个元素 + + // 计算中间元素 + for (int j = 1; j < i; j++) { + newRow.add(prevRow.get(j - 1) + prevRow.get(j)); + } + + newRow.add(1); // 每行最后一个元素 + triangle.add(newRow); + } + + return triangle; + } +} diff --git a/algorithm/GetIntersectionNode160.java b/algorithm/GetIntersectionNode160.java new file mode 100644 index 0000000000000..588ed5439d4c4 --- /dev/null +++ b/algorithm/GetIntersectionNode160.java @@ -0,0 +1,324 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashSet; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +/** + * 相交链表(LeetCode 160) + * + * 时间复杂度:O(m + n) + * - 需要遍历两个链表各一次 + * - 最坏情况下两个指针都要走完两个链表 + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用与链表长度相关的额外存储空间 + */ +public class GetIntersectionNode160 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + + ListNode(int x) { + val = x; + next = null; + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 提示用户输入链表A + System.out.println("请输入链表A的节点数:"); + // 读取链表A的节点数 + int aLength = scanner.nextInt(); + // 链表A的头节点 + ListNode headA = null; + // 链表A的当前节点 + ListNode currentA = null; + + System.out.println("请输入链表A的节点值:"); + // 读取链表A的节点值 + for (int i = 0; i < aLength; i++) { + // 读取节点值 + int val = scanner.nextInt(); + // 创建新节点 + ListNode node = new ListNode(val); + // 如果是第一个节点 + if (headA == null) { + headA = node; + currentA = node; + } else { + // 连接节点 + currentA.next = node; + currentA = node; + } + } + + // 提示用户输入链表B + System.out.println("请输入链表B的节点数:"); + // 读取链表B的节点数 + int bLength = scanner.nextInt(); + // 链表B的头节点 + ListNode headB = null; + // 链表B的当前节点 + ListNode currentB = null; + + System.out.println("请输入链表B的节点值:"); + // 读取链表B的节点值 + for (int i = 0; i < bLength; i++) { + // 读取节点值 + int val = scanner.nextInt(); + // 创建新节点 + ListNode node = new ListNode(val); + // 如果是第一个节点 + if (headB == null) { + headB = node; + currentB = node; + } else { + // 连接节点 + currentB.next = node; + currentB = node; + } + } + + // 输入相交节点位置(可选) + System.out.println("请输入链表A中相交节点的位置(从0开始,-1表示无相交):"); + // 读取相交节点位置 + int intersectPos = scanner.nextInt(); + + // 创建相交节点 + if (intersectPos >= 0) { + // 找到链表A的相交节点 + ListNode intersectNode = headA; + // 遍历到相交节点位置 + for (int i = 0; i < intersectPos && intersectNode != null; i++) { + intersectNode = intersectNode.next; + } + + // 将链表B的尾部连接到相交节点 + if (intersectNode != null) { + // 找到链表B的尾节点 + ListNode tailB = headB; + while (tailB.next != null) { + tailB = tailB.next; + } + // 连接链表B的尾节点到链表A的相交节点 + tailB.next = intersectNode; + } + } + + // 调用 getIntersectionNode 方法查找相交节点 + ListNode result = getIntersectionNode(headA, headB); + + // 输出结果 + if (result != null) { + // 打印相交节点值 + System.out.println("两个链表相交于节点值: " + result.val); + } else { + // 打印无相交信息 + System.out.println("两个链表不相交"); + } + } + + /** + * 找到两个单链表相交的起始节点 + * + * 算法思路: + * 使用双指针技巧,让两个指针分别遍历两个链表 + * 当一个指针到达链表末尾时,让它从另一个链表的头部开始 + * 这样两个指针最终会在相交节点相遇,或者同时到达末尾(无相交) + * + * 核心原理: + * 假设链表A长度为a+c,链表B长度为b+c(c为相交部分长度) + * 指针A走过的路径:a+c+b + * 指针B走过的路径:b+c+a + * 两者相等,如果存在相交节点,必然在相交节点相遇 + * + * 示例过程(链表A: 4->1->8->4->5, 链表B: 5->6->1->8->4->5): + * + * 初始状态: + * 指针A: 4->1->8->4->5->5->6->1->8->4->5 (到达末尾后从B开始) + * 指针B: 5->6->1->8->4->5->4->1->8->4->5 (到达末尾后从A开始) + * + * 在第8个节点处相遇,即相交节点 + * + * 时间复杂度分析: + * - 遍历两个链表:O(m + n),其中m为链表A的长度,n为链表B的长度 + * - 最坏情况下两个指针都要走完两个链表 + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * - 没有使用与链表长度相关的额外存储空间 + * + * @param headA 链表A的头节点 + * @param headB 链表B的头节点 + * @return 如果存在相交节点,返回相交节点;否则返回null + */ + public static ListNode getIntersectionNode(ListNode headA, ListNode headB) { + // 边界条件检查 + if (headA == null || headB == null) { + return null; + } + + // 初始化两个指针 + ListNode pointerA = headA; + ListNode pointerB = headB; + + // 当两个指针不相等时继续循环 + while (pointerA != pointerB) { + // 如果指针A到达链表末尾,则从链表B的头部开始 + if (pointerA == null) { + pointerA = headB; + } else { + pointerA = pointerA.next; + } + + // 如果指针B到达链表末尾,则从链表A的头部开始 + if (pointerB == null) { + pointerB = headA; + } else { + pointerB = pointerB.next; + } + } + + // 返回相交节点(如果无相交则返回null) + return pointerA; + } + + /** + * 方法2:计算长度差法 + * + * 算法思路: + * 1. 计算两个链表的长度 + * 2. 让较长链表的指针先走长度差步 + * 3. 两个指针同时移动,直到相遇 + * + * 示例过程(链表A: 4->1->8->4->5 (长度5), 链表B: 5->6->1->8->4->5 (长度6)): + * + * 1. 计算长度: lenA=5, lenB=6 + * 2. 链表B较长,长度差为1,让currentB先走1步 + * currentB指向6节点 + * 3. 两个指针同时移动: + * currentA=1, currentB=1 + * currentA=8, currentB=8 + * currentA=4, currentB=4 + * currentA=5, currentB=5 + * 两指针相遇,返回相交节点 + * + * 时间复杂度分析: + * - 计算链表长度:O(m + n),其中m为链表A的长度,n为链表B的长度 + * - 指针移动:O(max(m, n)) + * - 总时间复杂度:O(m + n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * + * @param headA 链表A的头节点 + * @param headB 链表B的头节点 + * @return 如果存在相交节点,返回相交节点;否则返回null + */ + public static ListNode getIntersectionNodeByLength(ListNode headA, ListNode headB) { + if (headA == null || headB == null) { + return null; + } + + // 计算链表A的长度 + int lenA = 0; + ListNode currentA = headA; + while (currentA != null) { + lenA++; + currentA = currentA.next; + } + + // 计算链表B的长度 + int lenB = 0; + ListNode currentB = headB; + while (currentB != null) { + lenB++; + currentB = currentB.next; + } + + // 重置指针 + currentA = headA; + currentB = headB; + + // 让较长链表的指针先走长度差步 + if (lenA > lenB) { + for (int i = 0; i < lenA - lenB; i++) { + currentA = currentA.next; + } + } else { + for (int i = 0; i < lenB - lenA; i++) { + currentB = currentB.next; + } + } + + // 两个指针同时移动,直到相遇 + while (currentA != currentB) { + currentA = currentA.next; + currentB = currentB.next; + } + + return currentA; + } + + /** + * 方法3:使用HashSet + * + * 算法思路: + * 1. 遍历链表A,将所有节点存入HashSet + * 2. 遍历链表B,检查节点是否在HashSet中 + * + * 示例过程(链表A: 4->1->8->4->5, 链表B: 5->6->1->8->4->5): + * + * 1. 遍历链表A,将节点[4,1,8,4,5]存入HashSet + * 2. 遍历链表B: + * 检查节点5: 不在HashSet中 + * 检查节点6: 不在HashSet中 + * 检查节点1: 在HashSet中,返回该节点 + * + * 时间复杂度分析: + * - 遍历链表A:O(m),其中m为链表A的长度 + * - 遍历链表B:O(n),其中n为链表B的长度 + * - HashSet操作:O(1) + * - 总时间复杂度:O(m + n) + * + * 空间复杂度分析: + * - HashSet存储链表A的所有节点:O(m) + * + * @param headA 链表A的头节点 + * @param headB 链表B的头节点 + * @return 如果存在相交节点,返回相交节点;否则返回null + */ + public static ListNode getIntersectionNodeByHashSet(ListNode headA, ListNode headB) { + if (headA == null || headB == null) { + return null; + } + + // 使用HashSet存储链表A的所有节点 + java.util.Set visited = new java.util.HashSet<>(); + ListNode current = headA; + while (current != null) { + visited.add(current); + current = current.next; + } + + // 遍历链表B,检查节点是否在HashSet中 + current = headB; + while (current != null) { + if (visited.contains(current)) { + return current; + } + current = current.next; + } + + return null; + } +} diff --git a/algorithm/GroupAnagrams49.java b/algorithm/GroupAnagrams49.java new file mode 100644 index 0000000000000..8e43259f65739 --- /dev/null +++ b/algorithm/GroupAnagrams49.java @@ -0,0 +1,253 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 字母分组(LeetCode 49) + * + * 时间复杂度:O(N * K * log K) + * - N 是字符串数组的长度 + * - K 是字符串的最大长度 + * - 对每个字符串进行排序需要 O(K * log K) 时间 + * - 总共需要处理 N 个字符串 + * + * 空间复杂度:O(N * K) + * - HashMap 存储所有字符串需要 O(N * K) 空间 + * - 排序时创建的字符数组需要 O(K) 空间 + * - 返回结果列表需要 O(N * K) 空间 + */ +public class GroupAnagrams49 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入字符串 + System.out.println("输入字符串:"); + + // 读取一行输入 + // 例如用户输入:"eat tea tan ate nat bat" + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + // line.split(" ") 将 "eat tea tan ate nat bat" 分割为 ["eat", "tea", "tan", "ate", "nat", "bat"] + String[] strs = line.split(" "); + + // 调用 groupAnagrams 方法对字母异位词进行分组 + List> result = groupAnagrams(strs); + + // 输出分组结果 + System.out.println("分组后的结果为:"); + + // 遍历并打印每个分组 + for (List group : result) { + System.out.println(group); + } + } + + /** + * 字母异位词分组 + * 将字符串数组中互为字母异位词的字符串分到同一组 + * 字母异位词是指两个字符串包含相同的字符,但字符的顺序可能不同 + * + * 算法思路: + * 1. 对每个字符串的字符进行排序,异位词排序后结果相同 + * 2. 使用排序后的字符串作为键,将原字符串分组 + * 3. 返回所有分组 + * + * 示例过程(以数组 ["eat","tea","tan","ate","nat","bat"] 为例): + * + * 字符串 排序后 分组过程 + * "eat" "aet" map={"aet": ["eat"]} + * "tea" "aet" map={"aet": ["eat","tea"]} + * "tan" "ant" map={"aet": ["eat","tea"], "ant": ["tan"]} + * "ate" "aet" map={"aet": ["eat","tea","ate"], "ant": ["tan"]} + * "nat" "ant" map={"aet": ["eat","tea","ate"], "ant": ["tan","nat"]} + * "bat" "abt" map={"aet": ["eat","tea","ate"], "ant": ["tan","nat"], "abt": ["bat"]} + * + * 时间复杂度分析: + * - 外层循环遍历N个字符串:O(N),其中N为输入字符串数组strs的长度 + * - 对每个字符串排序:O(K * log K),其中K是单个字符串的最大长度 + * - 哈希表操作(插入、查找):O(1)平均时间 + * - 总时间复杂度:O(N * K * log K) + * + * 空间复杂度分析: + * - HashMap存储所有字符串:O(N * K),其中N为字符串数量,K为平均字符串长度 + * - 字符数组存储:O(K),用于排序时的临时存储 + * - 返回结果列表:O(N * K),存储所有分组结果 + * - 总空间复杂度:O(N * K) + * + * @param strs 输入的字符串数组 + * @return 分组后的字母异位词列表 + */ + public static List> groupAnagrams(String[] strs) { + // 使用哈希表存储分组结果,键为排序后的字符串,值为该组的所有原始字符串 + Map> map = new HashMap<>(); + + // 遍历每个字符串 + for (String str : strs) { + // 将字符串转换为字符数组 + char[] chars = str.toCharArray(); + + // 对字符数组进行排序,使得异位词具有相同的排序结果 + Arrays.sort(chars); + + // 将排序后的字符数组转换为字符串作为键 + String key = new String(chars); + + // 如果该键不存在,则创建新的列表 + if (!map.containsKey(key)) { + map.put(key, new ArrayList<>()); + } + + // 将原始字符串添加到对应键的列表中 + map.get(key).add(str); + } + + // 返回所有分组的列表 + return new ArrayList<>(map.values()); + } + + + /** + * 方法2:使用字符计数作为键(避免排序) + * + * 算法思路: + * 1. 对每个字符串统计每个字符的出现次数 + * 2. 将字符计数数组转换为唯一字符串作为键 + * 3. 使用该键进行分组 + * + * 示例过程(以数组 ["eat","tea","tan","ate","nat","bat"] 为例): + * + * 字符串 字符计数 键字符串 分组过程 + * "eat" [1,0,0,0,1,0,...,1] "#1#0#0#0#1#0...#1" map={key: ["eat"]} + * "tea" [1,0,0,0,1,0,...,1] "#1#0#0#0#1#0...#1" map={key: ["eat","tea"]} + * "tan" [1,0,0,0,0,0,...,2] "#1#0#0#0#0#0...#2" map={key1: ["eat","tea"], key2: ["tan"]} + * + * 时间复杂度分析: + * - 外层循环遍历N个字符串:O(N),其中N为输入字符串数组strs的长度 + * - 对每个字符串统计字符:O(K),其中K为单个字符串的最大长度 + * - 构造键字符串:O(26) = O(1),固定26个小写字母 + * - 哈希表操作:O(1)平均时间 + * - 总时间复杂度:O(N * K) + * + * 空间复杂度分析: + * - HashMap存储所有字符串:O(N * K),其中N为字符串数量,K为平均字符串长度 + * - 计数数组:O(26) = O(1),固定大小的字符计数数组 + * - StringBuilder存储键字符串:O(26) = O(1),用于构建键字符串 + * - 返回结果列表:O(N * K),存储所有分组结果 + * - 总空间复杂度:O(N * K) + * + * @param strs 输入的字符串数组 + * @return 分组后的字母异位词列表 + */ + public static List> groupAnagramsCount(String[] strs) { + // 使用哈希表存储分组结果,键为字符计数字符串,值为该组的所有原始字符串 + Map> map = new HashMap<>(); + + // 遍历每个字符串 + for (String str : strs) { + // 统计每个字符的出现次数 + // 创建长度为26的数组,对应26个小写字母a-z + int[] count = new int[26]; // 初始化为[0,0,0,...,0] + + // 遍历字符串中的每个字符 + for (char c : str.toCharArray()) { + // 统计字符出现次数 + // c - 'a' 将字符转换为索引('a'->0, 'b'->1, ..., 'z'->25) + count[c - 'a']++; // 对应字符计数加1 + } + + // 将计数数组转换为字符串作为键 + // 使用StringBuilder提高字符串拼接效率 + StringBuilder sb = new StringBuilder(); + + // 遍历26个字母的计数 + for (int i = 0; i < 26; i++) { + // 添加分隔符避免计数混淆(例如计数10和1,0的区别) + sb.append('#'); + // 添加当前字母的计数 + sb.append(count[i]); + } + + // 将StringBuilder转换为字符串作为哈希表的键 + String key = sb.toString(); + + // 分组逻辑同上:如果键不存在则创建新列表,然后添加字符串 + if (!map.containsKey(key)) { + map.put(key, new ArrayList<>()); + } + // map.get(key).add(str) 添加字符串到对应列表 + map.get(key).add(str); + } + + // 返回所有分组的列表 + return new ArrayList<>(map.values()); + } + + /** + * 方法3:使用质数乘积作为键 + * + * 算法思路: + * 1. 为每个字母分配一个唯一的质数 + * 2. 计算每个字符串中所有字符对应质数的乘积 + * 3. 异位词具有相同的字符组成,因此乘积相同 + * 4. 使用乘积作为键进行分组 + * + * 数学原理: + * 根据算术基本定理,每个大于1的自然数都可以唯一地分解为质数的乘积 + * 因此,相同字符组成的字符串(异位词)会产生相同的乘积 + * + * 示例过程(以数组 ["eat","tea","tan","ate","nat","bat"] 为例): + * + * 字符串 字符对应质数 乘积 分组过程 + * "eat" 2(e) * 3(a) * 5(t) 30 map={30: ["eat"]} + * "tea" 5(t) * 2(e) * 3(a) 30 map={30: ["eat","tea"]} + * "tan" 5(t) * 3(a) * 7(n) 105 map={30: ["eat","tea"], 105: ["tan"]} + * + * 时间复杂度分析: + * - 外层循环遍历N个字符串:O(N),其中N为输入字符串数组strs的长度 + * - 对每个字符串计算乘积:O(K),其中K为单个字符串的最大长度 + * - 哈希表操作:O(1)平均时间 + * - 总时间复杂度:O(N * K) + * + * 空间复杂度分析: + * - HashMap存储所有字符串:O(N * K),其中N为字符串数量,K为平均字符串长度 + * - 质数数组:O(26) = O(1),存储26个质数的固定数组 + * - 返回结果列表:O(N * K),存储所有分组结果 + * - 总空间复杂度:O(N * K) + * + * @param strs 输入的字符串数组 + * @return 分组后的字母异位词列表 + */ + public static List> groupAnagramsPrime(String[] strs) { + // 26个质数对应26个小写字母 a-z + // 质数的选择确保了不同字符组合的唯一性 + int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101}; + + // 使用哈希表存储分组结果,键为质数乘积,值为该组的所有原始字符串 + // 使用Long类型避免整数溢出 + Map> map = new HashMap<>(); + + // 遍历每个字符串 + for (String str : strs) { + // 初始化乘积为1(乘法的单位元) + long key = 1; + + // 计算字符串的质数乘积 + // 遍历字符串中的每个字符 + for (char c : str.toCharArray()) { + // 计算字符对应的质数乘积 + key *= primes[c - 'a']; + } + + // 分组逻辑:如果键不存在则创建新列表,然后添加字符串 + if (!map.containsKey(key)) { + map.put(key, new ArrayList<>()); + } + map.get(key).add(str); + } + + // 返回所有分组的列表 + return new ArrayList<>(map.values()); + } +} diff --git a/algorithm/HaveCycleList141.java b/algorithm/HaveCycleList141.java new file mode 100644 index 0000000000000..1954568217ccf --- /dev/null +++ b/algorithm/HaveCycleList141.java @@ -0,0 +1,247 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashSet; +import java.util.Set; + +/** + * 环形链表检测算法(LeetCode 141) + * + * 时间复杂度: + * - 方法1(哈希表):O(n) + * 每个节点最多访问一次,哈希表查找和插入操作平均时间复杂度为O(1) + * - 方法2(快慢指针):O(n) + * 在最坏情况下,快指针需要遍历整个链表一次 + * + * 空间复杂度: + * - 方法1(哈希表):O(n) + * 需要存储所有访问过的节点 + * - 方法2(快慢指针):O(1) + * 只使用了常数个额外变量 + */ +public class HaveCycleList141 { + + // 定义链表节点类 + static class ListNode { + int val; + ListNode next; + ListNode(int x) { + val = x; + next = null; + } + } + + /** + * 方法1:使用哈希表检测环 + * + * 算法思路: + * 遍历链表,将访问过的节点存储在哈希表中 + * 如果遇到已经存在于哈希表中的节点,说明有环 + * 如果遍历到null,说明无环 + * + * 执行过程分析(以链表 1->2->3->4->2 为例,4指向2形成环): + * 1. 访问节点1,哈希表:{1} + * 2. 访问节点2,哈希表:{1,2} + * 3. 访问节点3,哈希表:{1,2,3} + * 4. 访问节点4,哈希表:{1,2,3,4} + * 5. 再次访问节点2,发现已存在于哈希表中,返回true + * + * 时间复杂度分析: + * - 遍历链表:O(n),其中n为链表节点数 + * - HashSet操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashSet存储节点:O(n) + * + * @param head 链表的头节点 + * @return 如果有环返回true,否则返回false + */ + public boolean hasCycleWithHashSet(ListNode head) { + // 使用HashSet存储访问过的节点 + Set visitedNodes = new HashSet<>(); + + // 从头节点开始遍历 + ListNode current = head; + while (current != null) { + // 如果当前节点已在集合中,说明有环 + if (visitedNodes.contains(current)) { + return true; + } + + // 将当前节点加入集合 + visitedNodes.add(current); + + // 移动到下一个节点 + current = current.next; + } + + // 遍历完成未发现环,返回false + return false; + } + + /** + * 方法2:使用快慢指针(Floyd判圈算法) + * + * 算法思路: + * 使用两个指针,快指针每次移动两步,慢指针每次移动一步 + * 如果链表有环,快指针最终会追上慢指针 + * 如果链表无环,快指针会先到达链表末尾 + * + * 执行过程分析(以链表 1->2->3->4->2 为例,4指向2形成环): + * 初始:slow=1, fast=1 + * 第1步:slow=2, fast=3 + * 第2步:slow=3, fast=2 (环中) + * 第3步:slow=4, fast=4 (相遇,返回true) + * + * 数学原理: + * 假设链表头到环入口距离为a,环长度为b + * 当慢指针进入环时,快指针已在环中 + * 快指针追赶慢指针,每次缩短1步距离 + * 最多经过b步后两者相遇 + * + * 时间复杂度分析: + * - 快指针遍历链表:O(n),其中n为链表节点数 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用两个指针变量:O(1) + * + * @param head 链表的头节点 + * @return 如果有环返回true,否则返回false + */ + public boolean hasCycleWithTwoPointers(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) { + HaveCycleList141 solution = new HaveCycleList141(); + + // 创建无环链表: 1->2->3->null + ListNode head1 = new ListNode(1); + head1.next = new ListNode(2); + head1.next.next = new ListNode(3); + + // 创建有环链表: 1->2->3->4->2 (4指向2) + ListNode head2 = new ListNode(1); + head2.next = new ListNode(2); + head2.next.next = new ListNode(3); + head2.next.next.next = new ListNode(4); + head2.next.next.next.next = head2.next; + + // 测试无环链表 + System.out.println("无环链表检测结果(哈希表): " + solution.hasCycleWithHashSet(head1)); + System.out.println("无环链表检测结果(快慢指针): " + solution.hasCycleWithTwoPointers(head1)); + + // 测试有环链表 + System.out.println("有环链表检测结果(哈希表): " + solution.hasCycleWithHashSet(head2)); + System.out.println("有环链表检测结果(快慢指针): " + solution.hasCycleWithTwoPointers(head2)); + } + + /** + * 方法3:修改节点值标记法(破坏性方法) + * + * 算法思路: + * 遍历链表,将访问过的节点值修改为特殊标记值 + * 如果遇到已被标记的节点,说明有环 + * + * 注意:这种方法会修改原链表的节点值,实际应用中不推荐 + * + * 执行过程分析(以链表 1->2->3->4->2 为例,4指向2形成环): + * 1. 访问节点1,标记为MAX_VALUE,链表变为 MAX_VALUE->2->3->4->2 + * 2. 访问节点2,标记为MAX_VALUE,链表变为 MAX_VALUE->MAX_VALUE->3->4->2 + * 3. 访问节点3,标记为MAX_VALUE,链表变为 MAX_VALUE->MAX_VALUE->MAX_VALUE->4->2 + * 4. 访问节点4,标记为MAX_VALUE,链表变为 MAX_VALUE->MAX_VALUE->MAX_VALUE->MAX_VALUE->2 + * 5. 再次访问节点2,发现值为MAX_VALUE,返回true + * + * 时间复杂度分析: + * - 遍历链表:O(n),其中n为链表节点数 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param head 链表的头节点 + * @return 如果有环返回true,否则返回false + */ + public boolean hasCycleByMarking(ListNode head) { + // 特殊标记值,需要确保不会与原链表中的值冲突 + final int MARK = Integer.MAX_VALUE; + + ListNode current = head; + while (current != null) { + // 如果当前节点已被标记,说明有环 + if (current.val == MARK) { + return true; + } + + // 标记当前节点 + current.val = MARK; + + // 移动到下一个节点 + current = current.next; + } + + // 遍历完成未发现环,返回false + return false; + } + + /** + * 方法4:使用异常处理检测环(投机取巧) + * + * 算法思路: + * 利用链表有环时toString()方法会产生StackOverflowError的特性 + * + * 注意:这种方法不推荐在生产环境中使用 + * + * 执行过程分析: + * 1. 对于无环链表:toString()方法正常执行,返回false + * 2. 对于有环链表:toString()方法递归调用导致栈溢出,捕获异常返回true + * + * 时间复杂度分析: + * - 无环情况:O(n),其中n为链表节点数 + * - 有环情况:O(1),发生栈溢出立即返回 + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * - 有环时递归调用栈:O(n) + * + * @param head 链表的头节点 + * @return 如果有环返回true,否则返回false + */ + public boolean hasCycleByToString(ListNode head) { + try { + // 尝试调用toString方法,有环时会抛出StackOverflowError + head.toString(); + return false; + } catch (StackOverflowError e) { + // 捕获到栈溢出错误,说明有环 + return true; + } + } +} diff --git a/algorithm/InOrderTraversal94.java b/algorithm/InOrderTraversal94.java new file mode 100644 index 0000000000000..4dc22e4cd2260 --- /dev/null +++ b/algorithm/InOrderTraversal94.java @@ -0,0 +1,352 @@ +package com.funian.algorithm.algorithm; + +import java.rmi.server.RemoteRef; +import java.util.*; + +/** + * 二叉树的中序遍历(LeetCode 94) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度: + * - 递归解法:O(h) + * h是二叉树的高度,递归调用栈的深度 + * 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + * - 迭代解法:O(h) + * 显式栈最多存储h个节点 + */ +public class InOrderTraversal94 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int val) { this.val = val; } + } + + /** + * 方法1:递归解法 + * + * 算法思路: + * 中序遍历的顺序是:左子树 -> 根节点 -> 右子树 + * 使用递归实现,先递归遍历左子树,再访问根节点,最后递归遍历右子树 + * + * 执行过程分析(以二叉树 [1,null,2,3] 为例): + * + * 1 + * \ + * 2 + * / + * 3 + * + * 递归调用过程: + * inorderTraversal(1) + * ├─ inorderTraversal(null) -> 返回 [] + * ├─ 访问节点1 -> result=[1] + * └─ inorderTraversal(2) + * ├─ inorderTraversal(3) + * │ ├─ inorderTraversal(null) -> 返回 [] + * │ ├─ 访问节点3 -> result=[1,3] + * │ └─ inorderTraversal(null) -> 返回 [] + * ├─ 访问节点2 -> result=[1,3,2] + * └─ inorderTraversal(null) -> 返回 [] + * + * 最终结果:[1,3,2] + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 中序遍历的结果列表 + */ + public List inorderTraversalRecursive(TreeNode root) { + // 创建结果列表 + List result = new ArrayList<>(); + // 调用递归辅助方法 + inorderHelper(root, result); + // 返回结果列表 + return result; + } + + /** + * 递归辅助方法 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param root 当前节点 + * @param result 结果列表 + */ + private void inorderHelper(TreeNode root, List result) { + // 基础情况:如果节点为空,直接返回 + if (root == null) { + return; + } + + // 递归遍历左子树 + inorderHelper(root.left, result); + + // 访问根节点 + result.add(root.val); + + // 递归遍历右子树 + inorderHelper(root.right, result); + } + + /** + * 方法2:迭代解法 + * + * 算法思路: + * 使用显式栈模拟递归过程 + * 1. 从根节点开始,沿着左子树一直向下,将路径上的节点入栈 + * 2. 弹出栈顶节点并访问,然后处理其右子树 + * 3. 重复上述过程直到栈为空且当前节点为空 + * + * 执行过程分析(以二叉树 [1,null,2,3] 为例): + * + * 1 + * \ + * 2 + * / + * 3 + * + * 执行步骤: + * 1. current=1, stack=[] + * 1->left=null,所以弹出1,result=[1],current=1->right=2 + * + * 2. current=2, stack=[] + * 2->left=3,入栈,stack=[2],current=3 + * + * 3. current=3, stack=[2] + * 3->left=null,所以弹出3,result=[1,3],current=3->right=null + * + * 4. current=null, stack=[2] + * 弹出2,result=[1,3,2],current=2->right=null + * + * 5. current=null, stack=[] + * 循环结束 + * + * 最终结果:[1,3,2] + * + * 时间复杂度分析: + * - 每个节点入栈和出栈一次:O(n),其中n为节点数 + * + * 空间复杂度分析: + * - 栈最多存储树的高度个节点:O(h) + * - 最坏情况:O(n),最好情况:O(log n) + * + * @param root 二叉树的根节点 + * @return 中序遍历的结果列表 + */ + public List inorderTraversalIterative(TreeNode root) { + // 创建结果列表 + List result = new ArrayList<>(); + // 创建栈用于模拟递归 + Stack stack = new Stack<>(); + // current 当前节点指针,初始指向根节点 + 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; + } + + /** + * 方法3:Morris遍历(线索二叉树) + * + * 算法思路: + * 利用叶子节点的空指针建立线索,避免使用栈或递归 + * 1. 如果当前节点无左子树,访问该节点并移向右子树 + * 2. 如果当前节点有左子树,找到其在中序遍历中的前驱节点 + * - 如果前驱节点的右指针为空,将其指向当前节点,然后移向左子树 + * - 如果前驱节点的右指针指向当前节点,说明左子树已遍历完,断开连接,访问当前节点,移向右子树 + * + * 时间复杂度分析: + * - 每个节点最多被访问3次:O(n),其中n为节点数 + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param root 二叉树的根节点 + * @return 中序遍历的结果列表 + */ + public List inorderTraversalMorris(TreeNode root) { + // 创建结果列表 + List result = new ArrayList<>(); + // current 当前节点指针,初始指向根节点 + TreeNode current = root; + + while (current != null) { + if (current.left == null) { + // 如果没有左子树,访问当前节点并移向右子树 + result.add(current.val); + current = current.right; + } else { + // 找到中序遍历中的前驱节点 + TreeNode predecessor = current.left; + while (predecessor.right != null && predecessor.right != current) { + predecessor = predecessor.right; + } + + if (predecessor.right == null) { + // 建立线索 + predecessor.right = current; + current = current.left; + } else { + // 断开线索,访问节点,移向右子树 + predecessor.right = null; + result.add(current.val); + current = current.right; + } + } + } + + // 返回结果列表 + return result; + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * 注意:这里采用层序遍历的输入方式,null表示空节点 + */ + public TreeNode createTree(Scanner scanner) { + // 提示用户输入 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取一行输入 + String input = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] values = input.split(" "); + + if (values.length == 0 || "null".equals(values[0])) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于层序遍历 + Queue queue = new java.util.LinkedList<>(); + queue.offer(root); + + // i 数组索引 + int i = 1; + while (!queue.isEmpty() && i < values.length) { + TreeNode node = queue.poll(); + + // 处理左子节点 + if (i < values.length && !"null".equals(values[i])) { + node.left = new TreeNode(Integer.parseInt(values[i])); + queue.offer(node.left); + } + i++; + + // 处理右子节点 + if (i < values.length && !"null".equals(values[i])) { + node.right = new TreeNode(Integer.parseInt(values[i])); + queue.offer(node.right); + } + i++; + } + + // 返回根节点 + return root; + } + + /** + * 辅助方法:打印遍历结果 + */ + public void printTraversalResult(List result, String method) { + System.out.println(method + "遍历结果: " + result); + } + + /** + * 主函数:处理用户输入并演示中序遍历 + */ + public static void main(String[] args) { + // 创建解决方案实例 + InOrderTraversal94 solution = new InOrderTraversal94(); + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 创建二叉树 + TreeNode root = solution.createTree(scanner); + + if (root == null) { + System.out.println("创建的二叉树为空"); + return; + } + + System.out.println("二叉树创建成功"); + + // 演示三种遍历方法 + while (true) { + // 打印操作选项 + System.out.println("\n请选择遍历方法:"); + System.out.println("1. 递归解法"); + System.out.println("2. 迭代解法"); + System.out.println("3. Morris遍历"); + System.out.println("4. 退出"); + System.out.print("请输入选项(1-4): "); + + // 读取用户选择 + int choice = scanner.nextInt(); + scanner.nextLine(); + + List result; + // 根据用户选择执行相应操作 + switch (choice) { + case 1: + result = solution.inorderTraversalRecursive(root); + solution.printTraversalResult(result, "递归"); + break; + + case 2: + result = solution.inorderTraversalIterative(root); + solution.printTraversalResult(result, "迭代"); + break; + + case 3: + result = solution.inorderTraversalMorris(root); + solution.printTraversalResult(result, "Morris"); + break; + + case 4: + // 退出程序 + System.out.println("退出程序"); + scanner.close(); + return; + + default: + // 处理无效选项 + System.out.println("无效选项,请重新输入"); + } + } + } +} diff --git a/algorithm/InvertTree226.java b/algorithm/InvertTree226.java new file mode 100644 index 0000000000000..608c15cae31d6 --- /dev/null +++ b/algorithm/InvertTree226.java @@ -0,0 +1,345 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Queue; +import java.util.LinkedList; + +/** + * 翻转二叉树(LeetCode 226) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度: + * - 方法1(递归):O(h) + * h是二叉树的高度,递归调用栈的深度 + * 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + * - 方法2(迭代):O(w) + * w是二叉树的最大宽度,队列最多存储一层的所有节点 + */ +public class InvertTree226 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + /** + * 方法1:递归解法 + * + * 算法思路: + * 递归地翻转左右子树,然后交换当前节点的左右子树 + * + * 执行过程分析(以二叉树 [4,2,7,1,3,6,9] 为例): + * + * 翻转前: + * 4 + * / \ + * 2 7 + * / \ / \ + * 1 3 6 9 + * + * 翻转后: + * 4 + * / \ + * 7 2 + * / \ / \ + * 9 6 3 1 + * + * 递归调用过程: + * invertTree(4) + * ├─ invertTree(2) + * │ ├─ invertTree(1) -> 返回1 + * │ ├─ invertTree(3) -> 返回3 + * │ ├─ 交换2的左右子树:2.left=3, 2.right=1 + * │ └─ 返回2 + * ├─ invertTree(7) + * │ ├─ invertTree(6) -> 返回6 + * │ ├─ invertTree(9) -> 返回9 + * │ ├─ 交换7的左右子树:7.left=9, 7.right=6 + * │ └─ 返回7 + * ├─ 交换4的左右子树:4.left=7, 4.right=2 + * └─ 返回4 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 翻转后的二叉树根节点 + */ + public TreeNode invertTree(TreeNode root) { + // 基础情况:如果节点为空,直接返回 null + // 这是递归的终止条件 + if (root == null) { + return null; + } + + // 递归翻转左子树 + TreeNode left = invertTree(root.left); + + // 递归翻转右子树 + TreeNode right = invertTree(root.right); + + // 交换当前节点的左右子树 + root.right = left; + root.left = right; + + // 返回当前翻转后的根节点 + return root; + } + + /** + * 方法2:迭代解法(层序遍历) + * + * 算法思路: + * 使用队列进行层序遍历,对每个节点交换其左右子树 + * + * 执行过程分析(以二叉树 [4,2,7,1,3,6,9] 为例): + * + * 翻转前: + * 4 + * / \ + * 2 7 + * / \ / \ + * 1 3 6 9 + * + * 翻转过程: + * 1. 处理节点4:交换左右子树(2和7) -> 4的左子树为7,右子树为2 + * 2. 处理节点7:交换左右子树(6和9) -> 7的左子树为9,右子树为6 + * 3. 处理节点2:交换左右子树(1和3) -> 2的左子树为3,右子树为1 + * 4. 处理节点9、6、3、1:它们都是叶子节点,无需交换 + * + * 翻转后: + * 4 + * / \ + * 7 2 + * / \ / \ + * 9 6 3 1 + * + * 时间复杂度分析: + * - 每个节点入队和出队一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 最坏情况:O(n),最好情况:O(1) + * + * @param root 二叉树的根节点 + * @return 翻转后的二叉树根节点 + */ + public TreeNode invertTreeIterative(TreeNode root) { + // 如果根节点为空,直接返回null + if (root == null) { + return null; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 当队列不为空时继续处理 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 交换当前节点的左右子树 + // 使用临时变量保存左子树 + TreeNode temp = node.left; + // 将右子树赋值给左子树 + node.left = node.right; + // 将临时变量(原左子树)赋值给右子树 + node.right = temp; + + // 将非空的左子节点加入队列 + if (node.left != null) { + queue.offer(node.left); + } + // 将非空的右子节点加入队列 + if (node.right != null) { + queue.offer(node.right); + } + } + + // 返回翻转后的根节点 + return root; + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + * + * 示例:输入"4 2 7 1 3 6 9" + * 1. 创建根节点4 + * 2. 处理节点4的子节点:左子节点为2,右子节点为7 + * 3. 处理节点2的子节点:左子节点为1,右子节点为3 + * 4. 处理节点7的子节点:左子节点为6,右子节点为9 + * + * 构建完成的二叉树: + * 4 + * / \ + * 2 7 + * / \ / \ + * 1 3 6 9 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + * + * 示例:对于二叉树 + * 4 + * / \ + * 2 7 + * / \ / \ + * 1 3 6 9 + * + * 输出结果:4 2 7 1 3 6 9 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + // 打印当前节点的值 + System.out.print(node.val + " "); + + // 如果左子节点不为空,将其加入队列 + if (node.left != null) { + queue.offer(node.left); + } + // 如果右子节点不为空,将其加入队列 + if (node.right != null) { + queue.offer(node.right); + } + } + // 换行 + System.out.println(); + } + + /** + * 主函数:处理用户输入并演示翻转二叉树 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印翻转前的二叉树 + * 4. 调用翻转方法翻转二叉树 + * 5. 打印翻转后的二叉树 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("二叉树翻转演示"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出程序 + System.out.println("创建的二叉树为空"); + return; + } + + // 打印翻转前的二叉树 + System.out.println("翻转前的二叉树:"); + printLevelOrder(root); + + // 翻转二叉树 + // 创建解决方案实例 + InvertTree226 solution = new InvertTree226(); + // 调用invertTree方法翻转二叉树 + TreeNode invertedRoot = solution.invertTree(root); + + // 打印翻转后的二叉树 + System.out.println("翻转后的二叉树:"); + printLevelOrder(invertedRoot); + } +} diff --git a/algorithm/IsSymmetrix101.java b/algorithm/IsSymmetrix101.java new file mode 100644 index 0000000000000..52fce5d54e6ee --- /dev/null +++ b/algorithm/IsSymmetrix101.java @@ -0,0 +1,344 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Queue; +import java.util.LinkedList; + +/** + * 对称二叉树(LeetCode 101) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度: + * - 方法1(递归):O(h) + * h是二叉树的高度,递归调用栈的深度 + * 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + * - 方法2(迭代):O(w) + * w是二叉树的最大宽度,队列最多存储一层的所有节点 + */ +public class IsSymmetrix101 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + /** + * 方法1:递归解法 + * + * 算法思路: + * 一个二叉树是对称的,当且仅当它的左子树和右子树互为镜像 + * 两个树互为镜像的条件: + * 1. 它们的根节点值相同 + * 2. 每个树的右子树都与另一个树的左子树镜像对称 + * + * 执行过程分析(以对称二叉树 [1,2,2,3,4,4,3] 为例): + * + * 1 + * / \ + * 2 2 + * / \ / \ + * 3 4 4 3 + * + * 递归调用过程: + * isSymmetric(1) + * └─ isMirror(2, 2) + * ├─ 2.val == 2.val -> true + * ├─ isMirror(2.left=3, 2.right=3) + * │ ├─ 3.val == 3.val -> true + * │ ├─ isMirror(3.left=null, 3.right=null) -> true + * │ └─ isMirror(3.right=null, 3.left=null) -> true + * │ └─ 返回 true + * ├─ isMirror(2.right=4, 2.left=4) + * │ ├─ 4.val == 4.val -> true + * │ ├─ isMirror(4.left=null, 4.right=null) -> true + * │ └─ isMirror(4.right=null, 4.left=null) -> true + * │ └─ 返回 true + * └─ 返回 true + * + * 时间复杂度分析: + * - 每个节点最多访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 如果是对称二叉树返回true,否则返回false + */ + public boolean isSymmetric(TreeNode root) { + // 空树被认为是对称的 + if (root == null) { + return true; + } + + // 判断左子树和右子树是否互为镜像 + return isMirror(root.left, root.right); + } + + /** + * 辅助方法:判断两个树是否互为镜像 + * + * 算法思路: + * 两个树互为镜像需要满足: + * 1. 两个节点都为空(基础情况1) + * 2. 只有一个节点为空(基础情况2,返回false) + * 3. 节点值相等,且左树的左子树与右树的右子树镜像,左树的右子树与右树的左子树镜像 + * + * 时间复杂度分析: + * - 每对节点最多访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param t1 第一个树的根节点 + * @param t2 第二个树的根节点 + * @return 如果两个树互为镜像返回true,否则返回false + */ + private boolean isMirror(TreeNode t1, TreeNode t2) { + // 基础情况1:两个节点都为空,是对称的 + if (t1 == null && t2 == null) { + return true; + } + + // 基础情况2:只有一个节点为空,不是对称的 + if (t1 == null || t2 == null) { + return false; + } + + // 递归判断: + // 1. 当前节点值相等 + // 2. t1的左子树与t2的右子树互为镜像 + // 3. t1的右子树与t2的左子树互为镜像 + return (t1.val == t2.val) + && isMirror(t1.left, t2.right) + && isMirror(t1.right, t2.left); + } + + /** + * 方法2:迭代解法 + * + * 算法思路: + * 使用队列进行层序遍历,每次从队列中取出两个节点进行比较 + * 这两个节点应该是互为镜像位置的节点 + * + * 执行过程分析(以对称二叉树 [1,2,2,3,4,4,3] 为例): + * + * 1 + * / \ + * 2 2 + * / \ / \ + * 3 4 4 3 + * + * 执行步骤: + * 1. 队列=[2,2](根节点的左右子节点) + * 取出2和2:值相等,将(3,3)和(4,4)加入队列 + * + * 2. 队列=[3,3,4,4] + * 取出3和3:值相等,左右子节点都为空 + * + * 3. 队列=[4,4] + * 取出4和4:值相等,左右子节点都为空 + * + * 4. 队列=[] + * 返回true + * + * 时间复杂度分析: + * - 每个节点最多入队和出队一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 最坏情况:O(n),最好情况:O(1) + * + * @param root 二叉树的根节点 + * @return 如果是对称二叉树返回true,否则返回false + */ + public boolean isSymmetricIterative(TreeNode root) { + // 空树被认为是对称的 + if (root == null) { + return true; + } + + // 创建队列用于迭代比较 + Queue 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 || 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 TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并判断二叉树是否对称 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建的二叉树 + * 4. 调用两种方法判断是否对称 + * 5. 打印判断结果 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("对称二叉树判断"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息 + System.out.println("创建的二叉树为空,空树被认为是对称的"); + // 打印结果 + System.out.println("结果: true"); + // 退出程序 + return; + } + + // 打印创建的二叉树 + System.out.println("创建的二叉树:"); + printLevelOrder(root); + + // 判断是否对称 + // 创建解决方案实例 + IsSymmetrix101 solution = new IsSymmetrix101(); + boolean result1 = solution.isSymmetric(root); + boolean result2 = solution.isSymmetricIterative(root); + + // 打印结果 + System.out.println("递归方法判断结果: " + result1); + System.out.println("迭代方法判断结果: " + result2); + } +} diff --git a/algorithm/IsValid20.java b/algorithm/IsValid20.java new file mode 100644 index 0000000000000..d55e0363be7f5 --- /dev/null +++ b/algorithm/IsValid20.java @@ -0,0 +1,207 @@ +package com.funian.algorithm.algorithm; + +/** + * 有效的括号(LeetCode 20) + * + * 时间复杂度:O(n) + * - n是字符串长度 + * - 需要遍历字符串一次 + * + * 空间复杂度:O(n) + * - 最坏情况下栈中存储所有左括号 + */ +import java.util.Scanner; +import java.util.Stack; + +public class IsValid20 { + + /** + * 主函数:处理用户输入并判断括号字符串是否有效 + * + * 算法流程: + * 1. 读取用户输入的字符串 + * 2. 调用isValid方法判断字符串是否有效 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入字符串 + System.out.print("请输入字符串:"); + // 读取用户输入的字符串 + String s = scanner.nextLine(); + + // 调用 isValid 方法判断字符串是否合法 + // 调用isValid方法判断字符串是否有效 + boolean result = isValid(s); + + // 输出结果 + // 打印判断结果 + System.out.println("字符串是否有效:" + result); + + // 关闭 Scanner + // 关闭Scanner资源 + scanner.close(); + } + + /** + * 判断括号字符串是否有效 + * + * 算法思路: + * 使用栈数据结构,遵循"后进先出"的原则 + * 1. 遇到左括号时,将其压入栈中 + * 2. 遇到右括号时,检查栈顶是否为对应的左括号 + * 3. 如果匹配则弹出栈顶元素,否则返回false + * 4. 遍历结束后,如果栈为空则说明所有括号都匹配 + * + * @param s 输入的括号字符串 + * @return 如果字符串有效返回true,否则返回false + */ + public static boolean isValid(String s) { + // 创建栈用于存放左括号 + // 栈的特点:后进先出(LIFO) + // 创建字符栈用于存储左括号 + Stack stack = new Stack<>(); + + // 遍历字符串中的每一个字符 + // 遍历字符串中的每个字符 + for (char c : s.toCharArray()) { + // 如果是左括号,则压入栈中 + // 左括号需要等待对应的右括号来匹配 + // 判断是否为左括号 + if (c == '(' || c == '[' || c == '{') { + // 将左括号压入栈中 + stack.push(c); + } + // 如果是右括号 + // 需要检查是否有对应的左括号匹配 + // 判断是否为右括号 + else if (c == ')' || c == ']' || c == '}') { + // 如果栈为空,说明没有对应的左括号 + // 这是一个重要的边界条件检查 + // 检查栈是否为空 + if (stack.isEmpty()) { + // 栈为空说明没有匹配的左括号,返回false + return false; + } + + // 弹出栈顶的左括号 + // 这里体现了栈的LIFO特性 + // 弹出栈顶元素 + char top = stack.pop(); + + // 检查是否匹配 + // 每种右括号必须与对应的左括号匹配 + // 检查括号是否匹配 + if ((c == ')' && top != '(') || + (c == ']' && top != '[') || + (c == '}' && top != '{')) { + // 括号不匹配,返回false + return false; + } + } + } + + // 如果栈为空,则所有括号均已匹配,返回 true + // 否则说明还有未匹配的左括号,返回 false + // 这是另一个重要的边界条件检查 + // 检查栈是否为空,空则说明所有括号都匹配 + return stack.isEmpty(); + } + + /** + * 方法2:使用HashMap优化版本 + * + * 算法思路: + * 使用HashMap存储括号的对应关系,使代码更清晰 + * + * @param s 输入的括号字符串 + * @return 如果字符串有效返回true,否则返回false + */ + public boolean isValidHashMap(String s) { + // 检查字符串是否为空或长度为奇数 + if (s == null || s.length() % 2 != 0) return false; + + // 创建HashMap存储括号对应关系 + java.util.Map map = new java.util.HashMap<>(); + // 存储右括号')'对应的左括号'(' + map.put(')', '('); + // 存储右括号']'对应的左括号'[' + map.put(']', '['); + // 存储右括号'}'对应的左括号'{' + map.put('}', '{'); + + // 创建字符栈 + Stack stack = new Stack<>(); + + // 遍历字符串中的每个字符 + for (char c : s.toCharArray()) { + // 如果是右括号 + // 检查字符是否为右括号 + if (map.containsKey(c)) { + // 检查栈顶元素是否匹配 + // 获取栈顶元素,栈空时使用'#'占位符 + char top = stack.isEmpty() ? '#' : stack.pop(); + // 检查栈顶元素是否与右括号对应的左括号匹配 + if (top != map.get(c)) { + // 不匹配则返回false + return false; + } + } else { + // 如果是左括号,压入栈中 + // 将左括号压入栈中 + stack.push(c); + } + } + + // 检查栈是否为空 + return stack.isEmpty(); + } + + /** + * 方法3:数组模拟栈(空间优化) + * + * 算法思路: + * 使用数组代替栈,避免Java集合类的开销 + * + * @param s 输入的括号字符串 + * @return 如果字符串有效返回true,否则返回false + */ + public boolean isValidArray(String s) { + // 检查字符串是否为空 + if (s == null || s.length() == 0) return true; + // 检查字符串长度是否为奇数 + if (s.length() % 2 != 0) return false; + + // 创建字符数组模拟栈 + char[] stack = new char[s.length()]; + // 初始化栈顶指针为-1(空栈状态) + int top = -1; + + // 遍历字符串中的每个字符 + for (char c : s.toCharArray()) { + // 判断是否为左括号 + if (c == '(' || c == '[' || c == '{') { + // 将左括号压入栈中并更新栈顶指针 + stack[++top] = c; + } else { + // 检查栈是否为空 + if (top == -1) return false; + + // 弹出栈顶元素并更新栈顶指针 + char topChar = stack[top--]; + // 检查括号是否匹配 + if ((c == ')' && topChar != '(') || + (c == ']' && topChar != '[') || + (c == '}' && topChar != '{')) { + // 括号不匹配,返回false + return false; + } + } + } + + // 检查栈是否为空 + return top == -1; + } +} diff --git a/algorithm/IsvalidBST98.java b/algorithm/IsvalidBST98.java new file mode 100644 index 0000000000000..7675f6fdb0990 --- /dev/null +++ b/algorithm/IsvalidBST98.java @@ -0,0 +1,387 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 验证二叉搜索树(LeetCode 98) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度: + * - 方法1(递归):O(h) + * h是二叉树的高度,递归调用栈的深度 + * 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + * - 方法2(迭代):O(h) + * 显式栈最多存储h个节点 + * - 方法3(中序遍历):O(h) + * 栈空间或递归调用栈 + */ +public class IsvalidBST98 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + /** + * 方法1:递归解法(带上下界约束) + *

+ * 算法思路: + * 对于二叉搜索树的每个节点,不仅要满足与父节点的大小关系, + * 还要满足与所有祖先节点的大小关系。 + * 因此,我们需要为每个节点维护一个有效的取值范围(lower, upper)。 + *

+ * 执行过程分析(以二叉树 [5,1,4,null,null,3,6] 为例): + *

+ * 5 + * / \ + * 1 4 + * / \ + * 3 6 + *

+ * 递归调用过程: + * isValidBST(root=5, lower=null, upper=null) + * ├─ check(5, null, null) -> true + * ├─ check(1, null, 5) -> true + * ├─ check(4, 5, null) -> false (4 < 5 不满足上界约束) + * └─ 返回 false + *

+ * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + *

+ * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 如果是有效的二叉搜索树返回true,否则返回false + */ + public boolean isValidBST(TreeNode root) { + // 调用递归辅助方法,初始上下界为null + return isValidBSTHelper(root, null, null); + } + + /** + * 递归辅助方法 + *

+ * 算法思路: + * 1. 检查当前节点是否为空,为空则返回true + * 2. 检查当前节点值是否在有效范围内 + * 3. 递归检查左右子树,更新相应的边界值 + *

+ * 时间复杂度分析: + * - 每个节点访问一次:O(n) + *

+ * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @param lower 下界(当前节点值必须大于下界) + * @param upper 上界(当前节点值必须小于上界) + * @return 如果以当前节点为根的子树是有效的BST返回true,否则返回false + */ + private boolean isValidBSTHelper(TreeNode node, Integer lower, Integer upper) { + // 基础情况:空节点被认为是有效的BST + if (node == null) { + return true; + } + + // 检查当前节点是否满足约束条件 + int val = node.val; + // 检查下界约束 + if (lower != null && val <= lower) { + return false; + } + // 检查上界约束 + if (upper != null && val >= upper) { + return false; + } + + // 递归检查左右子树 + // 左子树:上界更新为当前节点值 + // 右子树:下界更新为当前节点值 + return isValidBSTHelper(node.left, lower, val) + && isValidBSTHelper(node.right, val, upper); + } + + /** + * 方法2:中序遍历解法 + *

+ * 算法思路: + * 二叉搜索树的中序遍历结果应该是严格递增的序列 + * 因此,我们可以通过中序遍历二叉树,并检查遍历结果是否严格递增 + *

+ * 执行过程分析(以二叉树 [2,1,3] 为例): + *

+ * 2 + * / \ + * 1 3 + *

+ * 中序遍历结果:[1, 2, 3],严格递增,所以是有效的BST + *

+ * 时间复杂度分析: + * - 每个节点访问一次:O(n) + *

+ * 空间复杂度分析: + * - 显式栈最多存储树的高度个节点:O(h) + * - 最坏情况:O(n),最好情况:O(log n) + * + * @param root 二叉树的根节点 + * @return 如果是有效的二叉搜索树返回true,否则返回false + */ + public boolean isValidBSTInorder(TreeNode root) { + // 创建栈用于迭代中序遍历 + Stack stack = new Stack<>(); + // 用于记录前一个访问的节点值 + Integer inorder = null; + + while (!stack.isEmpty() || root != null) { + // 一直向左走,将路径上的节点入栈 + while (root != null) { + stack.push(root); + root = root.left; + } + + // 弹出栈顶节点 + root = stack.pop(); + + // 检查当前节点是否大于前一个节点 + if (inorder != null && root.val <= inorder) { + return false; + } + inorder = root.val; + + // 处理右子树 + root = root.right; + } + + return true; + } + + /** + * 方法3:递归中序遍历解法 + * + * 算法思路: + * 使用递归实现中序遍历,在遍历过程中检查节点值是否严格递增 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param root 二叉树的根节点 + * @return 如果是有效的二叉搜索树返回true,否则返回false + */ + // 用于记录前一个访问的节点值 + private Integer prev; + + public boolean isValidBSTInorderRecursive(TreeNode root) { + prev = null; + return inorderHelper(root); + } + + /** + * 中序遍历辅助方法 + * + * 算法思路: + * 1. 递归遍历左子树 + * 2. 检查当前节点与前一个节点的关系 + * 3. 递归遍历右子树 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param root 当前节点 + * @return 如果以当前节点为根的子树满足BST性质返回true,否则返回false + */ + private boolean inorderHelper(TreeNode root) { + if (root == null) { + return true; + } + + // 递归检查左子树 + if (!inorderHelper(root.left)) { + return false; + } + + // 检查当前节点 + if (prev != null && root.val <= prev) { + return false; + } + prev = root.val; + + // 递归检查右子树 + return inorderHelper(root.right); + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:中序遍历打印树结构 + * + * 算法思路: + * 按照左-根-右的顺序遍历二叉树并打印节点值 + */ + public static void printInorder(TreeNode root) { + if (root != null) { + printInorder(root.left); + System.out.print(root.val + " "); + printInorder(root.right); + } + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + if (root == null) { + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并验证二叉搜索树 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建的二叉树 + * 4. 调用三种方法验证BST + * 5. 打印验证结果 + */ + public static void main(String[] args) { + System.out.println("验证二叉搜索树"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空,空树被认为是有效的BST"); + System.out.println("结果: true"); + return; + } + + // 打印创建的二叉树 + System.out.println("创建的二叉树:"); + printLevelOrder(root); + + // 打印中序遍历结果 + System.out.print("中序遍历结果: "); + printInorder(root); + System.out.println(); + + // 验证BST + // 创建解决方案实例 + IsvalidBST98 solution = new IsvalidBST98(); + boolean result1 = solution.isValidBST(root); + boolean result2 = solution.isValidBSTInorder(root); + boolean result3 = solution.isValidBSTInorderRecursive(root); + + // 打印结果 + System.out.println("方法1(递归+上下界)验证结果: " + result1); + System.out.println("方法2(迭代中序遍历)验证结果: " + result2); + System.out.println("方法3(递归中序遍历)验证结果: " + result3); + } +} diff --git a/algorithm/Jump45.java b/algorithm/Jump45.java new file mode 100644 index 0000000000000..a1e3f84a084f5 --- /dev/null +++ b/algorithm/Jump45.java @@ -0,0 +1,222 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 跳跃游戏 II(LeetCode 45) + * + * 时间复杂度:O(n) + * - n是数组长度 + * - 只需要遍历数组一次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class Jump45 { + + /** + * 主函数:处理用户输入并计算到达最后一个下标的最小跳跃次数 + * + * 算法流程: + * 1. 读取用户输入的跳跃数组 + * 2. 调用 [jump](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/Jump45.java#L97-L128)方法计算最小跳跃次数 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入跳跃数组(以空格分隔):"); + String line = scanner.nextLine(); + String[] strNums = line.split(" "); + int n = strNums.length; + int[] nums = new int[n]; + + // 将输入的字符串转换为整数数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(strNums[i]); + } + + int jumps = jump(nums); + System.out.println("到达最后一个下标的最小跳跃次数:" + jumps); + } + + /** + * 计算到达最后一个下标的最小跳跃次数 + * + * 算法思路: + * 贪心算法,采用"跳跃区间"的思想 + * 维护三个变量: + * 1. jumps:已经完成的跳跃次数 + * 2. currentEnd:当前这次跳跃能到达的最远位置 + * 3. farthest:在当前跳跃区间内,下一次跳跃能到达的最远位置 + * + * 策略:在当前跳跃区间内,不断更新下一次跳跃能到达的最远位置 + * 当遍历完当前跳跃区间时,必须进行下一次跳跃,并更新相关变量 + * + * 执行过程分析(以`nums=[2,3,1,1,4]`为例): + * + * 初始状态: + * jumps = 0 + * currentEnd = 0 (当前跳跃能到达的最远位置) + * farthest = 0 (下一次跳跃能到达的最远位置) + * + * 遍历过程(注意:只需要遍历到n-2,因为题目保证能到达最后位置): + * + * i=0, nums[0]=2: + * farthest = max(0, 0+2) = 2 + * i=0 == currentEnd=0,需要跳跃 + * jumps = 1 + * currentEnd = farthest = 2 + * + * i=1, nums[1]=3: + * farthest = max(2, 1+3) = 4 + * i=1 < currentEnd=2,不需要跳跃 + * + * i=2, nums[2]=1: + * farthest = max(4, 2+1) = 4 + * i=2 == currentEnd=2,需要跳跃 + * jumps = 2 + * currentEnd = farthest = 4 + * + * i=3, nums[3]=1: + * farthest = max(4, 3+1) = 4 + * i=3 < currentEnd=4,不需要跳跃 + * + * 循环结束(i=4=n-1,不进入循环) + * + * 最终结果:jumps = 2 + * 最优策略:[2] -> [3] -> [4](下标0->1->4,跳跃2次) + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 跳跃数组,`nums[i]`表示在下标i处可以跳跃的最大长度 + * @return 到达最后一个下标的最小跳跃次数 + */ + public static int jump(int[] nums) { + // 跳跃次数,初始为0 + int jumps = 0; + + // 当前这次跳跃能够达到的最远位置 + int currentEnd = 0; + + // 在当前跳跃区间内,下一次跳跃能到达的最远位置 + int farthest = 0; + + int n = nums.length; + + // 遍历数组,注意只需要遍历到n-2 + // 因为题目假设总是能到达最后位置,不需要检查最后一个元素 + for (int i = 0; i < n - 1; i++) { + // 更新在当前跳跃区间内,下一次跳跃能到达的最远位置 + // 从位置i最远可以跳到 i + nums[i] + farthest = Math.max(farthest, i + nums[i]); + + // 如果到达当前跳跃的结束位置,说明必须进行下一次跳跃了 + // 这是贪心策略的关键:在必须跳跃时,选择能跳得最远的方案 + if (i == currentEnd) { + jumps++; // 跳跃次数加1 + currentEnd = farthest; // 更新当前跳跃能够达到的最远位置 + } + } + + return jumps; + } + + /** + * 方法2:动态规划解法 + * + * 算法思路: + * `dp[i]`表示到达位置i的最小跳跃次数 + * + * 时间复杂度分析: + * - 外层循环:O(n) + * - 内层循环:O(nums[i]) + * - 最坏情况总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - dp数组:O(n) + * + * @param nums 跳跃数组 + * @return 到达最后一个下标的最小跳跃次数 + */ + public int jumpDP(int[] nums) { + if (nums == null || nums.length <= 1) return 0; + + int[] dp = new int[nums.length]; + Arrays.fill(dp, nums.length); // 初始化为一个大值 + dp[0] = 0; // 起始位置需要0次跳跃 + + for (int i = 0; i < nums.length; i++) { + // 从位置i可以跳到的所有位置 + for (int j = 1; j <= nums[i] && i + j < nums.length; j++) { + dp[i + j] = Math.min(dp[i + j], dp[i] + 1); + } + } + + return dp[nums.length - 1]; + } + + /** + * 方法3:BFS解法 + * + * 算法思路: + * 将问题看作图论中的最短路径问题 + * 每个位置是一个节点,能跳跃的关系是边 + * 使用BFS找到从位置0到位置n-1的最短路径 + * + * 时间复杂度分析: + * - BFS遍历:O(n²)(最坏情况) + * - 队列操作:O(1) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 队列存储空间:O(n) + * - visited数组:O(n) + * - 总空间复杂度:O(n) + * + * @param nums 跳跃数组 + * @return 到达最后一个下标的最小跳跃次数 + */ + public int jumpBFS(int[] nums) { + if (nums == null || nums.length <= 1) return 0; + + java.util.Queue queue = new java.util.LinkedList<>(); + boolean[] visited = new boolean[nums.length]; + + queue.offer(0); + visited[0] = true; + + int level = 0; + + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + + for (int i = 0; i < size; i++) { + int current = queue.poll(); + + // 从当前位置可以跳到的所有位置 + for (int j = 1; j <= nums[current] && current + j < nums.length; j++) { + int next = current + j; + + if (next == nums.length - 1) { + return level; + } + + if (!visited[next]) { + queue.offer(next); + visited[next] = true; + } + } + } + } + + return level; + } +} diff --git a/algorithm/KSmall230.java b/algorithm/KSmall230.java new file mode 100644 index 0000000000000..dc88f8b6cc9fd --- /dev/null +++ b/algorithm/KSmall230.java @@ -0,0 +1,192 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 二叉搜索树中第K小的元素(LeetCode 230) + * + * 时间复杂度:O(H + k) + * - H是树的高度 + * - 最坏情况下需要遍历到第k个节点 + * + * 空间复杂度:O(H) + * - 递归调用栈的深度为树的高度H + */ +public class KSmall230 { + private int count = 0; // 计数器,用于记录当前访问的节点数 + private int result = -1; // 存储第 k 小元素的值 + + static class TreeNode { + int val; // 节点值 + TreeNode left; // 左子节点 + TreeNode right; // 右子节点 + + TreeNode(int x) { + val = x; // 构造函数 + } + } + + /** + * 中序遍历,查找第 k 小的元素 + * + * 算法思路: + * 利用二叉搜索树的性质:中序遍历的结果是有序的 + * 通过中序遍历,当访问到第k个节点时,该节点就是第k小的元素 + * + * 执行过程分析(以二叉搜索树 [3,1,4,null,2],k=1 为例): + * + * 3 + * / \ + * 1 4 + * \ + * 2 + * + * 中序遍历顺序:1 -> 2 -> 3 -> 4 + * 第1小的元素是1 + * + * 时间复杂度分析: + * - 最多访问k个节点:O(H + k),其中H为树的高度 + * - 当k较小时,只需要遍历到第k个节点即可停止 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(H),其中H为树的高度 + * + * @param node 当前节点 + * @param k 目标位置 + */ + public void inOrderTraversal(TreeNode node, int k) { + // 如果节点为空或已找到第 k 小元素,返回 + if (node == null || count >= k) return; + + // 遍历左子树 + inOrderTraversal(node.left, k); + + // 访问当前节点 + count++; + + // 如果当前计数等于 k + if (count == k) { + // 更新结果 + result = node.val; + // 找到第 k 小元素,返回 + return; + } + + // 遍历右子树 + inOrderTraversal(node.right, k); + } + + /** + * 查找第 k 小的元素 + * + * 算法思路: + * 调用中序遍历方法,在遍历过程中找到第k小的元素 + * + * 时间复杂度分析: + * - 与 [inOrderTraversal](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/KSmall230.java#L51-L77) 方法相同:O(H + k) + * + * 空间复杂度分析: + * - 与 [inOrderTraversal](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/KSmall230.java#L51-L77) 方法相同:O(H) + * + * @param root 二叉搜索树的根节点 + * @param k 目标位置 + * @return 第k小的元素值 + */ + public int kthSmallest(TreeNode root, int k) { + // 重置计数器和结果 + count = 0; + result = -1; + + // 执行中序遍历 + inOrderTraversal(root, k); + + // 返回第 k 小元素 + return result; + } + + /** + * 从用户输入构建二叉树 + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用数组存储节点,根据完全二叉树的性质确定父子关系 + * + * 时间复杂度分析: + * - 遍历所有输入节点一次:O(n),其中n为输入节点数 + * + * 空间复杂度分析: + * - 存储所有节点:O(n) + * + * @param scanner Scanner对象用于读取输入 + * @return 构建完成的二叉树根节点 + */ + public static TreeNode buildTree(Scanner scanner) { + // 提示用户输入 + System.out.print("请输入节点值(用空格分隔,-1 表示 null):"); + // 读取输入并分割 + String[] inputs = scanner.nextLine().split(" "); + + // 如果根节点为空 + if (inputs.length == 0 || inputs[0].equals("-1")) return null; + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(inputs[0])); + // 存储所有节点 + TreeNode[] nodes = new TreeNode[inputs.length]; + // 将根节点存入数组 + nodes[0] = root; + + for (int i = 1; i < inputs.length; i++) { + // 如果是空节点,跳过 + if (inputs[i].equals("-1")) continue; + + // 创建新节点 + TreeNode currentNode = new TreeNode(Integer.parseInt(inputs[i])); + // 存入数组 + nodes[i] = currentNode; + + // 计算父节点的索引 + int parentIndex = (i - 1) / 2; + + // 奇数索引为左子节点 + if (i % 2 == 1) { + nodes[parentIndex].left = currentNode; + } else { + nodes[parentIndex].right = currentNode; + } + } + + // 返回构建完成的树的根节点 + return root; + } + + /** + * 主函数:处理用户输入并查找第K小的元素 + * + * 程序执行流程: + * 1. 提示用户输入二叉搜索树节点值 + * 2. 根据输入构建二叉搜索树 + * 3. 提示用户输入k值 + * 4. 调用方法查找第k小的元素 + * 5. 输出结果 + */ + public static void main(String[] args) { + // 创建扫描器用于读取输入 + Scanner scanner = new Scanner(System.in); + + // 输入二叉树 + TreeNode root = buildTree(scanner); + + // 输入 k 值 + System.out.print("请输入 k 的值:"); + // 读取 k 的值 + int k = scanner.nextInt(); + + // 查找第 k 小元素 + KSmall230 solution = new KSmall230(); + int kthSmallest = solution.kthSmallest(root, k); + + // 输出结果 + System.out.println("第 " + k + " 小的元素是:" + kthSmallest); + } +} diff --git a/algorithm/LRUCache146.java b/algorithm/LRUCache146.java new file mode 100644 index 0000000000000..79e84badee412 --- /dev/null +++ b/algorithm/LRUCache146.java @@ -0,0 +1,235 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * LRU缓存机制(LeetCode 146) + * + * 时间复杂度:O(1) + * - get和put操作的平均时间复杂度都是O(1) + * - 哈希表的查找、插入和删除操作平均时间复杂度为O(1) + * - 双向链表的插入和删除操作时间复杂度为O(1) + * + * 空间复杂度:O(capacity) + * - 哈希表和双向链表最多存储capacity个节点 + */ +public class LRUCache146 { + + /** + * 双向链表节点定义 + */ + class DLinkedNode { + int key; + int value; + DLinkedNode prev; + DLinkedNode next; + + public DLinkedNode() {} + + public DLinkedNode(int _key, int _value) { + key = _key; + value = _value; + } + } + + // 哈希表:用于快速定位节点 + private Map cache = new HashMap<>(); + private int size; // 当前缓存大小 + private int capacity; // 缓存容量 + private DLinkedNode head, tail; // 双向链表的伪头节点和伪尾节点 + + /** + * 构造函数:初始化LRU缓存 + * + * @param capacity 缓存容量 + */ + public LRUCache146(int capacity) { + this.size = 0; + this.capacity = capacity; + + // 使用伪头部和伪尾部节点简化链表操作 + head = new DLinkedNode(); + tail = new DLinkedNode(); + head.next = tail; + tail.prev = head; + } + + /** + * 获取缓存中key对应的value + * + * 算法思路: + * 1. 如果key不存在,返回-1 + * 2. 如果key存在,通过哈希表定位节点,将其移到链表头部,并返回value + * + * @param key 要获取的键 + * @return 对应的值,如果不存在返回-1 + */ + public int get(int key) { + DLinkedNode node = cache.get(key); + if (node == null) { + return -1; + } + + // 如果 key 存在,先通过哈希表定位,再移到头部 + moveToHead(node); + return node.value; + } + + /** + * 向缓存中添加或更新键值对 + * + * 算法思路: + * 1. 如果key不存在: + * - 创建新节点 + * - 添加到哈希表 + * - 添加到链表头部 + * - 如果超出容量,删除链表尾部节点和哈希表中对应项 + * 2. 如果key存在: + * - 更新value + * - 将节点移到链表头部 + * + * @param key 键 + * @param value 值 + */ + public void put(int key, int value) { + DLinkedNode node = cache.get(key); + if (node == null) { + // 如果 key 不存在,创建一个新的节点 + DLinkedNode newNode = new DLinkedNode(key, value); + + // 添加进哈希表和双向链表的头部 + cache.put(key, newNode); + addToHead(newNode); + ++size; + + // 检查是否超出容量 + if (size > capacity) { + // 删除双向链表的尾部节点及哈希表中对应项 + DLinkedNode tail = removeTail(); + cache.remove(tail.key); + --size; + } + } else { + // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 + node.value = value; + moveToHead(node); + } + } + + /** + * 辅助方法:将节点添加到链表头部 + * + * @param node 要添加的节点 + */ + private void addToHead(DLinkedNode node) { + node.prev = head; + node.next = head.next; + head.next.prev = node; + head.next = node; + } + + /** + * 辅助方法:从链表中删除指定节点 + * + * @param node 要删除的节点 + */ + private void removeNode(DLinkedNode node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + /** + * 辅助方法:将节点移动到链表头部 + * + * @param node 要移动的节点 + */ + private void moveToHead(DLinkedNode node) { + removeNode(node); + addToHead(node); + } + + /** + * 辅助方法:删除链表尾部节点 + * + * @return 被删除的节点 + */ + private DLinkedNode removeTail() { + DLinkedNode res = tail.prev; + removeNode(res); + return res; + } + + /** + * 辅助方法:打印当前缓存状态 + */ + public void printCache() { + System.out.print("当前缓存内容(从最近使用到最久未使用): "); + DLinkedNode current = head.next; + while (current != tail) { + System.out.print("(" + current.key + "," + current.value + ") "); + current = current.next; + } + System.out.println(); + System.out.println("当前缓存大小: " + size + "/" + capacity); + } + + /** + * 主函数:处理用户输入并演示LRU缓存操作 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + System.out.print("请输入LRU缓存的容量: "); + int capacity = scanner.nextInt(); + LRUCache146 lruCache = new LRUCache146(capacity); + + System.out.println("LRU缓存初始化完成,容量为: " + capacity); + + while (true) { + System.out.println("\n请选择操作:"); + System.out.println("1. put(key, value) - 添加或更新缓存"); + System.out.println("2. get(key) - 获取缓存值"); + System.out.println("3. 查看当前缓存状态"); + System.out.println("4. 退出"); + System.out.print("请输入选项(1-4): "); + + int choice = scanner.nextInt(); + + switch (choice) { + case 1: + System.out.print("请输入key: "); + int putKey = scanner.nextInt(); + System.out.print("请输入value: "); + int putValue = scanner.nextInt(); + lruCache.put(putKey, putValue); + System.out.println("执行 put(" + putKey + ", " + putValue + ")"); + break; + + case 2: + System.out.print("请输入key: "); + int getKey = scanner.nextInt(); + int result = lruCache.get(getKey); + if (result == -1) { + System.out.println("执行 get(" + getKey + "),结果: 未找到"); + } else { + System.out.println("执行 get(" + getKey + "),结果: " + result); + } + break; + + case 3: + lruCache.printCache(); + break; + + case 4: + System.out.println("退出程序"); + scanner.close(); + return; + + default: + System.out.println("无效选项,请重新输入"); + } + } + } +} diff --git a/algorithm/LargestArea84.java b/algorithm/LargestArea84.java new file mode 100644 index 0000000000000..b1edbeb88bcef --- /dev/null +++ b/algorithm/LargestArea84.java @@ -0,0 +1,214 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Stack; +import java.util.Arrays; + +/** + * 柱状图中最大的矩形(LeetCode 84)- 单调栈 + * + * 时间复杂度:O(n) + * - 每个元素最多入栈和出栈一次 + * + * 空间复杂度:O(n) + * - 栈最多存储n个元素 + */ +public class LargestArea84 { + + /** + * 主函数:处理用户输入并计算柱状图中最大矩形面积 + * + * 算法流程: + * 1. 读取用户输入的柱状图高度数组 + * 2. 调用largestRectangleArea方法计算最大面积 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入柱状图的高度(以空格分隔):"); + String line = scanner.nextLine(); + String[] strHeights = line.split(" "); + int[] heights = new int[strHeights.length]; + + // 将输入的字符串高度转换为整数数组 + for (int i = 0; i < strHeights.length; i++) { + heights[i] = Integer.parseInt(strHeights[i]); + } + + int maxArea = largestRectangleArea(heights); + System.out.println("能够勾勒出来的最大矩形面积是: " + maxArea); + } + + /** + * 计算柱状图中能够勾勒出来的最大矩形面积 + * + * 算法思路: + * 使用单调递增栈存储柱子的索引 + * 1. 遍历柱子高度数组(并在末尾添加一个高度为0的虚拟柱子) + * 2. 当当前高度小于栈顶索引对应的高度时,说明找到了右边界 + * 3. 弹出栈顶元素作为矩形的高度,计算宽度并更新最大面积 + * 4. 将当前索引入栈 + * + * 执行过程分析(以heights=[2,1,5,6,2,3]为例): + * + * 添加虚拟柱子后:[2,1,5,6,2,3,0] + * + * 初始状态: + * stack = [] + * maxArea = 0 + * + * 遍历过程: + * i=0, height=2: + * 栈为空,将0入栈 + * stack = [0] + * + * i=1, height=1: + * 1 < heights[0]=2,找到右边界 + * 弹出0,height=2,width=1-(-1)-1=1(栈为空时宽度为i) + * area=2*1=2,maxArea=max(0,2)=2 + * 将1入栈 + * stack = [1] + * + * i=2, height=5: + * 5 > heights[1]=1,将2入栈 + * stack = [1,2] + * + * i=3, height=6: + * 6 > heights[2]=5,将3入栈 + * stack = [1,2,3] + * + * i=4, height=2: + * 2 < heights[3]=6,找到右边界 + * 弹出3,height=6,width=4-2-1=1 + * area=6*1=6,maxArea=max(2,6)=6 + * + * 2 < heights[2]=5,找到右边界 + * 弹出2,height=5,width=4-1-1=2 + * area=5*2=10,maxArea=max(6,10)=10 + * + * 2 > heights[1]=1,将4入栈 + * stack = [1,4] + * + * i=5, height=3: + * 3 > heights[4]=2,将5入栈 + * stack = [1,4,5] + * + * i=6, height=0: + * 0 < heights[5]=3,找到右边界 + * 弹出5,height=3,width=6-4-1=1 + * area=3*1=3,maxArea=max(10,3)=10 + * + * 0 < heights[4]=2,找到右边界 + * 弹出4,height=2,width=6-1-1=4 + * area=2*4=8,maxArea=max(10,8)=10 + * + * 0 < heights[1]=1,找到右边界 + * 弹出1,height=1,width=6-(-1)-1=6(栈为空) + * area=1*6=6,maxArea=max(10,6)=10 + * + * 最终结果:maxArea = 10 + * 最大矩形:高度为5和6的柱子组成的矩形,面积=5*2=10 + * + * @param heights 柱状图的高度数组 + * @return 最大矩形面积 + */ + public static int largestRectangleArea(int[] heights) { + // 记录最大面积 + int maxArea = 0; + // 使用单调递增栈存储柱子的索引 + Stack stack = new Stack<>(); + // 柱子数量 + int n = heights.length; + + // 遍历柱子(包括一个虚拟的0高度柱子) + for (int i = 0; i <= n; i++) { + // 处理柱子的高度,末尾加一个高度为0的柱子以处理剩余的柱子 + // 当i==n时,使用0高度来确保栈中所有元素都被处理 + int currentHeight = (i == n) ? 0 : heights[i]; + + // 当栈不为空且当前高度小于栈顶索引对应的高度时 + // 说明栈顶柱子找到了右边界,可以计算以该柱子为高的最大矩形面积 + while (!stack.isEmpty() && currentHeight < heights[stack.peek()]) { + // 弹出栈顶元素作为矩形的高度 + int height = heights[stack.pop()]; + + // 计算矩形的宽度 + // 如果栈为空,说明该柱子是目前为止最矮的,宽度为i + // 如果栈不为空,宽度为 i - stack.peek() - 1 + int width = stack.isEmpty() ? i : i - stack.peek() - 1; + + // 更新最大面积 + maxArea = Math.max(maxArea, height * width); + } + + // 将当前柱子的索引入栈 + stack.push(i); + } + + // 返回最大面积 + return maxArea; + } + + /** + * 方法2:不使用虚拟柱子的版本 + * + * @param heights 柱状图的高度数组 + * @return 最大矩形面积 + */ + public int largestRectangleAreaAlternative(int[] heights) { + if (heights == null || heights.length == 0) return 0; + + Stack stack = new Stack<>(); + int maxArea = 0; + + for (int i = 0; i < heights.length; i++) { + while (!stack.isEmpty() && heights[i] < 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); + } + + // 处理栈中剩余的元素 + while (!stack.isEmpty()) { + int height = heights[stack.pop()]; + int width = stack.isEmpty() ? heights.length : heights.length - stack.peek() - 1; + maxArea = Math.max(maxArea, height * width); + } + + return maxArea; + } + + /** + * 方法3:分治法(仅供学习,效率较低) + * + * @param heights 柱状图的高度数组 + * @return 最大矩形面积 + */ + public int largestRectangleAreaDivideConquer(int[] heights) { + if (heights == null || heights.length == 0) return 0; + return divideConquer(heights, 0, heights.length - 1); + } + + private int divideConquer(int[] heights, int start, int end) { + if (start > end) return 0; + + // 找到最小高度的索引 + int minIndex = start; + for (int i = start; i <= end; i++) { + if (heights[i] < heights[minIndex]) { + minIndex = i; + } + } + + // 以最小高度为高的矩形面积 + int areaWithMinHeight = heights[minIndex] * (end - start + 1); + + // 递归计算左右两部分的最大面积 + int leftMax = divideConquer(heights, start, minIndex - 1); + int rightMax = divideConquer(heights, minIndex + 1, end); + + return Math.max(areaWithMinHeight, Math.max(leftMax, rightMax)); + } +} diff --git a/algorithm/LengthOfLIS300.java b/algorithm/LengthOfLIS300.java new file mode 100644 index 0000000000000..466d3d3b009ff --- /dev/null +++ b/algorithm/LengthOfLIS300.java @@ -0,0 +1,259 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 最长递增子序列(LeetCode 300)- 动态规划 + * + * 时间复杂度: + * - 方法1(动态规划):O(n²) + * 外层循环运行n次,内层循环最多运行i次 + * - 方法2(二分查找优化):O(n log n) + * 外层循环运行n次,每次二分查找耗时O(log n) + * + * 空间复杂度:O(n) + * - 需要长度为n的DP数组或tails数组 + */ +public class LengthOfLIS300 { + + /** + * 主函数:处理用户输入并计算最长递增子序列的长度 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用 [lengthOfLIS](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/LengthOfLIS300.java#L141-L182)方法计算最长递增子序列的长度 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入数组 + System.out.print("请输入整数数组(用空格分隔):"); + String[] input = scanner.nextLine().split(" "); + int[] nums = new int[input.length]; + for (int i = 0; i < input.length; i++) { + nums[i] = Integer.parseInt(input[i]); + } + + int result = lengthOfLIS(nums); + System.out.println("最长严格递增子序列的长度是:" + result); + + // 演示其他解法 + LengthOfLIS300 solution = new LengthOfLIS300(); + int result2 = solution.lengthOfLISBinarySearch(nums); + System.out.println("二分查找优化方法结果:" + result2); + } + + /** + * 计算最长严格递增子序列的长度 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示以`nums[i]`结尾的最长严格递增子序列的长度 + * 状态转移方程: + * `dp[i] = max(dp[j] + 1)` for all j where 0 <= j < i and `nums[j]` < `nums[i]` + * + * 执行过程分析(以`nums=[10,9,2,5,3,7,101,18]`为例): + * + * 初始化DP数组: + * dp = [1, 1, 1, 1, 1, 1, 1, 1] + * 索引: 0 1 2 3 4 5 6 7 + * 值: 10 9 2 5 3 7 101 18 + * + * 填充DP数组: + * i=1, `nums[1]=9`: + * j=0, `nums[0]=10`: 9 < 10,不满足条件 + * dp[1] = 1 + * + * i=2, `nums[2]=2`: + * j=0, `nums[0]=10`: 2 < 10,不满足条件 + * j=1, `nums[1]=9`: 2 < 9,不满足条件 + * dp[2] = 1 + * + * i=3, `nums[3]=5`: + * j=0, `nums[0]=10`: 5 < 10,不满足条件 + * j=1, `nums[1]=9`: 5 < 9,不满足条件 + * j=2, `nums[2]=2`: 5 > 2,满足条件,dp[3] = max(1, dp[2]+1) = max(1, 2) = 2 + * + * i=4, `nums[4]=3`: + * j=0, `nums[0]=10`: 3 < 10,不满足条件 + * j=1, `nums[1]=9`: 3 < 9,不满足条件 + * j=2, `nums[2]=2`: 3 > 2,满足条件,dp[4] = max(1, dp[2]+1) = max(1, 2) = 2 + * j=3, `nums[3]=5`: 3 < 5,不满足条件 + * + * i=5, `nums[5]=7`: + * j=0, `nums[0]=10`: 7 < 10,不满足条件 + * j=1, `nums[1]=9`: 7 < 9,不满足条件 + * j=2, `nums[2]=2`: 7 > 2,满足条件,dp[5] = max(1, dp[2]+1) = max(1, 2) = 2 + * j=3, `nums[3]=5`: 7 > 5,满足条件,dp[5] = max(2, dp[3]+1) = max(2, 3) = 3 + * j=4, `nums[4]=3`: 7 > 3,满足条件,dp[5] = max(3, dp[4]+1) = max(3, 3) = 3 + * + * i=6, `nums[6]=101`: + * j=0到j=5中,所有`nums[j]` < 101 + * dp[6] = max(dp[0到5]) + 1 = max(1,1,1,2,2,3) + 1 = 4 + * + * i=7, `nums[7]=18`: + * j=0, `nums[0]=10`: 18 > 10,满足条件 + * j=5, `nums[5]=7`: 18 > 7,满足条件,dp[7] = max(..., dp[5]+1) = max(..., 4) = 4 + * j=6, `nums[6]=101`: 18 < 101,不满足条件 + * + * 最终DP数组: + * dp = [1, 1, 1, 2, 2, 3, 4, 4] + * + * 最长递增子序列长度:max(dp) = 4 + * 一个可能的最长递增子序列:[2, 3, 7, 18] 或 [2, 3, 7, 101] + * + * 时间复杂度分析: + * - 初始化DP数组:O(n) + * - 填充DP数组:O(n²) + * - 查找最大值:O(n) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * + * @param nums 输入的整数数组 + * @return 最长严格递增子序列的长度 + */ + public static int lengthOfLIS(int[] nums) { + // 边界情况:空数组 + if (nums.length == 0) return 0; + + // dp[i] 表示以 nums[i] 结尾的最长严格递增子序列的长度 + int[] dp = new int[nums.length]; + + // 初始化 dp 数组 + // 每个元素自身就是一个长度为 1 的子序列 + for (int i = 0; i < nums.length; i++) { + dp[i] = 1; + } + + // 动态规划填充 dp 数组 + // 外层循环遍历每个元素 + for (int i = 1; i < nums.length; i++) { + // 内层循环检查所有在i之前的元素 + for (int j = 0; j < i; j++) { + // 只考虑严格递增的情况:nums[i] > nums[j] + if (nums[i] > nums[j]) { + // 状态转移方程: + // 以nums[i]结尾的最长递增子序列长度 = + // max(当前值, 以nums[j]结尾的最长递增子序列长度 + 1) + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + } + + // 找到 dp 数组中的最大值,即为最长递增子序列的长度 + int maxLength = 0; + for (int length : dp) { + maxLength = Math.max(maxLength, length); + } + + return maxLength; + } + + /** + * 方法2:二分查找优化解法 + * + * 算法思路: + * 维护一个数组`tails`,`tails[i]`表示长度为i+1的递增子序列的最小尾部元素 + * 使用二分查找来找到插入位置 + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 每次二分查找:O(log n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - tails数组存储空间:O(n) + * + * @param nums 输入的整数数组 + * @return 最长严格递增子序列的长度 + */ + public int lengthOfLISBinarySearch(int[] nums) { + if (nums == null || nums.length == 0) return 0; + + // tails[i] 表示长度为 i+1 的递增子序列的最小尾部元素 + int[] tails = new int[nums.length]; + int size = 0; + + for (int num : nums) { + // 使用二分查找找到num应该插入的位置 + int left = 0, right = size; + while (left < right) { + int mid = left + (right - left) / 2; + if (tails[mid] < num) { + left = mid + 1; + } else { + right = mid; + } + } + + // 更新tails数组 + tails[left] = num; + + // 如果插入位置在数组末尾,说明递增子序列长度增加 + if (left == size) { + size++; + } + } + + return size; + } + + /** + * 扩展方法:返回实际的最长递增子序列 + * + * 算法思路: + * 在计算最长递增子序列长度的同时,记录每个元素的前驱元素, + * 最后通过回溯前驱元素重构最长递增子序列 + * + * 时间复杂度分析: + * - 计算DP值和前驱:O(n²) + * - 重构子序列:O(n) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * - 前驱数组存储空间:O(n) + * - 结果数组存储空间:O(k),k为最长递增子序列长度 + * + * @param nums 输入的整数数组 + * @return 最长递增子序列 + */ + public int[] findLIS(int[] nums) { + if (nums == null || nums.length == 0) return new int[0]; + + int[] dp = new int[nums.length]; + int[] prev = new int[nums.length]; + Arrays.fill(dp, 1); + Arrays.fill(prev, -1); + + int maxLength = 1; + int maxIndex = 0; + + for (int i = 1; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) { + dp[i] = dp[j] + 1; + prev[i] = j; + } + } + + if (dp[i] > maxLength) { + maxLength = dp[i]; + maxIndex = i; + } + } + + // 重构最长递增子序列 + int[] result = new int[maxLength]; + int index = maxIndex; + for (int i = maxLength - 1; i >= 0; i--) { + result[i] = nums[index]; + index = prev[index]; + } + + return result; + } +} diff --git a/algorithm/LettercCombinations17.java b/algorithm/LettercCombinations17.java new file mode 100644 index 0000000000000..67e3e17cad82f --- /dev/null +++ b/algorithm/LettercCombinations17.java @@ -0,0 +1,356 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.HashMap; + +/** + * 电话号码的字母组合(LeetCode 17) + * + * 时间复杂度:O(3^m × 4^n) + * - m是对应3个字母的数字个数,n是对应4个字母的数字个数 + * - 每个数字对应多个字母选择,总共有3^m × 4^n种组合 + * + * 空间复杂度:O(m + n) + * - 递归调用栈深度为数字串长度 + * - 需要额外的列表存储当前路径和数字字母映射 + */ +public class LettercCombinations17 { + + // 数字到字母的映射 + private static final Map PHONE_MAP = new HashMap<>(); + static { + PHONE_MAP.put('2', "abc"); + PHONE_MAP.put('3', "def"); + PHONE_MAP.put('4', "ghi"); + PHONE_MAP.put('5', "jkl"); + PHONE_MAP.put('6', "mno"); + PHONE_MAP.put('7', "pqrs"); + PHONE_MAP.put('8', "tuv"); + PHONE_MAP.put('9', "wxyz"); + } + + /** + * 生成电话号码的所有字母组合 + * + * 算法思路: + * 使用回溯算法,通过递归和状态重置生成所有可能的字母组合 + * 1. 建立数字到字母的映射关系 + * 2. 递归处理每个数字,尝试对应的每个字母 + * 3. 构建完整组合后添加到结果中 + * 4. 回溯时重置状态,尝试其他可能 + * + * 执行过程分析(以digits="23"为例): + * + * 数字映射: + * '2' -> "abc" + * '3' -> "def" + * + * 递归树: + * "" + * / | \ + * "a" "b" "c" + * / | \ / | \ / | \ + * "ad""ae""af""bd""be""bf""cd""ce""cf" + * + * 详细执行过程: + * + * 1. index=0, digit='2': 对应字母"abc" + * - choose('a'): combination="a", index=1 + * - index=1, digit='3': 对应字母"def" + * - choose('d'): combination="ad", index=2 + * - index=2 == digits.length(),添加"ad"到结果 + * - choose('e'): combination="ae", index=2 + * - index=2 == digits.length(),添加"ae"到结果 + * - choose('f'): combination="af", index=2 + * - index=2 == digits.length(),添加"af"到结果 + * - choose('b'): combination="b", index=1 + * - 同样处理'd','e','f',得到"bd","be","bf" + * - choose('c'): combination="c", index=1 + * - 同样处理'd','e','f',得到"cd","ce","cf" + * + * 最终结果:["ad","ae","af","bd","be","bf","cd","ce","cf"] + * + * 时间复杂度分析: + * - 设输入数字串长度为L,每个数字对应的字母数为K(3或4) + * - 最坏情况下需要遍历所有可能组合:O(K^L) + * - 具体为O(3^m × 4^n),其中: + * - m: 输入字符串中对应3个字母的数字个数(数字2,3,4,5,6,8的个数) + * - n: 输入字符串中对应4个字母的数字个数(数字7,9的个数) + * - 例如:digits="237",则m=2(数字2,3),n=1(数字7),复杂度为O(3^2 × 4^1) = O(36) + * + * 空间复杂度分析: + * - 递归调用栈深度等于数字串长度:O(L) + * - L: 输入数字字符串的长度,即递归的最大深度 + * - 存储单个组合的空间:O(L) + * - 总空间复杂度:O(L),其中L为输入数字字符串的长度 + * + * @param digits 输入的数字字符串 + * @return 所有可能的字母组合 + */ + public List letterCombinations(String digits) { + // 创建结果列表用于存储所有可能的字母组合 + List result = new ArrayList<>(); + + // 边界情况:空字符串 + if (digits == null || digits.length() == 0) { + return result; + } + + // 使用StringBuilder构建当前字母组合,提高字符串操作效率 + StringBuilder combination = new StringBuilder(); + + // 开始回溯算法生成所有组合 + backtrack(digits, 0, combination, result); + + return result; + } + + /** + * 回溯辅助方法 + * + * 算法思路: + * 递归地为每个数字位置选择一个字母,构建完整组合后回溯尝试其他可能 + * + * @param digits 输入的数字字符串 + * @param index 当前处理的数字位置 + * @param combination 当前构建的字母组合 + * @param result 存储所有字母组合的结果列表 + */ + private void backtrack(String digits, int index, StringBuilder combination, List result) { + // 递归终止条件:已处理完所有数字 + if (index == digits.length()) { + // 将当前组合添加到结果中 + result.add(combination.toString()); + return; + } + + // 获取当前数字对应的字母 + char digit = digits.charAt(index); + String letters = PHONE_MAP.get(digit); + + // 尝试每个字母作为当前数字的对应字符 + for (int i = 0; i < letters.length(); i++) { + char letter = letters.charAt(i); + + // 做选择:将字母添加到当前组合 + combination.append(letter); + + // 递归:处理下一个数字 + backtrack(digits, index + 1, combination, result); + + // 撤销选择:回溯,移除字母,为尝试下一个字母做准备 + combination.deleteCharAt(combination.length() - 1); + } + + } + + + /** + * 方法2:迭代解法 + * + * 算法思路: + * 逐个处理数字,将新数字对应的字母与已有组合进行组合 + * + * 执行过程分析(以digits="23"为例): + * + * 初始:result = [""] + * + * 处理'2'(对应"abc"): + * 对""分别添加'a','b','c' + * result = ["a", "b", "c"] + * + * 处理'3'(对应"def"): + * 对"a"分别添加'd','e','f' -> "ad","ae","af" + * 对"b"分别添加'd','e','f' -> "bd","be","bf" + * 对"c"分别添加'd','e','f' -> "cd","ce","cf" + * result = ["ad","ae","af","bd","be","bf","cd","ce","cf"] + * + * 时间复杂度分析: + * - 外层循环遍历所有数字:O(L),L为数字串长度 + * - 内层双重循环生成新组合:O(3^m × 4^n) + * - 总时间复杂度:O(3^m × 4^n) + * - 其中m: 输入字符串中对应3个字母的数字个数 + * - 其中n: 输入字符串中对应4个字母的数字个数 + * - 例如:digits="27",则m=1(数字2),n=1(数字7),复杂度为O(3^1 × 4^1) = O(12) + * + * 空间复杂度分析: + * - 使用临时列表存储中间结果:O(3^m × 4^n) + * - 3^m × 4^n: 所有可能的字母组合总数 + * - 总空间复杂度:O(3^m × 4^n) + * + * @param digits 输入的数字字符串 + * @return 所有可能的字母组合 + */ + public List letterCombinationsIterative(String digits) { + // 创建结果列表用于存储所有可能的字母组合 + List result = new ArrayList<>(); + + // 边界情况:空字符串 + if (digits == null || digits.length() == 0) { + return result; + } + + // 初始化为空字符串,作为构建组合的起点 + result.add(""); + + // 逐个处理每个数字字符 + for (char digit : digits.toCharArray()) { + // 获取当前数字对应的字母字符串 + String letters = PHONE_MAP.get(digit); + // 创建临时列表存储新的组合 + List temp = new ArrayList<>(); + + // 将当前数字对应的每个字母与已有组合进行拼接 + for (String combination : result) { + for (char letter : letters.toCharArray()) { + // 生成新的组合并添加到临时列表中 + temp.add(combination + letter); + } + } + + // 更新结果列表为新的组合列表 + result = temp; + } + + return result; + } + + /** + * 方法3:队列解法 + * + * 算法思路: + * 使用队列存储中间组合结果,按层次逐个处理数字 + * + * 执行过程分析(以digits="23"为例): + * + * 初始:queue = [""] + * + * 处理'2'(对应"abc"): + * 取出"",分别添加'a','b','c' + * queue = ["a", "b", "c"] + * + * 处理'3'(对应"def"): + * 取出"a",分别添加'd','e','f' -> "ad","ae","af" + * 取出"b",分别添加'd','e','f' -> "bd","be","bf" + * 取出"c",分别添加'd','e','f' -> "cd","ce","cf" + * queue = ["ad","ae","af","bd","be","bf","cd","ce","cf"] + * + * 时间复杂度分析: + * - 外层循环遍历所有数字:O(L),L为数字串长度 + * - 内层双重循环生成新组合:O(3^m × 4^n) + * - 总时间复杂度:O(3^m × 4^n) + * - 其中m: 输入字符串中对应3个字母的数字个数 + * - 其中n: 输入字符串中对应4个字母的数字个数 + * - 例如:digits="234",则m=3(数字2,3,4),n=0,复杂度为O(3^3 × 4^0) = O(27) + * + * 空间复杂度分析: + * - 队列存储中间结果:O(3^m × 4^n) + * - 3^m × 4^n: 所有可能的字母组合总数,队列在处理过程中需要存储这些组合 + * - 总空间复杂度:O(3^m × 4^n) + * + * @param digits 输入的数字字符串 + * @return 所有可能的字母组合 + */ + public List letterCombinationsQueue(String digits) { + // 创建结果列表用于存储所有可能的字母组合 + List result = new ArrayList<>(); + + // 边界情况检查 + if (digits == null || digits.length() == 0) { + return result; + } + + // 使用队列存储中间组合结果 + java.util.Queue queue = new java.util.LinkedList<>(); + // 初始化队列,添加空字符串作为起点 + queue.offer(""); + + // 逐个处理每个数字字符 + for (char digit : digits.toCharArray()) { + // 获取当前数字对应的字母字符串 + String letters = PHONE_MAP.get(digit); + // 获取当前队列大小,即当前层的组合数量 + int size = queue.size(); + + // 处理当前层的所有组合 + for (int i = 0; i < size; i++) { + // 取出队列头部的当前组合 + String current = queue.poll(); + // 将当前数字对应的每个字母与当前组合拼接 + for (char letter : letters.toCharArray()) { + // 将新生成的组合重新加入队列 + queue.offer(current + letter); + } + } + } + + // 将队列中剩余的所有组合转移到结果列表中 + while (!queue.isEmpty()) { + result.add(queue.poll()); + } + + return result; + } + + /** + * 辅助方法:读取用户输入的数字字符串 + * + * @return 用户输入的数字字符串 + */ + public static String readDigits() { + // 创建输入读取器 + Scanner scanner = new Scanner(System.in); + // 提示用户输入 + System.out.print("请输入数字字符串 (2-9): "); + // 返回用户输入的字符串 + return scanner.nextLine(); + } + + /** + * 辅助方法:打印字母组合 + * + * @param result 字母组合列表 + */ + public static void printCombinations(List result) { + // 打印标题 + System.out.println("所有字母组合:"); + // 遍历并打印每个组合 + for (int i = 0; i < result.size(); i++) { + System.out.println((i + 1) + ": " + result.get(i)); + } + // 打印总计数量 + System.out.println("总共 " + result.size() + " 个组合"); + } + + /** + * 主函数:处理用户输入并生成所有字母组合 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("电话号码的字母组合"); + + // 读取用户输入的数字字符串 + String digits = readDigits(); + System.out.println("输入数字: \"" + digits + "\""); + + // 生成所有字母组合 + LettercCombinations17 solution = new LettercCombinations17(); + List result1 = solution.letterCombinations(digits); + List result2 = solution.letterCombinationsIterative(digits); + List result3 = solution.letterCombinationsQueue(digits); + + // 输出结果 + System.out.println("回溯方法结果:"); + printCombinations(result1); + + System.out.println("\n迭代方法结果:"); + printCombinations(result2); + + System.out.println("\n队列方法结果:"); + printCombinations(result3); + } +} diff --git a/algorithm/LevelOrderBinaryTree102.java b/algorithm/LevelOrderBinaryTree102.java new file mode 100644 index 0000000000000..3c65f6e757dae --- /dev/null +++ b/algorithm/LevelOrderBinaryTree102.java @@ -0,0 +1,329 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.LinkedList; +import java.util.Scanner; + +/** + * 二叉树的层序遍历(LeetCode 102) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(w) + * - w是二叉树的最大宽度 + * - 队列最多存储一层的所有节点 + */ +public class LevelOrderBinaryTree102 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int val) { this.val = val; } + } + + /** + * 二叉树的层序遍历 + * + * 算法思路: + * 使用队列进行广度优先搜索(BFS) + * 1. 将根节点加入队列 + * 2. 当队列不为空时,记录当前层的节点数 + * 3. 依次处理当前层的所有节点,并将下一层的节点加入队列 + * 4. 重复步骤2-3直到队列为空 + * + * 执行过程分析(以二叉树 [3,9,20,null,null,15,7] 为例): + * + * 3 + * / \ + * 9 20 + * / \ + * 15 7 + * + * 执行步骤: + * 初始状态:队列=[3],结果=[] + * + * 第1层处理: + * - size=1 + * - 弹出3,加入结果[[3]],将9和20加入队列 + * - 队列=[9,20] + * + * 第2层处理: + * - size=2 + * - 弹出9,加入结果[[3],[9]],9无子节点 + * - 弹出20,加入结果[[3],[9,20]],将15和7加入队列 + * - 队列=[15,7] + * + * 第3层处理: + * - size=2 + * - 弹出15,加入结果[[3],[9,20],[15]],15无子节点 + * - 弹出7,加入结果[[3],[9,20],[15,7]],7无子节点 + * - 队列=[] + * + * 返回结果:[[3],[9,20],[15,7]] + * + * 时间复杂度分析: + * - 每个节点入队和出队一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 结果列表存储所有节点:O(n) + * - 总空间复杂度:O(n) + * + * @param root 二叉树的根节点 + * @return 按层序排列的节点值列表 + */ + public List> levelOrder(TreeNode root) { + // 创建结果列表,用于存储每层的节点值 + List> result = new ArrayList<>(); + + // 如果根节点为空,直接返回空结果 + if (root == null) { + return result; + } + + // 使用队列进行广度优先搜索 + Queue queue = new LinkedList<>(); + queue.offer(root); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 记录当前层的节点数 + int size = queue.size(); + + // 存储当前层的节点值 + List currentLevel = new ArrayList<>(); + + // 处理当前层的所有节点 + for (int i = 0; i < size; i++) { + // 弹出队列中的节点 + TreeNode node = queue.poll(); + + // 将节点值加入当前层结果 + currentLevel.add(node.val); + + // 将下一层的节点加入队列 + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } + + // 将当前层结果加入最终结果 + result.add(currentLevel); + } + + return result; + } + + + /** + * 递归解法 + * + * 算法思路: + * 使用深度优先搜索(DFS),通过层数参数来确定节点应该加入哪一层的结果列表 + * + * 执行过程分析(以二叉树 [3,9,20,null,null,15,7] 为例): + * + * 3 + * / \ + * 9 20 + * / \ + * 15 7 + * + * 递归调用过程: + * levelOrderHelper(3, 0, result) + * ├─ result.size() <= 0 -> 创建新列表,result=[[3]] + * ├─ levelOrderHelper(9, 1, result) + * │ ├─ result.size() <= 1 -> 创建新列表,result=[[3],[9]] + * │ ├─ levelOrderHelper(null, 2, result) -> 返回 + * │ └─ levelOrderHelper(null, 2, result) -> 返回 + * ├─ levelOrderHelper(20, 1, result) + * │ ├─ result.size() = 2 > 1 -> 直接添加,result=[[3],[9,20]] + * │ ├─ levelOrderHelper(15, 2, result) + * │ │ ├─ result.size() <= 2 -> 创建新列表,result=[[3],[9,20],[15]] + * │ │ └─ ... + * │ └─ levelOrderHelper(7, 2, result) + * │ ├─ result.size() = 3 > 2 -> 直接添加,result=[[3],[9,20],[15,7]] + * │ └─ ... + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 结果列表存储所有节点:O(n) + * - 总空间复杂度:O(n) + * + * @param root 二叉树的根节点 + * @return 按层序排列的节点值列表 + */ + public List> levelOrderRecursive(TreeNode root) { + // 创建结果列表,用于存储每层的节点值 + List> result = new ArrayList<>(); + levelOrderHelper(root, 0, result); + return result; + } + + /** + * 递归辅助方法 + * + * 算法思路: + * 1. 如果节点为空,直接返回 + * 2. 如果结果列表的大小小于等于当前层数,为当前层创建新的列表 + * 3. 将当前节点值加入对应层的列表 + * 4. 递归处理左右子树,层数加1 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param node 当前节点 + * @param level 当前层数 + * @param result 结果列表 + */ + private void levelOrderHelper(TreeNode node, int level, List> result) { + // 基础情况:如果节点为空,直接返回 + if (node == null) { + return; + } + + // 如果当前层还没有对应的列表,创建一个新的列表 + if (result.size() <= level) { + result.add(new ArrayList<>()); + } + + // 将当前节点值加入对应层的列表 + result.get(level).add(node.val); + + // 递归处理左右子树,层数加1 + levelOrderHelper(node.left, level + 1, result); + levelOrderHelper(node.right, level + 1, result); + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:打印层序遍历结果 + * + * 算法思路: + * 遍历结果列表,按层打印每层的节点值 + */ + public static void printLevelOrderResult(List> result) { + System.out.println("层序遍历结果:"); + for (int i = 0; i < result.size(); i++) { + System.out.println("第" + (i + 1) + "层: " + result.get(i)); + } + } + + /** + * 主函数:处理用户输入并执行层序遍历 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建成功的提示信息 + * 4. 调用两种方法执行层序遍历 + * 5. 打印遍历结果 + */ + public static void main(String[] args) { + System.out.println("二叉树层序遍历"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空"); + return; + } + + System.out.println("二叉树创建成功"); + + // 执行层序遍历 + // 创建解决方案实例 + LevelOrderBinaryTree102 solution = new LevelOrderBinaryTree102(); + List> result1 = solution.levelOrder(root); + List> result2 = solution.levelOrderRecursive(root); + + // 打印结果 + System.out.println("\n迭代方法结果:"); + printLevelOrderResult(result1); + + System.out.println("\n递归方法结果:"); + printLevelOrderResult(result2); + } +} diff --git a/algorithm/LongestCommonSubsequence1143.java b/algorithm/LongestCommonSubsequence1143.java new file mode 100644 index 0000000000000..8244e7c002f06 --- /dev/null +++ b/algorithm/LongestCommonSubsequence1143.java @@ -0,0 +1,182 @@ +package com.funian.algorithm.algorithm; + +/** + * 最长公共子序列(LeetCode 1143)- 动态规划 + * + * 时间复杂度:O(m * n) + * - m是text1的长度,n是text2的长度 + * - 需要填充m*n的DP表 + * + * 空间复杂度:O(m * n) + * - 需要m*n的DP表存储中间结果 + * - 可以优化到O(min(m,n)),但这里为了清晰起见使用完整DP表 + */ +import java.util.Scanner; +import java.util.Arrays; + +public class LongestCommonSubsequence1143 { + + /** + * 主函数:处理用户输入并计算最长公共子序列的长度 + * + * 算法流程: + * 1. 读取用户输入的两个字符串 + * 2. 调用 [longestCommonSubsequence](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/LongestCommonSubsequence1143.java#L117-L144)方法计算最长公共子序列的长度 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入两个字符串 + System.out.print("请输入第一个字符串 text1: "); + String text1 = scanner.nextLine(); + System.out.print("请输入第二个字符串 text2: "); + String text2 = scanner.nextLine(); + + // 调用 longestCommonSubsequence 方法计算最长公共子序列的长度 + int result = longestCommonSubsequence(text1, text2); + // 输出结果 + System.out.println("最长公共子序列的长度:" + result); + + // 演示如何获取实际的最长公共子序列 + LongestCommonSubsequence1143 solution = new LongestCommonSubsequence1143(); + String lcs = solution.findLCS(text1, text2); + System.out.println("最长公共子序列内容: \"" + lcs + "\""); + } + + /** + * 计算两个字符串的最长公共子序列长度 + * + * 算法思路: + * 使用动态规划,定义`dp[i][j]`表示text1的前i个字符和text2的前j个字符的最长公共子序列长度 + * 状态转移方程: + * 1. 如果text1[i-1] == text2[j-1],则dp[i][j] = dp[i-1][j-1] + 1 + * 2. 如果text1[i-1] != text2[j-1],则dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + * + * 执行过程分析(以text1="abcde", text2="ace"为例): + * + * 初始化DP表(m=5, n=3): + * "" a c e + * "" 0 0 0 0 + * a 0 + * b 0 + * c 0 + * d 0 + * e 0 + * + * 填充DP表: + * "" a c e + * "" 0 0 0 0 + * a 0 1 1 1 + * b 0 1 1 1 + * c 0 1 2 2 + * d 0 1 2 2 + * e 0 1 2 3 + * + * 填充过程详解: + * dp[1][1]: a==a, dp[0][0]+1 = 1 + * dp[1][2]: a!=c, max(dp[0][2], dp[1][1]) = max(0,1) = 1 + * dp[3][2]: c==c, dp[2][1]+1 = 1+1 = 2 + * dp[5][3]: e==e, dp[4][2]+1 = 2+1 = 3 + * + * 最终结果:dp[5][3] = 3,LCS为"ace" + * + * 时间复杂度分析: + * - 初始化DP表:O(m*n) + * - 填充DP表:O(m*n) + * - 总时间复杂度:O(m*n) + * + * 空间复杂度分析: + * - DP表存储空间:O(m*n) + * + * @param text1 第一个字符串 + * @param text2 第二个字符串 + * @return 最长公共子序列的长度 + */ + public static int longestCommonSubsequence(String text1, String text2) { + int m = text1.length(); // text1 的长度 + int n = text2.length(); // text2 的长度 + + // 创建二维数组 dp,大小为 (m + 1) x (n + 1) + // dp[i][j] 表示 text1 前 i 个字符和 text2 前 j 个字符的最长公共子序列长度 + int[][] dp = new int[m + 1][n + 1]; + + // 填充 dp 数组 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + // 如果字符相等,最长公共子序列长度加 1 + if (text1.charAt(i - 1) == text2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + // 否则取左边或上边的最大值(分别表示不包含text1[i-1]或不包含text2[j-1]的情况) + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + // 返回最后一个元素,即最长公共子序列的长度 + return dp[m][n]; + } + + /** + * 扩展方法:找出实际的最长公共子序列 + * + * 算法思路: + * 在计算完DP表后,通过回溯的方式构造出实际的LCS字符串 + * 从DP表的右下角开始,根据状态转移的路径反向构造LCS + * + * 时间复杂度分析: + * - 填充DP表:O(m*n) + * - 回溯构造LCS:O(m+n) + * - 总时间复杂度:O(m*n) + * + * 空间复杂度分析: + * - DP表存储空间:O(m*n) + * - StringBuilder存储LCS:O(min(m,n)) + * + * @param text1 第一个字符串 + * @param text2 第二个字符串 + * @return 最长公共子序列 + */ + public String findLCS(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]); + } + } + } + + // 回溯构造LCS + StringBuilder lcs = new StringBuilder(); + int i = m, j = n; + + while (i > 0 && j > 0) { + if (text1.charAt(i - 1) == text2.charAt(j - 1)) { + // 字符相等,是LCS的一部分 + lcs.append(text1.charAt(i - 1)); + i--; + j--; + } else if (dp[i - 1][j] > dp[i][j - 1]) { + // 上面的值更大,说明不包含text1[i-1] + i--; + } else { + // 左边的值更大或相等,说明不包含text2[j-1] + j--; + } + } + + // 因为是从后往前构造的,需要反转 + return lcs.reverse().toString(); + } +} diff --git a/algorithm/LongestConsecutive128.java b/algorithm/LongestConsecutive128.java new file mode 100644 index 0000000000000..b6f3d35ad58b5 --- /dev/null +++ b/algorithm/LongestConsecutive128.java @@ -0,0 +1,318 @@ +package com.funian.algorithm.algorithm; + +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +/** + * 最长连续序列(LeetCode 128) + * + * 时间复杂度:O(n) + * - 虽然看起来有嵌套循环,但每个元素最多被访问两次 + * - 外层循环遍历所有唯一元素:O(n) + * - 内层while循环只对序列的起始元素执行,总共O(n) + * - 总体时间复杂度为O(n) + * + * 空间复杂度:O(n) + * - 使用HashSet存储所有数组元素:O(n) + */ +public class LongestConsecutive128 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("输入数组:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] strs = line.split(" "); + + // 创建整型数组 + int[] nums = new int[strs.length]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < strs.length; i++) { + nums[i] = Integer.parseInt(strs[i]); + } + + // 调用 longestConsect 方法查找最长连续序列长度 + int result = longestConsect(nums); + + // 输出结果 + System.out.println("最长连续序列长度为:" + result); + } + + /** + * 最长连续子序列 + * 在给定整数数组中找到数字连续的最长序列的长度 + * + * 算法思路: + * 1. 使用HashSet存储所有数字,实现O(1)查找和去重 + * 2. 对于每个数字,只有当它是序列起点时(num-1不存在)才开始计算序列长度 + * 3. 从起点开始向上查找连续数字,计算序列长度 + * 4. 记录并更新最长序列长度 + * + * 示例过程(以数组 [100,4,200,1,3,2] 为例): + * + * numSet = {100, 4, 200, 1, 3, 2} + * + * 遍历过程: + * num=100: num-1=99不存在,是起点 + * 序列: 100,长度: 1 + * num=4: num-1=3存在,不是起点,跳过 + * num=200: num-1=199不存在,是起点 + * 序列: 200,长度: 1 + * num=1: num-1=0不存在,是起点 + * 序列: 1->2->3->4,长度: 4 + * num=3: num-1=2存在,不是起点,跳过 + * num=2: num-1=1存在,不是起点,跳过 + * + * 最长序列: [1,2,3,4],长度: 4 + * + * 时间复杂度分析: + * - 创建HashSet:O(n),其中n为输入数组`nums`的长度 + * - 外层for循环:O(m),其中m为HashSet中唯一元素的个数 + * - 内层while循环:总共O(m)(每个元素最多被访问两次) + * - 虽然有嵌套循环,但只有当元素是连续序列起点时才会执行while循环 + * - 每个元素最多作为起点访问一次,作为序列中元素访问一次 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashSet存储元素:O(m),存储输入数组中的所有唯一元素,最坏情况下m=n + * - 其他变量:O(1),使用常数个额外变量 + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @return 最长连续序列的长度 + */ + public static int longestConsect(int[] nums) { + // 使用HashSet存储所有数字,用于O(1)时间复杂度的查找,并去重 + Set numSet = new HashSet<>(); + + // 遍历输入数组,将所有数字添加到HashSet中 + for (int num : nums) { + numSet.add(num); + } + + // 记录最长连续序列的长度 + int longestCount = 0; + + // 遍历HashSet中的每个数字 + for (int num : numSet) { + // 只有当num-1不存在时,num才是一个序列的起点 + // 这样避免了重复计算序列的一部分 + if (!numSet.contains(num - 1)) { + // 当前序列的起始数字 + int currentNum = num; + + // 当前序列的长度,初始为1(包含起始数字) + int currentCount = 1; + + // 向上查找连续的数字 + while (numSet.contains(currentNum + 1)) { + // 如果存在下一个连续数字,则更新currentNum + currentNum++; + // 增加当前序列长度 + currentCount++; + } + + // 更新最长序列长度 + longestCount = Math.max(longestCount, currentCount); + } + } + + // 返回最长连续序列的长度 + return longestCount; + } + + /** + * 方法2:排序后遍历(时间复杂度较高但思路简单) + * + * 算法思路: + * 1. 先对数组进行排序 + * 2. 遍历排序后的数组,统计连续序列长度 + * + * 示例过程(以数组 [100,4,200,1,3,2] 为例): + * + * 排序后数组: [1, 2, 3, 4, 100, 200] + * + * 遍历过程: + * i=1: nums[1]=2, nums[0]=1, 连续,currentCount=2 + * i=2: nums[2]=3, nums[1]=2, 连续,currentCount=3 + * i=3: nums[3]=4, nums[2]=3, 连续,currentCount=4 + * i=4: nums[4]=100, nums[3]=4, 不连续,更新longestCount=4,重置currentCount=1 + * i=5: nums[5]=200, nums[4]=100, 不连续,longestCount仍为4,currentCount=1 + * + * 最长序列长度: 4 + * + * 时间复杂度分析: + * - 排序操作:O(n log n),其中n为输入数组`nums`的长度 + * - 遍历数组:O(n),单次遍历排序后的数组 + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 排序算法可能需要额外空间:O(log n)到O(n) + * - 快速排序需要O(log n)栈空间 + * - 归并排序需要O(n)额外数组空间 + * - 其他变量:O(1),使用常数个额外变量 + * - 总空间复杂度:O(log n)到O(n) + * + * @param nums 输入的整数数组 + * @return 最长连续序列的长度 + */ + public static int longestConsecutiveSort(int[] nums) { + // 边界条件:空数组 + if (nums.length == 0) { + return 0; + } + + // 对数组进行排序 + java.util.Arrays.sort(nums); + + // 记录最长连续序列长度 + int longestCount = 1; + // 记录当前连续序列长度 + int currentCount = 1; + + // 从第二个元素开始遍历 + for (int i = 1; i < nums.length; i++) { + // 如果当前元素等于前一个元素,跳过(去重) + if (nums[i] == nums[i - 1]) { + continue; + } + // 如果当前元素等于前一个元素+1,说明连续 + else if (nums[i] == nums[i - 1] + 1) { + currentCount++; + } + // 否则,连续序列中断 + else { + // 更新最长序列长度 + longestCount = Math.max(longestCount, currentCount); + // 重置当前序列长度 + currentCount = 1; + } + } + + // 最后一次更新最长序列长度 + return Math.max(longestCount, currentCount); + } + + /** + * 方法3:并查集解法 + * + * 算法思路: + * 1. 使用HashMap存储数字和对应的索引 + * 2. 使用并查集将相邻的数字合并到同一集合 + * 3. 返回最大集合的大小 + * + * 示例过程(以数组 [100,4,200,1,3,2] 为例): + * + * 初始化并查集,每个元素独立成集合: + * 集合: {0}, {1}, {2}, {3}, {4}, {5} + * + * 处理过程: + * i=0, nums[0]=100: 无相邻元素,集合不变 + * i=1, nums[1]=4: 有相邻元素3(nums[4]),合并{1}和{4} -> {1,4} + * i=2, nums[2]=200: 无相邻元素,集合不变 + * i=3, nums[3]=1: 有相邻元素2(nums[5]),合并{3}和{5} -> {3,5} + * i=4, nums[4]=3: 有相邻元素2(nums[5])和4(nums[1]),合并{4,1}和{3,5} -> {1,3,4,5} + * i=5, nums[5]=2: 有相邻元素1(nums[3])和3(nums[4]),已在同一集合 + * + * 最大集合{1,3,4,5}大小为4,对应序列[1,2,3,4] + * + * 时间复杂度分析: + * - 遍历数组:O(n),其中n为输入数组`nums`的长度 + * - 并查集操作:近似O(1)(考虑路径压缩和按秩合并) + * - [find](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/NumIsLands200.java#L311-L319)操作经过路径压缩后接近O(1) + * - [union](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/NumIsLands200.java#L321-L345)操作经过按秩合并后接近O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashMap存储元素:O(m),存储数组中的唯一元素及其索引,m为唯一元素个数 + * - 并查集数组:O(n),[parent](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/NumIsLands200.java#L281-L281)数组和[size](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/LRUCache146.java#L40-L40)数组各需要O(n)空间,n为输入数组长度 + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @return 最长连续序列的长度 + */ + public static int longestConsecutiveUnionFind(int[] nums) { + if (nums.length == 0) { + return 0; + } + + // 使用HashMap存储数字和对应的索引 + java.util.Map map = new java.util.HashMap<>(); + UnionFind uf = new UnionFind(nums.length); + + // 遍历数组,建立并查集 + for (int i = 0; i < nums.length; i++) { + // 如果数字已存在,跳过 + if (map.containsKey(nums[i])) { + continue; + } + + // 将数字和索引关联 + map.put(nums[i], i); + + // 如果前一个数字存在,合并两个集合 + if (map.containsKey(nums[i] - 1)) { + uf.union(i, map.get(nums[i] - 1)); + } + + // 如果后一个数字存在,合并两个集合 + if (map.containsKey(nums[i] + 1)) { + uf.union(i, map.get(nums[i] + 1)); + } + } + + // 返回最大集合的大小 + return uf.getMaxSize(); + } + + /** + * 并查集辅助类 + */ + static class UnionFind { + private int[] parent; + private int[] size; + private int maxSize; + + public UnionFind(int n) { + parent = new int[n]; + size = new int[n]; + maxSize = 1; + + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + public void union(int x, int y) { + int rootX = find(x); + int rootY = find(y); + + if (rootX != rootY) { + // 按秩合并 + parent[rootX] = rootY; + size[rootY] += size[rootX]; + maxSize = Math.max(maxSize, size[rootY]); + } + } + + public int getMaxSize() { + return maxSize; + } + } +} diff --git a/algorithm/LongestPalindrome5.java b/algorithm/LongestPalindrome5.java new file mode 100644 index 0000000000000..1a00353b428b4 --- /dev/null +++ b/algorithm/LongestPalindrome5.java @@ -0,0 +1,212 @@ +package com.funian.algorithm.algorithm; + +/** + * 最长回文子串(LeetCode 5)- 中心扩展法 + * + * 时间复杂度:O(n²) + * - 外层循环运行n次(n为字符串长度) + * - 每次中心扩展最多需要O(n)时间 + * - 总时间复杂度为O(n²) + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 不需要额外的数据结构存储中间结果 + */ +import java.util.Scanner; + +public class LongestPalindrome5 { + + /** + * 主函数:处理用户输入并找出最长回文子串 + * + * 算法流程: + * 1. 读取用户输入的字符串 + * 2. 调用 [longestPalindrome](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/LongestPalindrome5.java#L77-L112)方法找出最长回文子串 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入字符串 + System.out.print("输入字符串: "); + String s = scanner.nextLine(); + + // 找到最长回文子串 + String longestPalindrome = longestPalindrome(s); + System.out.println("最长回文子串是: " + longestPalindrome); + } + + /** + * 找出字符串中的最长回文子串 + * + * 算法思路: + * 使用中心扩展法,遍历每个可能的回文中心,向两边扩展寻找回文 + * 回文有两种情况: + * 1. 奇数长度回文:以单个字符为中心 + * 2. 偶数长度回文:以两个字符之间为中心 + * + * 执行过程分析(以字符串 "babad" 为例): + * + * 遍历每个字符作为中心: + * i=0, 字符'b': + * 奇数回文:以'b'为中心,扩展得到"b",长度1 + * 偶数回文:以'b'和'a'之间为中心,无法扩展,长度0 + * 当前最长回文:"b",长度1 + * + * i=1, 字符'a': + * 奇数回文:以'a'为中心,无法扩展,长度1 + * 偶数回文:以'a'和'b'之间为中心,无法扩展,长度0 + * 当前最长回文:"b",长度1 + * + * i=2, 字符'b': + * 奇数回文:以'b'为中心,向两边扩展得到"bab",长度3 + * 偶数回文:以'b'和'a'之间为中心,无法扩展,长度0 + * 更新最长回文:"bab",长度3 + * + * i=3, 字符'a': + * 奇数回文:以'a'为中心,无法扩展,长度1 + * 偶数回文:以'a'和'd'之间为中心,无法扩展,长度0 + * 当前最长回文:"bab",长度3 + * + * i=4, 字符'd': + * 奇数回文:以'd'为中心,扩展得到"d",长度1 + * 偶数回文:无 + * 当前最长回文:"bab",长度3 + * + * 再以字符串 "cbbd" 为例: + * + * i=0, 字符'c': 最长回文"c",长度1 + * i=1, 字符'b': + * 奇数回文:"b",长度1 + * 偶数回文:以'b'和'b'之间为中心,扩展得到"bb",长度2 + * 更新最长回文:"bb",长度2 + * i=2, 字符'b': 最长回文仍为"bb",长度2 + * i=3, 字符'd': 最长回文仍为"bb",长度2 + * + * 时间复杂度分析: + * - 外层循环运行n次(n为字符串长度):O(n) + * - 每次中心扩展最多需要O(n)时间:O(n) + * - 总时间复杂度为O(n²) + * + * 空间复杂度分析: + * - 只使用了常数个额外变量:O(1) + * - 不需要额外的数据结构存储中间结果:O(1) + * + * @param s 输入字符串 + * @return 最长回文子串 + */ + public static String longestPalindrome(String s) { + // 边界情况:空字符串或长度为0的字符串 + if (s == null || s.length() < 1) return ""; + + // 记录最长回文子串的起始和结束位置 + int start = 0, end = 0; + + // 遍历每个字符作为回文中心 + for (int i = 0; i < s.length(); i++) { + // 尝试以 s[i] 为中心的奇数长度回文 + int len1 = expandAroundCenter(s, i, i); + // 尝试以 s[i] 和 s[i+1] 为中心的偶数长度回文 + int len2 = expandAroundCenter(s, i, i + 1); + + // 取两种情况下的最大长度 + int len = Math.max(len1, len2); + + // 如果当前回文长度大于已记录的最长回文长度,更新起始和结束位置 + if (len > end - start) { + // 计算新的起始位置 + start = i - (len - 1) / 2; + // 计算新的结束位置 + end = i + len / 2; + } + } + + // 返回最长回文子串 + return s.substring(start, end + 1); + } + + /** + * 从中心向两边扩展寻找回文 + * + * 算法思路: + * 从给定的中心位置向两边扩展,直到字符不匹配或到达边界 + * + * 时间复杂度分析: + * - 最多扩展n次:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param s 输入字符串 + * @param left 左边界 + * @param right 右边界 + * @return 回文的长度 + */ + private static int expandAroundCenter(String s, int left, int right) { + // 当左右字符相等且边界有效时,继续向外扩展 + while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) { + left--; // 左边界左移 + right++; // 右边界右移 + } + // 返回回文的长度 + return right - left - 1; + } + + /** + * 方法2:动态规划解法 + * + * 算法思路: + * 使用二维DP表,dp[i][j]表示s[i...j]是否为回文 + * 状态转移方程: + * dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1] + * + * 时间复杂度分析: + * - 初始化单字符回文:O(n) + * - 检查长度为2的子串:O(n) + * - 检查长度大于2的子串:O(n²) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 二维DP表:O(n²) + * + * @param s 输入字符串 + * @return 最长回文子串 + */ + public static String longestPalindromeDP(String s) { + if (s == null || s.length() < 1) return ""; + + int n = s.length(); + boolean[][] dp = new boolean[n][n]; + int start = 0; + int maxLength = 1; + + // 单个字符都是回文 + for (int i = 0; i < n; i++) { + dp[i][i] = true; + } + + // 检查长度为2的子串 + for (int i = 0; i < n - 1; i++) { + if (s.charAt(i) == s.charAt(i + 1)) { + dp[i][i + 1] = true; + start = i; + maxLength = 2; + } + } + + // 检查长度大于2的子串 + for (int len = 3; len <= n; len++) { + for (int i = 0; i < n - len + 1; i++) { + int j = i + len - 1; + + if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) { + dp[i][j] = true; + start = i; + maxLength = len; + } + } + } + + return s.substring(start, start + maxLength); + } +} diff --git a/algorithm/LongestSubstring3.java b/algorithm/LongestSubstring3.java new file mode 100644 index 0000000000000..569cef7e1b6c7 --- /dev/null +++ b/algorithm/LongestSubstring3.java @@ -0,0 +1,245 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +/** + * 无重复字符的最长子串(LeetCode 3) + * + * 时间复杂度:O(n) + * - 虽然有嵌套结构,但每个字符最多被访问两次(一次被右指针访问,一次被左指针访问) + * - 左右指针都只向前移动,不会回退 + * + * 空间复杂度:O(min(m,n)) + * - m 是字符集大小(如ASCII字符集大小为128) + * - HashSet 最多存储 min(m,n) 个字符 + * - n 是字符串长度 + */ +public class LongestSubstring3 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入字符串 + System.out.println("输入字符串:"); + + // 读取用户输入的字符串 + String s = scanner.nextLine(); + + // 调用 longestSubstring 方法计算最长无重复子串长度 + int result = longestSubstring(s); + + // 输出结果 + System.out.println("不重复的字符串长度为:" + result); + } + + /** + * 计算字符串中无重复字符的最长子串长度 + * 使用滑动窗口 + HashSet 的方法 + * + * 算法思路: + * 1. 使用双指针维护一个滑动窗口 [left, right) + * 2. 使用 HashSet 记录当前窗口中包含的字符 + * 3. 右指针不断向右扩展窗口 + * 4. 当遇到重复字符时,左指针向右收缩窗口直到没有重复字符 + * 5. 记录过程中窗口的最大长度 + * + * 示例过程(以字符串 "abcabcbb" 为例): + * 步骤 窗口 HashSet maxLen + * 1 [a) {a} 1 + * 2 [ab) {a,b} 2 + * 3 [abc) {a,b,c} 3 + * 4 [abca) 发现重复a - + * 5 [bca) {b,c,a} 3 + * 6 [bcab) 发现重复b - + * 7 [cab) {c,a,b} 3 + * 8 [cabc) 发现重复c - + * 9 [abc) {a,b,c} 3 + * 10 [abcb) 发现重复b - + * 11 [bcb) 发现重复b - + * 12 [cb) {c,b} 3 + * 13 [cbb) 发现重复b - + * 14 [bb) 发现重复b - + * 15 [b) {b} 3 + * 16 [) {} 3 + * 结果:最长无重复子串长度为3 + * + * 时间复杂度分析: + * - 右指针遍历字符串:O(n),其中n为输入字符串`s`的长度 + * - 左指针最多移动n次:O(n) + * - HashSet操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashSet存储字符:O(min(m,n)),m为字符集大小,n为字符串长度 + * - 其他变量:O(1) + * - 总空间复杂度:O(min(m,n)) + * + * @param s 输入的字符串 + * @return 无重复字符的最长子串长度 + */ + public static int longestSubstring(String s) { + // 使用 HashSet 存储当前窗口中的字符,用于快速判断是否有重复 + Set set = new HashSet<>(); + + // 滑动窗口的左边界(包含) + int left = 0; + + // 滑动窗口的右边界(不包含) + int right = 0; + + // 获取字符串长度 + int n = s.length(); + + // 记录最长子串长度 + int maxLength = 0; + + // 右指针遍历整个字符串 + while (right < n) { + // 如果右指针指向的字符不在当前窗口中 + if (!set.contains(s.charAt(right))) { + // 将该字符加入 HashSet + set.add(s.charAt(right)); + + // 右指针右移,扩展窗口 + right++; + + // 更新最大长度(窗口大小为 right - left) + maxLength = Math.max(maxLength, right - left); + } else { + // 如果右指针指向的字符在当前窗口中(出现重复) + // 从 HashSet 中移除左指针指向的字符 + set.remove(s.charAt(left)); + + // 左指针右移,收缩窗口 + left++; + } + } + + // 返回最长无重复子串的长度 + return maxLength; + } + + /** + * 方法2:优化的滑动窗口(使用HashMap记录字符位置) + * + * 算法思路: + * 使用HashMap记录每个字符最后出现的位置,当遇到重复字符时可以直接跳转 + * + * 示例过程(以字符串 "abcabcbb" 为例): + * + * 初始化: map = {}, left = 0, maxLength = 0 + * + * right=0, c='a': map不包含'a',更新map={'a':0},maxLength=max(0,0-0+1)=1 + * right=1, c='b': map不包含'b',更新map={'a':0,'b':1},maxLength=max(1,1-0+1)=2 + * right=2, c='c': map不包含'c',更新map={'a':0,'b':1,'c':2},maxLength=max(2,2-0+1)=3 + * right=3, c='a': map包含'a'且位置0>=left(0),left=0+1=1,更新map={'a':3,'b':1,'c':2},maxLength=max(3,3-1+1)=3 + * right=4, c='b': map包含'b'且位置1>=left(1),left=1+1=2,更新map={'a':3,'b':4,'c':2},maxLength=max(3,4-2+1)=3 + * right=5, c='c': map包含'c'且位置2>=left(2),left=2+1=3,更新map={'a':3,'b':4,'c':5},maxLength=max(3,5-3+1)=3 + * right=6, c='b': map包含'b'且位置4>=left(3),left=4+1=5,更新map={'a':3,'b':6,'c':5},maxLength=max(3,6-5+1)=2 + * right=7, c='b': map包含'b'且位置6>=left(5),left=6+1=7,更新map={'a':3,'b':7,'c':5},maxLength=max(2,7-7+1)=2 + * + * 最终结果:maxLength = 3 + * + * 时间复杂度分析: + * - 单次遍历字符串:O(n),其中n为输入字符串`s`的长度 + * - HashMap操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashMap存储字符位置:O(min(m,n)),m为字符集大小,n为字符串长度 + * - 其他变量:O(1) + * - 总空间复杂度:O(min(m,n)) + * + * @param s 输入的字符串 + * @return 无重复字符的最长子串长度 + */ + public static int longestSubstringOptimized(String s) { + // 使用HashMap存储字符及其最后出现的位置 + java.util.Map map = new java.util.HashMap<>(); + int left = 0; + int maxLength = 0; + + // 遍历字符串 + for (int right = 0; right < s.length(); right++) { + char c = s.charAt(right); + + // 如果字符已存在且在当前窗口内 + if (map.containsKey(c) && map.get(c) >= left) { + // 直接将左指针跳转到重复字符的下一个位置 + left = map.get(c) + 1; + } + + // 更新字符的最新位置 + map.put(c, right); + + // 更新最大长度 + maxLength = Math.max(maxLength, right - left + 1); + } + + return maxLength; + } + + /** + * 方法3:使用数组代替HashSet(适用于ASCII字符) + * + * 算法思路: + * 使用布尔数组代替HashSet,提高访问速度 + * + * 示例过程(以字符串 "abcabcbb" 为例): + * + * 初始化: visited = [128个false], left = 0, maxLength = 0 + * + * right=0, c='a'(97): visited[97]=false,标记visited[97]=true,maxLength=max(0,0-0+1)=1 + * right=1, c='b'(98): visited[98]=false,标记visited[98]=true,maxLength=max(1,1-0+1)=2 + * right=2, c='c'(99): visited[99]=false,标记visited[99]=true,maxLength=max(2,2-0+1)=3 + * right=3, c='a'(97): visited[97]=true,进入while循环 + * s.charAt(0)='a'==c,跳出while,left=1,标记visited[97]=true,maxLength=max(3,3-1+1)=3 + * right=4, c='b'(98): visited[98]=true,进入while循环 + * s.charAt(1)='b'==c,跳出while,left=2,标记visited[98]=true,maxLength=max(3,4-2+1)=3 + * ... + * + * 时间复杂度分析: + * - 单次遍历字符串:O(n),其中n为输入字符串`s`的长度 + * - 数组访问:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 布尔数组:O(128) = O(1),固定大小的ASCII字符集 + * - 其他变量:O(1) + * - 总空间复杂度:O(1) + * + * @param s 输入的字符串 + * @return 无重复字符的最长子串长度 + */ + public static int longestSubstringArray(String s) { + // 使用布尔数组记录字符是否在当前窗口中(假设为ASCII字符) + boolean[] visited = new boolean[128]; + int left = 0; + int maxLength = 0; + + for (int right = 0; right < s.length(); right++) { + char c = s.charAt(right); + + // 如果字符已在当前窗口中 + if (visited[c]) { + // 收缩窗口直到没有重复字符 + while (s.charAt(left) != c) { + visited[s.charAt(left)] = false; + left++; + } + // 跳过重复字符 + left++; + } + + // 标记字符为已访问 + visited[c] = true; + + // 更新最大长度 + maxLength = Math.max(maxLength, right - left + 1); + } + + return maxLength; + } +} diff --git a/algorithm/LongestValidParentheses32.java b/algorithm/LongestValidParentheses32.java new file mode 100644 index 0000000000000..473be2cd62a0e --- /dev/null +++ b/algorithm/LongestValidParentheses32.java @@ -0,0 +1,289 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Stack; + +/** + * 最长有效括号(LeetCode 32)- 动态规划/栈 + * + * 时间复杂度: + * - 方法1(动态规划):O(n) + * 只需要遍历字符串一次 + * - 方法2(栈):O(n) + * 只需要遍历字符串一次 + * + * 空间复杂度: + * - 方法1(动态规划):O(n) + * 需要长度为n的DP数组 + * - 方法2(栈):O(n) + * 栈最多存储n个元素 + */ +public class LongestValidParentheses32 { + + /** + * 计算最长有效括号子串的长度 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示以`s[i]`结尾的最长有效括号子串的长度 + * + * 状态转移: + * 1. 如果`s[i]` = '(',则`dp[i]` = 0(以'('结尾不可能是有效括号) + * 2. 如果`s[i]` = ')': + * a. 如果`s[i-1]` = '(',则`dp[i]` = `dp[i-2]` + 2 + * b. 如果`s[i-1]` = ')'且`dp[i-1]` > 0: + * - 如果`s[i-dp[i-1]-1]` = '(',则`dp[i]` = `dp[i-1]` + 2 + `dp[i-dp[i-1]-2]` + * + * 执行过程分析(以`s=")()())"`为例): + * + * 初始化DP数组: + * dp = [0, 0, 0, 0, 0, 0] + * 索引: 0 1 2 3 4 5 + * 字符: ) ( ) ( ) ) + * + * 填充DP数组: + * i=0, `s[0]=')'`: `dp[0]` = 0 + * + * i=1, `s[1]='('`: `dp[1]` = 0 + * + * i=2, `s[2]=')'`, `s[1]='('`: + * `dp[2]` = `dp[0]` + 2 = 0 + 2 = 2 + * dp = [0, 0, 2, 0, 0, 0] + * + * i=3, `s[3]='('`: `dp[3]` = 0 + * + * i=4, `s[4]=')'`, `s[3]='('`: + * `dp[4]` = `dp[2]` + 2 = 2 + 2 = 4 + * dp = [0, 0, 2, 0, 4, 0] + * + * i=5, `s[5]=')'`, `s[4]=')'`, `dp[4]=4`: + * 检查`s[5-dp[4]-1]` = `s[5-4-1]` = `s[0]` = ')' + * 不匹配,`dp[5]` = 0 + * dp = [0, 0, 2, 0, 4, 0] + * + * 最终结果:max(dp) = 4 + * 最长有效括号子串:"()()",长度为4 + * + * 时间复杂度分析: + * - 遍历字符串一次:O(n) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * + * @param s 输入的括号字符串 + * @return 最长有效括号子串的长度 + */ + public int longestValidParenthesesDP(String s) { + if (s == null || s.length() <= 1) return 0; + + // dp[i] 表示以 s[i] 结尾的最长有效括号子串的长度 + int[] dp = new int[s.length()]; + int maxLength = 0; + + // 从第二个字符开始遍历 + for (int i = 1; i < s.length(); i++) { + // 只有当当前字符是 ')' 时才可能形成有效括号 + if (s.charAt(i) == ')') { + // 情况1:s[i-1] = '(',即 "...()" + if (s.charAt(i - 1) == '(') { + // 长度为前前位置的最长长度 + 2 + dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; + } + // 情况2:s[i-1] = ')' 且 dp[i-1] > 0,即 "...))" 形式 + else if (dp[i - 1] > 0) { + // 计算匹配位置 + int matchIndex = i - dp[i - 1] - 1; + // 如果匹配位置有效且为 '(' + if (matchIndex >= 0 && s.charAt(matchIndex) == '(') { + // 长度为中间部分长度 + 2 + 匹配位置前的最长长度 + dp[i] = dp[i - 1] + 2 + (matchIndex > 0 ? dp[matchIndex - 1] : 0); + } + } + } + + // 更新最大长度 + maxLength = Math.max(maxLength, dp[i]); + } + + return maxLength; + } + + /** + * 方法2:使用栈解法 + * + * 算法思路: + * 使用栈存储字符下标,维护有效括号的边界 + * 1. 栈底始终保存最后一个未匹配的')'的下标或-1 + * 2. 遇到'('时将其下标入栈 + * 3. 遇到')'时弹出栈顶元素: + * - 如果栈为空,说明当前')'无法匹配,将其下标入栈作为新的边界 + * - 如果栈不为空,当前有效长度为 i - stack.peek() + * + * 执行过程分析(以`s=")()())"`为例): + * + * 初始状态: + * stack = [-1] + * maxLength = 0 + * + * 遍历过程: + * i=0, `s[0]=')'`: + * 弹出-1,栈为空 + * 将0入栈,stack = [0] + * + * i=1, `s[1]='('`: + * 将1入栈,stack = [0, 1] + * + * i=2, `s[2]=')'`: + * 弹出1,栈不为空 + * 当前长度 = 2 - 0 = 2 + * maxLength = max(0, 2) = 2 + * + * i=3, `s[3]='('`: + * 将3入栈,stack = [0, 3] + * + * i=4, `s[4]=')'`: + * 弹出3,栈不为空 + * 当前长度 = 4 - 0 = 4 + * maxLength = max(2, 4) = 4 + * + * i=5, `s[5]=')'`: + * 弹出0,栈为空 + * 将5入栈,stack = [5] + * + * 最终结果:maxLength = 4 + * + * 时间复杂度分析: + * - 遍历字符串一次:O(n) + * + * 空间复杂度分析: + * - 栈存储空间:O(n) + * + * @param s 输入的括号字符串 + * @return 最长有效括号子串的长度 + */ + public int longestValidParenthesesStack(String s) { + if (s == null || s.length() <= 1) return 0; + + Stack stack = new Stack<>(); + // 栈底初始化为-1,作为边界 + stack.push(-1); + int maxLength = 0; + + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '(') { + // 遇到'(',将其下标入栈 + stack.push(i); + } else { + // 遇到')',弹出栈顶元素 + stack.pop(); + + if (stack.isEmpty()) { + // 如果栈为空,说明当前')'无法匹配 + // 将其下标入栈作为新的边界 + stack.push(i); + } else { + // 如果栈不为空,计算当前有效长度 + int currentLength = i - stack.peek(); + maxLength = Math.max(maxLength, currentLength); + } + } + } + + return maxLength; + } + + /** + * 方法3:双向扫描解法 + * + * 算法思路: + * 从左到右扫描一次,从右到左扫描一次 + * 分别统计左右括号的数量,当数量相等时更新最大长度 + * + * 时间复杂度分析: + * - 双向扫描:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param s 输入的括号字符串 + * @return 最长有效括号子串的长度 + */ + public int longestValidParenthesesScan(String s) { + if (s == null || s.length() <= 1) return 0; + + int left = 0, right = 0; + int maxLength = 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; + } + + /** + * 辅助方法:读取用户输入的字符串 + * + * 时间复杂度分析: + * - 读取输入:O(n) + * + * 空间复杂度分析: + * - 存储字符串:O(n) + * + * @return 用户输入的字符串 + */ + public static String readString() { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入括号字符串:"); + return scanner.nextLine(); + } + + /** + * 主函数:处理用户输入并计算最长有效括号子串的长度 + */ + public static void main(String[] args) { + System.out.println("最长有效括号"); + + // 读取用户输入的字符串 + String s = readString(); + System.out.println("输入字符串: \"" + s + "\""); + + // 计算最长有效括号子串的长度 + LongestValidParentheses32 solution = new LongestValidParentheses32(); + int result1 = solution.longestValidParenthesesDP(s); + int result2 = solution.longestValidParenthesesStack(s); + int result3 = solution.longestValidParenthesesScan(s); + + // 输出结果 + System.out.println("动态规划方法结果: " + result1); + System.out.println("栈方法结果: " + result2); + System.out.println("双向扫描方法结果: " + result3); + } +} diff --git a/algorithm/MajorityElement169.java b/algorithm/MajorityElement169.java new file mode 100644 index 0000000000000..1ba2a8a6d30ee --- /dev/null +++ b/algorithm/MajorityElement169.java @@ -0,0 +1,225 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 多数元素(LeetCode 169) + * + * 时间复杂度: + * - 方法1(哈希表):O(n) + * 需要遍历数组一次 + * - 方法2(排序):O(n log n) + * 排序需要O(n log n)时间 + * - 方法3(Boyer-Moore投票算法):O(n) + * 需要遍历数组一次 + * + * 空间复杂度: + * - 方法1(哈希表):O(n) + * 需要哈希表存储元素计数 + * - 方法2(排序):O(1) + * 如果允许修改原数组,则为O(1);否则需要O(n)空间复制数组 + * - 方法3(Boyer-Moore投票算法):O(1) + * 只使用常数个额外变量 + */ +public class MajorityElement169 { + + /** + * 方法1:哈希表解法 + * + * 算法思路: + * 使用哈希表统计每个元素出现的次数,然后找出出现次数超过n/2的元素 + * + * 执行过程分析(以数组 [2,2,1,1,1,2,2] 为例): + * + * 遍历过程: + * 1. 处理2:map={2:1} + * 2. 处理2:map={2:2} + * 3. 处理1:map={2:2, 1:1} + * 4. 处理1:map={2:2, 1:2} + * 5. 处理1:map={2:2, 1:3} + * 6. 处理2:map={2:3, 1:3} + * 7. 处理2:map={2:4, 1:3} + * + * 检查计数: + * n=7, n/2=3 + * 2的计数为4 > 3,返回2 + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * - 哈希表操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 哈希表存储元素计数:O(n) + * - 最坏情况下需要存储所有不同元素:O(n) + * + * @param nums 整数数组 + * @return 多数元素 + */ + public int majorityElementHashMap(int[] nums) { + Map countMap = new HashMap<>(); + int n = nums.length; + int majorityThreshold = n / 2; + + // 统计每个元素的出现次数 + for (int num : nums) { + countMap.put(num, countMap.getOrDefault(num, 0) + 1); + + // 如果某个元素的计数超过n/2,直接返回 + if (countMap.get(num) > majorityThreshold) { + return num; + } + } + + // 根据题目保证,一定存在多数元素,所以这里不会执行到 + return -1; + } + + /** + * 方法2:排序解法 + * + * 算法思路: + * 对数组进行排序,多数元素一定会出现在数组的中间位置 + * 因为多数元素出现次数超过n/2,所以无论在中间位置的左边还是右边, + * 中间位置都一定是多数元素 + * + * 执行过程分析(以数组 [2,2,1,1,1,2,2] 为例): + * + * 排序前:[2,2,1,1,1,2,2] + * 排序后:[1,1,1,2,2,2,2] + * 中间位置:7/2 = 3 + * nums[3] = 2,即为多数元素 + * + * 时间复杂度分析: + * - 排序操作:O(n log n) + * - 访问中间元素:O(1) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 如果允许修改原数组,则为O(1) + * - 否则需要O(n)空间复制数组 + * - 排序算法本身可能需要额外空间,取决于具体实现 + * + * @param nums 整数数组 + * @return 多数元素 + */ + public int majorityElementSort(int[] nums) { + Arrays.sort(nums); + return nums[nums.length / 2]; + } + + /** + * 方法3:Boyer-Moore投票算法(最优解) + * + * 算法思路: + * 将多数元素记为+1,其他元素记为-1,将它们加起来,和一定大于0 + * 使用两个变量: + * 1. candidate:候选的多数元素 + * 2. count:投票计数器 + * + * 执行过程分析(以数组 [2,2,1,1,1,2,2] 为例): + * + * 初始状态:candidate=0, count=0 + * + * 遍历过程: + * 1. 处理2:count=0,candidate=2, count=1 + * 2. 处理2:count=1,candidate=2, count=2 + * 3. 处理1:count=2,1≠2, count=1 + * 4. 处理1:count=1,1≠2, count=0 + * 5. 处理1:count=0,candidate=1, count=1 + * 6. 处理2:count=1,1≠2, count=0 + * 7. 处理2:count=0,candidate=2, count=1 + * + * 最终candidate=2,即为多数元素 + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * - candidate和count两个变量 + * + * @param nums 整数数组 + * @return 多数元素 + */ + public int majorityElement(int[] nums) { + int candidate = 0; + int count = 0; + + // 投票过程 + for (int num : nums) { + if (count == 0) { + // 如果计数为0,更新候选元素 + candidate = num; + } + + // 根据当前元素与候选元素是否相同更新计数 + if (num == candidate) { + count++; + } else { + count--; + } + } + + // 返回候选元素(根据题目保证,一定是多数元素) + return candidate; + } + + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 读取输入:O(m),m为输入字符数 + * - 解析为整数:O(n),n为数组长度 + * - 总时间复杂度:O(m+n) + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * - 总空间复杂度:O(m+n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入整数数组(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 主函数:处理用户输入并找出多数元素 + */ + public static void main(String[] args) { + System.out.println("多数元素查找"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("输入的数组: " + Arrays.toString(nums)); + + // 计算多数元素 + MajorityElement169 solution = new MajorityElement169(); + int result1 = solution.majorityElementHashMap(nums); + int result2 = solution.majorityElementSort(nums); + int result3 = solution.majorityElement(nums); + + // 输出结果 + System.out.println("哈希表方法结果: " + result1); + System.out.println("排序方法结果: " + result2); + System.out.println("Boyer-Moore投票算法结果: " + result3); + } + +} diff --git a/algorithm/MaxDepthBinaryTree104.java b/algorithm/MaxDepthBinaryTree104.java new file mode 100644 index 0000000000000..92350775ce674 --- /dev/null +++ b/algorithm/MaxDepthBinaryTree104.java @@ -0,0 +1,257 @@ +package com.funian.algorithm.algorithm; + +import java.util.LinkedList; +import java.util.Queue; +import java.util.Scanner; + +/** + * 二叉树的最大深度(LeetCode 104) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度: + * - 方法1(递归DFS):O(h) + * h是二叉树的高度,递归调用栈的深度 + * 最坏情况下(完全不平衡的树)为O(n),最好情况下(完全平衡的树)为O(log n) + * - 方法2(迭代BFS):O(w) + * w是二叉树的最大宽度,队列最多存储一层的所有节点 + */ +public class MaxDepthBinaryTree104 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + TreeNode(int val) { this.val = val; } + } + + /** + * 方法1:递归解法(深度优先搜索) + * + * 算法思路: + * 二叉树的最大深度等于左子树和右子树最大深度的最大值加1 + * 递归计算左右子树的深度,取较大值加1 + * + * 执行过程分析(以二叉树 [3,9,20,null,null,15,7] 为例): + * + * 3 + * / \ + * 9 20 + * / \ + * 15 7 + * + * 递归调用过程: + * maxDepth(3) + * ├─ maxDepth(9) + * │ ├─ maxDepth(null) -> 0 + * │ └─ maxDepth(null) -> 0 + * │ └─ return max(0,0)+1 = 1 + * ├─ maxDepth(20) + * │ ├─ maxDepth(15) + * │ │ ├─ maxDepth(null) -> 0 + * │ │ └─ maxDepth(null) -> 0 + * │ │ └─ return max(0,0)+1 = 1 + * │ ├─ maxDepth(7) + * │ │ ├─ maxDepth(null) -> 0 + * │ │ └─ maxDepth(null) -> 0 + * │ │ └─ return max(0,0)+1 = 1 + * │ └─ return max(1,1)+1 = 2 + * └─ return max(1,2)+1 = 3 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 最坏情况(链状树):O(n) + * - 最好情况(平衡树):O(log n) + * + * @param root 二叉树的根节点 + * @return 二叉树的最大深度 + */ + public int maxDepthRecursive(TreeNode root) { + // 基础情况:如果节点为空,深度为0 + if (root == null) { + return 0; + } + + // 递归计算左子树的最大深度 + int leftDepth = maxDepthRecursive(root.left); + + // 递归计算右子树的最大深度 + int rightDepth = maxDepthRecursive(root.right); + + // 返回左右子树最大深度的最大值加1 + return Math.max(leftDepth, rightDepth) + 1; + } + + /** + * 方法2:迭代解法(广度优先搜索,层序遍历) + * + * 算法思路: + * 使用队列进行层序遍历,每遍历完一层,深度加1 + * + * 执行过程分析(以二叉树 [3,9,20,null,null,15,7] 为例): + * + * 3 + * / \ + * 9 20 + * / \ + * 15 7 + * + * 执行步骤: + * 第1层:队列=[3],size=1 + * 弹出3,加入9和20,队列=[9,20] + * depth=1 + * + * 第2层:队列=[9,20],size=2 + * 弹出9,无子节点,队列=[20] + * 弹出20,加入15和7,队列=[15,7] + * depth=2 + * + * 第3层:队列=[15,7],size=2 + * 弹出15,无子节点,队列=[7] + * 弹出7,无子节点,队列=[] + * depth=3 + * + * 队列为空,返回depth=3 + * + * 时间复杂度分析: + * - 每个节点入队和出队一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 最坏情况:O(n),最好情况:O(1) + * + * @param root 二叉树的根节点 + * @return 二叉树的最大深度 + */ + public int maxDepth(TreeNode root) { + // 如果根节点为空,树的深度为 0 + if (root == null) { + return 0; + } + + // 使用队列进行广度优先搜索(BFS) + Queue queue = new LinkedList(); + + // 将根节点加入队列 + queue.offer(root); + + // 用于存储树的深度 + int depth = 0; + + // 当队列不为空时,说明还有节点未遍历 + while (!queue.isEmpty()) { + // 获取当前层的节点数 + int size = queue.size(); + + // 遍历当前层的所有节点 + while (size > 0) { + // 弹出队列中的节点 + TreeNode node = queue.poll(); + + // 如果当前节点有左子节点,将左子节点加入队列 + if (node.left != null) { + queue.offer(node.left); + } + + // 如果当前节点有右子节点,将右子节点加入队列 + if (node.right != null) { + queue.offer(node.right); + } + + // 当前层节点数量减1 + size--; + } + + // 每遍历完一层,深度加1 + depth++; + } + + // 返回二叉树的最大深度 + return depth; + } + + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * 注意:这里采用层序遍历的输入方式,null表示空节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取一行输入 + String input = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] values = input.split(" "); + + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + queue.offer(root); + + // i 数组索引 + int i = 1; + while (!queue.isEmpty() && i < values.length) { + TreeNode node = queue.poll(); + + // 处理左子节点 + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + node.left = new TreeNode(Integer.parseInt(values[i])); + queue.offer(node.left); + } + i++; + + // 处理右子节点 + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + node.right = new TreeNode(Integer.parseInt(values[i])); + queue.offer(node.right); + } + i++; + } + + // 返回根节点 + return root; + } + + /** + * 主函数:处理用户输入并计算二叉树的最大深度 + */ + public static void main(String[] args) { + // 打印标题 + System.out.println("二叉树最大深度计算"); + + // 创建二叉树 + TreeNode root = createTree(); + + if (root == null) { + System.out.println("创建的二叉树为空,最大深度为: 0"); + return; + } + + System.out.println("二叉树创建成功"); + + // 计算最大深度 + // 创建解决方案实例 + MaxDepthBinaryTree104 solution = new MaxDepthBinaryTree104(); + int depth1 = solution.maxDepthRecursive(root); + int depth2 = solution.maxDepth(root); + + // 打印结果 + System.out.println("递归方法计算的最大深度: " + depth1); + System.out.println("迭代方法计算的最大深度: " + depth2); + } +} diff --git a/algorithm/MaxPathSum124.java b/algorithm/MaxPathSum124.java new file mode 100644 index 0000000000000..31ecd6bf87bbe --- /dev/null +++ b/algorithm/MaxPathSum124.java @@ -0,0 +1,178 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 二叉树中的最大路径和(LeetCode 124) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(h) + * - h是二叉树的高度 + * - 递归调用栈的深度 + */ +public class MaxPathSum124 { + + // 定义二叉树节点类 + static class TreeNode { + int val; // 节点值 + TreeNode left; // 左子节点 + TreeNode right; // 右子节点 + TreeNode(int x) { val = x; } // 构造函数 + } + + // 记录最大路径和 + private int maxPathSum = Integer.MIN_VALUE; + + /** + * 主方法,计算二叉树的最大路径和 + * + * 算法思路: + * 对于每个节点,我们考虑经过该节点的最大路径和 + * 路径可以是: + * 1. 只包含当前节点 + * 2. 当前节点 + 左子树路径 + * 3. 当前节点 + 右子树路径 + * 4. 左子树路径 + 当前节点 + 右子树路径 + * + * 执行过程分析(以二叉树 [1,2,3] 为例): + * + * 1 + * / \ + * 2 3 + * + * 计算过程: + * 1. 节点2:最大贡献值为2 + * 2. 节点3:最大贡献值为3 + * 3. 节点1:最大路径和为2+1+3=6 + * + * 时间复杂度分析: + * - 调用 [maxGain](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/MaxPathSum124.java#L101-L124) 方法遍历所有节点:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * + * @param root 二叉树的根节点 + * @return 二叉树的最大路径和 + */ + public int maxPathSum(TreeNode root) { + // 重置最大路径和 + maxPathSum = Integer.MIN_VALUE; + // 调用辅助方法计算最大贡献值 + maxGain(root); + // 返回最大路径和 + return maxPathSum; + } + + /** + * 辅助方法,计算节点的最大贡献值 + * + * 算法思路: + * 1. 递归计算左右子树的最大贡献值 + * 2. 计算经过当前节点的最大路径和 + * 3. 更新全局最大路径和 + * 4. 返回当前节点对父节点的最大贡献值 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @return 当前节点对父节点的最大贡献值 + */ + private int maxGain(TreeNode node) { + if (node == null) { + return 0; + } + + // 递归计算左子树和右子树的最大贡献值 + int leftGain = Math.max(maxGain(node.left), 0); + int rightGain = Math.max(maxGain(node.right), 0); + + // 更新最大路径和 + maxPathSum = Math.max(maxPathSum, node.val + leftGain + rightGain); + + // 返回当前节点的最大贡献值 + return node.val + Math.max(leftGain, rightGain); + } + + /** + * 创建二叉树的方法 + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用数组存储节点,根据完全二叉树的性质确定父子关系 + * + * 时间复杂度分析: + * - 遍历所有输入节点:O(n) + * + * 空间复杂度分析: + * - 存储节点数组:O(n) + * + * @return 构建完成的二叉树根节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入 + System.out.println("请输入树的节点值,以-1表示空节点(按层次遍历输入):"); + // 读取输入并分割 + String input = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] values = input.split(" "); + + if (values.length == 0 || values[0].equals("-1")) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 存储所有节点 + TreeNode[] nodes = new TreeNode[values.length]; + // 将根节点放入数组中 + nodes[0] = root; + + // 按层次遍历构建树 + for (int i = 1; i < values.length; i++) { + if (!values[i].equals("-1")) { + // 创建新节点 + nodes[i] = new TreeNode(Integer.parseInt(values[i])); + // 判断当前节点是左子节点还是右子节点 + if (i % 2 == 1) { + nodes[(i - 1) / 2].left = nodes[i]; + } else { + nodes[(i - 2) / 2].right = nodes[i]; + } + } else { + nodes[i] = null; + } + } + + // 返回构建完成的树的根节点 + return root; + } + + /** + * 主函数:处理用户输入并计算二叉树中的最大路径和 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 调用方法计算最大路径和 + * 4. 输出计算结果 + */ + public static void main(String[] args) { + // 创建Solution对象 + MaxPathSum124 solution = new MaxPathSum124(); + // 创建二叉树 + TreeNode root = createTree(); + // 计算最大路径和 + int result = solution.maxPathSum(root); + // 输出结果 + System.out.println("二叉树的最大路径和为:" + result); + } +} diff --git a/algorithm/MaxProduct152.java b/algorithm/MaxProduct152.java new file mode 100644 index 0000000000000..9c862cc321a86 --- /dev/null +++ b/algorithm/MaxProduct152.java @@ -0,0 +1,246 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 乘积最大子数组(LeetCode 152)- 动态规划 + * + * 时间复杂度:O(n) + * - n是数组长度 + * - 只需要遍历数组一次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class MaxProduct152 { + + /** + * 主函数:处理用户输入并计算乘积最大的连续子数组的乘积 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用 [maxProduct](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/MaxProduct152.java#L125-L162)方法计算最大乘积 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入数组 + System.out.print("请输入整数数组(用空格分隔):"); + String[] input = scanner.nextLine().split(" "); + int[] nums = new int[input.length]; + for (int i = 0; i < input.length; i++) { + nums[i] = Integer.parseInt(input[i]); + } + + int result = maxProduct(nums); + System.out.println("乘积最大的连续子数组的乘积是:" + result); + } + + /** + * 计算乘积最大的连续子数组的乘积 + * + * 算法思路: + * 由于负数的存在,最大值和最小值可能会相互转换 + * 因此需要同时维护以当前位置结尾的最大乘积和最小乘积 + * + * 状态定义: + * maxProduct:以当前位置结尾的最大乘积 + * minProduct:以当前位置结尾的最小乘积 + * result:全局最大乘积 + * + * 状态转移: + * 当遇到正数时:maxProduct可能变为maxProduct*nums[i]或nums[i] + * 当遇到负数时:maxProduct和minProduct会互换角色 + * 当遇到0时:重置乘积为0 + * + * 执行过程分析(以nums=[2,3,-2,4]为例): + * + * 初始状态: + * maxProduct = 2 + * minProduct = 2 + * result = 2 + * + * 遍历过程: + * i=1, nums[1]=3: + * nums[1]=3 > 0 + * maxProduct = max(3, 2*3) = max(3, 6) = 6 + * minProduct = min(3, 2*3) = min(3, 6) = 3 + * result = max(2, 6) = 6 + * + * i=2, nums[2]=-2: + * nums[2]=-2 < 0,交换maxProduct和minProduct + * temp = maxProduct = 6 + * maxProduct = minProduct = 3 + * minProduct = temp = 6 + * maxProduct = max(-2, 3*(-2)) = max(-2, -6) = -2 + * minProduct = min(-2, 6*(-2)) = min(-2, -12) = -12 + * result = max(6, -2) = 6 + * + * i=3, nums[3]=4: + * nums[3]=4 > 0 + * maxProduct = max(4, -2*4) = max(4, -8) = 4 + * minProduct = min(4, -12*4) = min(4, -48) = -48 + * result = max(6, 4) = 6 + * + * 最终结果:result = 6 + * 最大乘积子数组:[2,3],乘积=2*3=6 + * + * 执行过程分析(以nums=[-2,0,-1]为例): + * + * 初始状态: + * maxProduct = -2 + * minProduct = -2 + * result = -2 + * + * 遍历过程: + * i=1, nums[1]=0: + * nums[1]=0 + * maxProduct = max(0, -2*0) = max(0, 0) = 0 + * minProduct = min(0, -2*0) = min(0, 0) = 0 + * result = max(-2, 0) = 0 + * + * i=2, nums[2]=-1: + * nums[2]=-1 < 0,交换maxProduct和minProduct + * temp = maxProduct = 0 + * maxProduct = minProduct = 0 + * minProduct = temp = 0 + * maxProduct = max(-1, 0*(-1)) = max(-1, 0) = 0 + * minProduct = min(-1, 0*(-1)) = min(-1, 0) = -1 + * result = max(0, 0) = 0 + * + * 最终结果:result = 0 + * 最大乘积子数组:[0],乘积=0 + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 输入的整数数组 + * @return 乘积最大的连续子数组的乘积 + */ + public static int maxProduct(int[] nums) { + // 边界情况:空数组 + if (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 = max(当前元素单独作为子数组, 前一个最大乘积*当前元素) + maxProduct = Math.max(nums[i], maxProduct * nums[i]); + // minProduct = min(当前元素单独作为子数组, 前一个最小乘积*当前元素) + minProduct = Math.min(nums[i], minProduct * nums[i]); + + // 更新全局最大乘积 + result = Math.max(result, maxProduct); + } + + return result; + } + + /** + * 方法2:标准动态规划解法(更清晰的思路) + * + * 算法思路: + * 同时维护当前位置的最大乘积和最小乘积 + * 因为负数可能导致最大值和最小值互换 + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 输入的整数数组 + * @return 乘积最大的连续子数组的乘积 + */ + public int maxProductStandard(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++) { + // 计算三种可能的乘积 + int currentMax = maxProduct * nums[i]; + int currentMin = minProduct * nums[i]; + + // 更新最大乘积和最小乘积 + maxProduct = Math.max(nums[i], Math.max(currentMax, currentMin)); + minProduct = Math.min(nums[i], Math.min(currentMax, currentMin)); + + // 更新全局最大值 + result = Math.max(result, maxProduct); + } + + return result; + } + + /** + * 扩展方法:返回乘积最大的连续子数组 + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * - 返回子数组:O(k),k为子数组长度 + * + * @param nums 输入的整数数组 + * @return 乘积最大的连续子数组 + */ + public int[] getMaxProductSubarray(int[] nums) { + if (nums == null || nums.length == 0) return new int[0]; + + int maxProduct = nums[0]; + int minProduct = nums[0]; + int result = nums[0]; + int start = 0, end = 0, tempStart = 0; + + for (int i = 1; i < nums.length; i++) { + if (nums[i] < 0) { + int temp = maxProduct; + maxProduct = minProduct; + minProduct = temp; + } + + int newMax = Math.max(nums[i], maxProduct * nums[i]); + int newMin = Math.min(nums[i], minProduct * nums[i]); + + if (newMax > result) { + result = newMax; + end = i; + start = tempStart; + } + + if (newMax == nums[i]) { + tempStart = i; + } + + maxProduct = newMax; + minProduct = newMin; + } + + return Arrays.copyOfRange(nums, start, end + 1); + } +} diff --git a/algorithm/MaxProfit121.java b/algorithm/MaxProfit121.java new file mode 100644 index 0000000000000..ab6294df3925d --- /dev/null +++ b/algorithm/MaxProfit121.java @@ -0,0 +1,201 @@ +package com.funian.algorithm.algorithm; + +/** + * 买卖股票的最佳时机(LeetCode 121) + * + * 时间复杂度:O(n) + * - n是价格数组的长度 + * - 只需要遍历数组一次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +import java.util.Scanner; +import java.util.Arrays; + +public class MaxProfit121 { + + /** + * 主函数:处理用户输入并计算最大利润 + * + * 算法流程: + * 1. 读取用户输入的股票价格数组 + * 2. 调用 [maxProfit](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/MaxProfit121.java#L97-L124)方法计算最大利润 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入股票价格(以空格分隔):"); + String line = scanner.nextLine(); + String[] strPrices = line.split(" "); + int n = strPrices.length; + int[] prices = new int[n]; + + // 将输入的字符串价格转换为整数数组 + for (int i = 0; i < n; i++) { + prices[i] = Integer.parseInt(strPrices[i]); + } + + int maxProfit = maxProfit(prices); + System.out.println("最大利润为:" + maxProfit); + } + + /** + * 计算买卖股票的最大利润 + * + * 算法思路: + * 一次遍历,维护两个变量: + * 1. minPrice:到目前为止遇到的最低价格(最佳买入时机) + * 2. maxProfit:到目前为止能获得的最大利润 + * + * 对于每一天的价格,我们: + * 1. 更新历史最低价格 + * 2. 计算如果今天卖出能获得的利润 + * 3. 更新最大利润 + * + * 执行过程分析(以prices=[7,1,5,3,6,4]为例): + * + * 初始状态: + * minPrice = MAX_VALUE + * maxProfit = 0 + * + * 遍历过程: + * day 0, price=7: + * 更新minPrice = 7 + * profit = 7-7 = 0 + * maxProfit = max(0, 0) = 0 + * + * day 1, price=1: + * 更新minPrice = 1 (1 < 7) + * profit = 1-1 = 0 + * maxProfit = max(0, 0) = 0 + * + * day 2, price=5: + * minPrice保持1 (5 > 1) + * profit = 5-1 = 4 + * maxProfit = max(0, 4) = 4 + * + * day 3, price=3: + * minPrice保持1 (3 > 1) + * profit = 3-1 = 2 + * maxProfit = max(4, 2) = 4 + * + * day 4, price=6: + * minPrice保持1 (6 > 1) + * profit = 6-1 = 5 + * maxProfit = max(4, 5) = 5 + * + * day 5, price=4: + * minPrice保持1 (4 > 1) + * profit = 4-1 = 3 + * maxProfit = max(5, 3) = 5 + * + * 最终结果:maxProfit = 5 + * 最佳策略:第2天买入(price=1),第5天卖出(price=6),利润=6-1=5 + * + * 时间复杂度分析: + * - 遍历价格数组:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param prices 股票价格数组,prices[i]表示第i天的股票价格 + * @return 能获得的最大利润 + */ + public static int maxProfit(int[] prices) { + // 初始化最小价格为最大整数值 + // 这样确保第一个价格一定会更新minPrice + int minPrice = Integer.MAX_VALUE; + + // 初始化最大利润为0 + // 如果无法获得正利润,就返回0(不交易) + int maxProfit = 0; + + // 遍历每一天的价格 + for (int price : prices) { + // 更新到目前为止的最小价格(最佳买入时机) + if (price < minPrice) { + minPrice = price; + } + + // 计算如果今天卖出能获得的利润 + int profit = price - minPrice; + + // 更新到目前为止的最大利润 + if (profit > maxProfit) { + maxProfit = profit; + } + } + + // 返回最大利润 + return maxProfit; + } + + /** + * 方法2:动态规划解法 + * + * 算法思路: + * 定义两个状态: + * 1. hold:持有股票时的最大收益 + * 2. sold:不持有股票时的最大收益 + * + * 状态转移: + * hold = max(hold, -price) // 继续持有 或 买入 + * sold = max(sold, hold + price) // 继续不持有 或 卖出 + * + * 时间复杂度分析: + * - 遍历价格数组:O(n) + * - 每次操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param prices 股票价格数组 + * @return 能获得的最大利润 + */ + public int maxProfitDP(int[] prices) { + if (prices == null || prices.length <= 1) return 0; + + int hold = -prices[0]; // 持有股票 + int sold = 0; // 不持有股票 + + for (int i = 1; i < prices.length; i++) { + sold = Math.max(sold, hold + prices[i]); // 卖出股票 + hold = Math.max(hold, -prices[i]); // 买入股票 + } + + return sold; + } + + /** + * 方法3:暴力解法(仅供学习,效率较低) + * + * 算法思路: + * 尝试所有可能的买入和卖出组合 + * + * 时间复杂度分析: + * - 双重循环:O(n²) + * - 每次操作:O(1) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param prices 股票价格数组 + * @return 能获得的最大利润 + */ + public int maxProfitBruteForce(int[] prices) { + int maxProfit = 0; + + for (int i = 0; i < prices.length; i++) { + for (int j = i + 1; j < prices.length; j++) { + maxProfit = Math.max(maxProfit, prices[j] - prices[i]); + } + } + + return maxProfit; + } +} diff --git a/algorithm/MaxSlidingWindow239.java b/algorithm/MaxSlidingWindow239.java new file mode 100644 index 0000000000000..83c739e4e4d8c --- /dev/null +++ b/algorithm/MaxSlidingWindow239.java @@ -0,0 +1,252 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.Scanner; + +/** + * 滑动窗口最大值(LeetCode 239) + * + * 时间复杂度:O(n) + * - 每个元素最多被加入和移出双端队列各一次 + * - 虽然有内层while循环,但整体上每个元素的操作是常数时间 + * + * 空间复杂度:O(k) + * - 双端队列中最多存储k个元素的索引 + * - 结果数组空间不计入,因为它是输出的一部分 + */ +public class MaxSlidingWindow239 { + public static void main(String[] args) { + // 读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入整数数组 + System.out.print("请输入整数数组 nums(以空格分隔):"); + + // 读取输入并按空格分割 + String[] input = scanner.nextLine().split(" "); + + // 创建整型数组 + int[] nums = new int[input.length]; + + // 将字符串转换为整数 + for (int i = 0; i < input.length; i++) { + nums[i] = Integer.parseInt(input[i]); + } + + // 提示用户输入滑动窗口大小 + System.out.print("请输入滑动窗口大小 k:"); + + // 读取窗口大小 + int k = scanner.nextInt(); + + // 调用 maxSlidingWindow 方法计算每个滑动窗口的最大值 + int[] result = maxSlidingWindow(nums, k); + + // 输出结果 + System.out.print("滑动窗口中的最大值为:"); + for (int val : result) { + System.out.print(val + " "); + } + System.out.println(); + } + + /** + * 计算滑动窗口中的最大值 + * 使用双端队列维护一个单调递减的队列 + * + * 算法思路: + * 1. 使用双端队列存储数组元素的索引 + * 2. 队列中索引对应的元素值保持单调递减顺序 + * 3. 队列头部始终是当前窗口的最大值索引 + * 4. 遍历数组时维护队列的性质 + * + * 示例过程(以数组 [1,3,-1,-3,5,3,6,7], k=3 为例): + * + * 窗口位置 队列状态(存储索引) 队列元素值 最大值 + * [1 3 -1] -3 5 3 6 7 [1,2] [3,-1] 3 + * 1 [3 -1 -3] 5 3 6 7 [1,2,3] [3,-1,-3] 3 + * 1 3 [-1 -3 5] 3 6 7 [4] [5] 5 + * 1 3 -1 [-3 5 3] 6 7 [4,5] [5,3] 5 + * 1 3 -1 -3 [5 3 6] 7 [6] [6] 6 + * 1 3 -1 -3 5 [3 6 7] [7] [7] 7 + * + * 结果:[3,3,5,5,6,7] + * + * 时间复杂度分析: + * - 每个元素最多入队和出队一次:O(n),其中n为输入数组`nums`的长度 + * - 内层while循环整体上每个元素最多被处理一次:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 双端队列最多存储k个元素索引:O(k),k为滑动窗口大小 + * - 结果数组:O(n-k+1) + * - 总空间复杂度:O(k) + * + * @param nums 整数数组 + * @param k 滑动窗口大小 + * @return 每个滑动窗口中的最大值数组 + */ + public static int[] maxSlidingWindow(int[] nums, int k) { + // 边界条件检查 + if (nums == null || nums.length == 0 || k == 0) return new int[0]; + + // 获取数组长度 + int n = nums.length; + + // 创建结果数组,长度为 n - k + 1 + int[] result = new int[n - k + 1]; + + // 使用双端队列存储数组元素的索引 + Deque deque = new ArrayDeque<>(); + + // 遍历数组 + for (int i = 0; i < n; i++) { + // 移除滑动窗口外的元素索引 + // 如果队列头部索引超出当前窗口范围,则移除 + if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) { + deque.pollFirst(); + } + + // 保持队列递减顺序 + // 移除所有小于当前元素的队列尾部元素 + // 这样保证队列头部始终是当前窗口的最大值索引 + while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { + deque.pollLast(); + } + + // 添加当前元素下标到队列尾部 + deque.offerLast(i); + + // 当窗口形成时(i >= k - 1),记录当前窗口的最大值 + if (i >= k - 1) { + // 队列头部索引对应的元素就是当前窗口的最大值 + result[i - k + 1] = nums[deque.peekFirst()]; + } + } + + // 返回每个滑动窗口的最大值数组 + return result; + } + + /** + * 方法2:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 对每个窗口遍历所有元素找出最大值 + * + * 示例过程(以数组 [1,3,-1,-3,5,3,6,7], k=3 为例): + * + * 窗口[1,3,-1]: max(1,3,-1) = 3 + * 窗口[3,-1,-3]: max(3,-1,-3) = 3 + * 窗口[-1,-3,5]: max(-1,-3,5) = 5 + * 窗口[-3,5,3]: max(-3,5,3) = 5 + * 窗口[5,3,6]: max(5,3,6) = 6 + * 窗口[3,6,7]: max(3,6,7) = 7 + * + * 结果:[3,3,5,5,6,7] + * + * 时间复杂度分析: + * - 外层循环:O(n-k+1),n为数组长度 + * - 内层循环:O(k),k为窗口大小 + * - 总时间复杂度:O(n*k) + * + * 空间复杂度分析: + * - 只使用了几个变量:O(1) + * - 结果数组:O(n-k+1) + * - 总空间复杂度:O(n) + * + * @param nums 整数数组 + * @param k 滑动窗口大小 + * @return 每个滑动窗口中的最大值数组 + */ + public static int[] maxSlidingWindowBruteForce(int[] nums, int k) { + if (nums == null || nums.length == 0 || k == 0) return new int[0]; + + int n = nums.length; + int[] result = new int[n - k + 1]; + + // 对每个窗口计算最大值 + for (int i = 0; i <= n - k; i++) { + int max = nums[i]; + for (int j = 1; j < k; j++) { + if (nums[i + j] > max) { + max = nums[i + j]; + } + } + result[i] = max; + } + + return result; + } + + /** + * 方法3:使用优先队列(堆)实现 + * + * 算法思路: + * 使用最大堆存储元素值和索引,动态维护窗口内元素 + * + * 示例过程(以数组 [1,3,-1,-3,5,3,6,7], k=3 为例): + * + * 1. 初始化堆,加入前3个元素: [(3,1), (1,0), (-1,2)] + * result[0] = 3 + * + * 2. 处理第4个元素-3: + * 加入堆: [(3,1), (1,0), (-1,2), (-3,3)] + * 移除过期元素(无): [(3,1), (1,0), (-1,2), (-3,3)] + * result[1] = 3 + * + * 3. 处理第5个元素5: + * 加入堆: [(5,4), (3,1), (1,0), (-1,2), (-3,3)] + * 移除过期元素(-1,2), (3,1), (1,0): [(5,4), (-3,3)] + * result[2] = 5 + * + * ...继续处理直到结束 + * + * 时间复杂度分析: + * - 遍历数组:O(n),n为数组长度 + * - 堆操作:O(log k),每次插入和删除操作 + * - 总时间复杂度:O(n*log k) + * + * 空间复杂度分析: + * - 优先队列:O(k),最多存储k个元素 + * - 结果数组:O(n-k+1) + * - 总空间复杂度:O(k) + * + * @param nums 整数数组 + * @param k 滑动窗口大小 + * @return 每个滑动窗口中的最大值数组 + */ + public static int[] maxSlidingWindowHeap(int[] nums, int k) { + if (nums == null || nums.length == 0 || k == 0) return new int[0]; + + int n = nums.length; + int[] result = new int[n - k + 1]; + + // 使用优先队列存储对,按值降序排列 + java.util.PriorityQueue pq = new java.util.PriorityQueue<>( + (a, b) -> b[0] - a[0] + ); + + // 初始化前k个元素 + for (int i = 0; i < k; i++) { + pq.offer(new int[]{nums[i], i}); + } + result[0] = pq.peek()[0]; + + // 处理后续元素 + for (int i = k; i < n; i++) { + pq.offer(new int[]{nums[i], i}); + + // 移除窗口外的元素 + while (pq.peek()[1] <= i - k) { + pq.poll(); + } + + result[i - k + 1] = pq.peek()[0]; + } + + return result; + } +} diff --git a/algorithm/MaxSubArray53.java b/algorithm/MaxSubArray53.java new file mode 100644 index 0000000000000..2f6f76ad69c69 --- /dev/null +++ b/algorithm/MaxSubArray53.java @@ -0,0 +1,282 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 最大子数组和(LeetCode 53) + * + * 时间复杂度:O(n) + * - 只需要遍历数组一次 + * - 每次迭代都进行常数时间的操作 + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用与输入数组大小相关的额外存储空间 + */ +public class MaxSubArray53 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("请输入整数数组(用空格分隔):"); + + // 读取输入的数组 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] str = line.split(" "); + + // 获取数组长度 + int n = str.length; + + // 创建整型数组 + int[] nums = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(str[i]); + } + + // 调用 maxSubArray 方法计算最大子数组和 + int result = maxSubArray(nums); + + // 输出结果 + System.out.println("最大子数组和为:" + result); + } + + /** + * 使用动态规划方法求解最大子数组和(Kadane算法) + * + * 算法思路: + * 1. 对于每个元素,我们决定是将其加入到之前的子数组中,还是从当前元素重新开始 + * 2. 状态转移方程:dp[i] = max(nums[i], dp[i-1] + nums[i]) + * 3. 其中 dp[i] 表示以第 i 个元素结尾的最大子数组和 + * 4. 由于只需要前一个状态,可以使用一个变量代替整个 dp 数组 + * + * 示例过程(以数组 [-2,1,-3,4,-1,2,1,-5,4] 为例): + * 索引: 0 1 2 3 4 5 6 7 8 + * 元素: -2 1 -3 4 -1 2 1 -5 4 + * + * i=0: currentSum=-2, maxSum=-2 + * i=1: currentSum=max(1, -2+1)=1, maxSum=max(-2,1)=1 + * i=2: currentSum=max(-3, 1-3)=-2, maxSum=max(1,-2)=1 + * i=3: currentSum=max(4, -2+4)=4, maxSum=max(1,4)=4 + * i=4: currentSum=max(-1, 4-1)=3, maxSum=max(4,3)=4 + * i=5: currentSum=max(2, 3+2)=5, maxSum=max(4,5)=5 + * i=6: currentSum=max(1, 5+1)=6, maxSum=max(5,6)=6 + * i=7: currentSum=max(-5, 6-5)=1, maxSum=max(6,1)=6 + * i=8: currentSum=max(4, 1+4)=5, maxSum=max(6,5)=6 + * + * 结果:最大子数组和为6,对应子数组[4,-1,2,1] + * + * 时间复杂度分析: + * - 单次遍历数组:O(n),其中n为输入数组`nums`的长度 + * - 每次迭代进行常数时间操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了几个变量存储状态:O(1) + * - 没有使用额外数组:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + * @return 最大子数组和 + */ + public static int maxSubArray(int[] nums) { + // 初始化最大和为第一个元素 + int maxSum = nums[0]; + + // 当前子数组的最大和初始化为第一个元素 + int currentSum = nums[0]; + + // 从第二个元素开始遍历数组 + for (int i = 1; i < nums.length; i++) { + // 当前子数组和为当前元素或加上前一个子数组和的较大值 + // 这表示我们决定是继续之前的子数组还是从当前元素重新开始 + currentSum = Math.max(nums[i], currentSum + nums[i]); + // 更新全局最大和 + maxSum = Math.max(maxSum, currentSum); + } + + // 返回最大子数组和 + return maxSum; + } + + /** + * 方法2:分治法解法 + * + * 算法思路: + * 将数组分为两部分,最大子数组和可能出现在: + * 1. 左半部分 + * 2. 右半部分 + * 3. 跨越中点的子数组 + * + * 示例过程(以数组 [-2,1,-3,4,-1,2,1,-5,4] 为例): + * + * 1. 初始调用: maxSubArrayHelper(nums, 0, 8) + * mid = 4, 左半部分[0,4], 右半部分[5,8] + * + * 2. 递归处理左半部分[-2,1,-3,4,-1]: + * mid = 2, 左半部分[0,2], 右半部分[3,4] + * 左半部分最大和: [-2,1,-3] = -1 + * 右半部分最大和: [4,-1] = 4 + * 跨越中点最大和: [1,-3,4] = 2 + * 左半部分结果: max(-1,4,2) = 4 + * + * 3. 递归处理右半部分[2,1,-5,4]: + * mid = 6, 左半部分[5,6], 右半部分[7,8] + * 左半部分最大和: [2,1] = 3 + * 右半部分最大和: [-5,4] = 4 + * 跨越中点最大和: [1,-5,4] = 0 + * 右半部分结果: max(3,4,0) = 4 + * + * 4. 计算跨越初始中点最大和: + * 左侧最大和: [4,-1,2,1] = 6 + * 右侧最大和: [-5,4] = 4 + * 跨越中点最大和: 6 + 4 = 10 + * + * 5. 最终结果: max(4,4,10) = 10 + * + * 时间复杂度分析: + * - 递归深度:O(log n),其中n为输入数组`nums`的长度 + * - 每层递归处理:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 递归调用栈:O(log n) + * - 其他变量:O(1) + * - 总空间复杂度:O(log n) + * + * @param nums 输入的整数数组 + * @return 最大子数组和 + */ + public static int maxSubArrayDivideConquer(int[] nums) { + return maxSubArrayHelper(nums, 0, nums.length - 1); + } + + /** + * 分治法辅助方法 + * + * 算法思路: + * 1. 将数组递归分为左右两部分 + * 2. 分别计算左半部分、右半部分和跨越中点的最大子数组和 + * 3. 返回三者中的最大值 + * + * @param nums 数组 + * @param left 左边界 + * @param right 右边界 + * @return 最大子数组和 + */ + private static int maxSubArrayHelper(int[] nums, int left, int right) { + // 基础情况:只有一个元素 + if (left == right) { + return nums[left]; + } + + // 计算中点 + int mid = left + (right - left) / 2; + + // 递归计算左半部分和右半部分的最大子数组和 + int leftSum = maxSubArrayHelper(nums, left, mid); + int rightSum = maxSubArrayHelper(nums, mid + 1, right); + + // 计算跨越中点的最大子数组和 + int leftMax = Integer.MIN_VALUE; + int sum = 0; + // 从中点向左计算最大和 + for (int i = mid; i >= left; i--) { + sum += nums[i]; + leftMax = Math.max(leftMax, sum); + } + + int rightMax = Integer.MIN_VALUE; + sum = 0; + // 从中点+1向右计算最大和 + for (int i = mid + 1; i <= right; i++) { + sum += nums[i]; + rightMax = Math.max(rightMax, sum); + } + + int crossSum = leftMax + rightMax; + + // 返回三者中的最大值 + return Math.max(Math.max(leftSum, rightSum), crossSum); + } + + /** + * 方法3:返回最大子数组的起始和结束位置 + * + * 算法思路: + * 在计算最大子数组和的同时,记录子数组的起始和结束位置 + * + * 示例过程(以数组 [-2,1,-3,4,-1,2,1,-5,4] 为例): + * + * 初始化: maxSum=-2, currentSum=-2, start=0, end=0, tempStart=0 + * + * i=1, nums[1]=1: currentSum<0, currentSum=1, tempStart=1 + * currentSum>maxSum, maxSum=1, start=1, end=1 + * + * i=2, nums[2]=-3: currentSum>=0, currentSum=1+(-3)=-2 + * currentSummaxSum, maxSum=4, start=3, end=3 + * + * i=4, nums[4]=-1: currentSum>=0, currentSum=4+(-1)=3 + * currentSum=0, currentSum=3+2=5 + * currentSum>maxSum, maxSum=5, start=3, end=5 + * + * i=6, nums[6]=1: currentSum>=0, currentSum=5+1=6 + * currentSum>maxSum, maxSum=6, start=3, end=6 + * + * i=7, nums[7]=-5: currentSum>=0, currentSum=6+(-5)=1 + * currentSum=0, currentSum=1+4=5 + * currentSum maxSum) { + maxSum = currentSum; + start = tempStart; + end = i; + } + } + + return new int[]{maxSum, start, end}; + } +} diff --git a/algorithm/MaxWater11.java b/algorithm/MaxWater11.java new file mode 100644 index 0000000000000..e5de2933f74c3 --- /dev/null +++ b/algorithm/MaxWater11.java @@ -0,0 +1,241 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 盛最多水的容器(LeetCode 11) + * + * 时间复杂度:O(n) + * - 使用双指针技术,两个指针总共最多移动 n 次 + * - 每次移动只进行常数时间的操作 + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用与输入数组大小相关的额外存储空间 + */ +public class MaxWater11 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入高度数组 + System.out.println("输入高度数组:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] strs = line.split(" "); + + // 获取数组长度 + int n = strs.length; + + // 创建整型数组存储高度 + int[] height = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + height[i] = Integer.parseInt(strs[i]); + } + + // 调用 maxWater 方法计算最大容量 + int result = maxWater(height); + + // 输出结果 + System.out.println("最大容量为:" + result); + } + + /** + * 计算盛最多水的容器容量 + * 给定一个长度为 n 的整数数组 height,有 n 条垂线, + * 第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。 + * 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + * + * 算法思路: + * 使用双指针技术,从数组两端开始向中间移动 + * 每次移动较短边的指针,因为这样才能可能找到更大的面积 + * 面积 = 宽度 × 高度(高度取两个边的较小值) + * + * 示例过程(以数组 [1,8,6,2,5,4,8,3,7] 为例): + * + * 初始状态: + * height = [1, 8, 6, 2, 5, 4, 8, 3, 7] + * L R + * width = 8, height = min(1,7) = 1, area = 8×1 = 8 + * + * 移动较短边(L): + * height = [1, 8, 6, 2, 5, 4, 8, 3, 7] + * L R + * width = 7, height = min(8,7) = 7, area = 7×7 = 49 + * + * 移动较短边(R): + * height = [1, 8, 6, 2, 5, 4, 8, 3, 7] + * L R + * width = 6, height = min(8,3) = 3, area = 6×3 = 18 + * + * 继续移动直到指针相遇... + * + * 最大面积: 49 + * + * 时间复杂度分析: + * - 双指针遍历数组:O(n),其中n为输入数组`height`的长度 + * - 每次循环进行常数时间操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了几个变量存储指针和面积:O(1) + * - 没有使用额外数组:O(1) + * - 总空间复杂度:O(1) + * + * @param height 输入的高度数组 + * @return 容器能盛的最大水量 + */ + public static int maxWater(int[] height) { + // 获取数组长度 + int n = height.length; + + // 左指针,指向数组开始位置 + int left = 0; + + // 右指针,指向数组结束位置 + int right = n - 1; + + // 记录最大面积 + int maxArea = 0; + + // 双指针向中间移动,直到相遇 + while (left < right) { + // 计算当前宽度(两个指针之间的距离) + int width = right - left; + + // 计算当前高度(两个指针指向高度的较小值) + int minHeight = Math.min(height[left], height[right]); + + // 计算当前面积 + int currentArea = width * minHeight; + + // 更新最大面积 + maxArea = Math.max(maxArea, currentArea); + + // 移动较短边的指针,因为这样才能可能找到更大的面积 + // 如果移动较长边,面积只会变小或不变 + if (height[left] < height[right]) { + left++; // 移动左指针向右 + } else { + right--; // 移动右指针向左 + } + } + + // 返回找到的最大面积 + return maxArea; + } + + + /** + * 方法2:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 遍历所有可能的两条线组合,计算每种情况下的面积,找出最大值 + * + * 示例过程(以数组 [1,8,6,2,5,4,8,3,7] 为例): + * + * 遍历所有组合计算面积: + * i=0,j=1: area = (1-0) × min(1,8) = 1 × 1 = 1 + * i=0,j=2: area = (2-0) × min(1,6) = 2 × 1 = 2 + * ... + * i=1,j=8: area = (8-1) × min(8,7) = 7 × 7 = 49 + * ... + * i=8,j=8: 结束 + * + * 最大面积: 49 + * + * 时间复杂度分析: + * - 双重循环遍历所有组合:O(n²),其中n为输入数组`height`的长度 + * - 外层循环:O(n) + * - 内层循环:O(n) + * - 每次计算面积:O(1) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 只使用了几个变量:O(1) + * - 总空间复杂度:O(1) + * + * @param height 输入的高度数组 + * @return 容器能盛的最大水量 + */ + public static int maxWaterBruteForce(int[] height) { + int maxArea = 0; + + // 双重循环遍历所有可能的两条线组合 + for (int i = 0; i < height.length; i++) { + for (int j = i + 1; j < height.length; j++) { + // 计算面积:宽度 × 高度(取较小值) + int area = (j - i) * Math.min(height[i], height[j]); + maxArea = Math.max(maxArea, area); + } + } + + return maxArea; + } + + /** + * 方法3:优化的双指针法(提前终止) + * + * 算法思路: + * 在标准双指针法基础上增加提前终止条件,当当前最大可能面积小于已知最大面积时提前终止 + * + * 示例过程(以数组 [1,8,6,2,5,4,8,3,7] 为例): + * + * 与标准双指针法类似,但增加提前终止条件: + * 1. 预先计算数组最大高度 maxHeight = 8 + * 2. 在每次循环中检查 width × maxHeight ≤ maxArea 是否成立 + * 3. 如果成立则提前终止,因为不可能找到更大的面积 + * + * 时间复杂度分析: + * - 最坏情况仍为O(n),其中n为输入数组`height`的长度 + * - 平均情况下由于提前终止会更快 + * - 计算最大高度:O(n) + * - 双指针遍历:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 使用了几个额外变量:O(1) + * - 总空间复杂度:O(1) + * + * @param height 输入的高度数组 + * @return 容器能盛的最大水量 + */ + public static int maxWaterOptimized(int[] height) { + int left = 0; + int right = height.length - 1; + int maxArea = 0; + + // 记录当前最大高度,用于提前终止 + int maxHeight = 0; + for (int h : height) { + maxHeight = Math.max(maxHeight, h); + } + + while (left < right) { + int width = right - left; + + // 如果当前最大可能面积已经小于已知最大面积,提前终止 + if (width * maxHeight <= maxArea) { + break; + } + + int minHeight = Math.min(height[left], height[right]); + int currentArea = width * minHeight; + maxArea = Math.max(maxArea, currentArea); + + if (height[left] < height[right]) { + left++; + } else { + right--; + } + } + + return maxArea; + } + +} diff --git a/algorithm/Merge56.java b/algorithm/Merge56.java new file mode 100644 index 0000000000000..32041fbf74567 --- /dev/null +++ b/algorithm/Merge56.java @@ -0,0 +1,323 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 合并区间(LeetCode 56) + * + * 时间复杂度:O(n log n) + * - 排序需要 O(n log n) 时间 + * - 遍历数组需要 O(n) 时间 + * - 总时间复杂度为 O(n log n) + * + * 空间复杂度:O(n) + * - 存储合并结果的列表需要 O(n) 空间 + * - 排序可能需要 O(log n) 的递归栈空间 + */ +public class Merge56 { + /** + * 合并重叠区间 + * + * 算法思路: + * 1. 首先按照区间的起始位置对所有区间进行排序 + * 2. 遍历排序后的区间,逐个检查是否与前一个区间重叠 + * 3. 如果重叠则合并,否则将当前区间添加到结果中 + * + * 重叠判断条件:当前区间起始位置 <= 前一个区间结束位置 + * 合并方法:新区间的结束位置为两个区间结束位置的最大值 + * + * 示例过程(以 intervals=[[1,3],[2,6],[8,10],[15,18]] 为例): + * + * 排序后: [[1,3],[2,6],[8,10],[15,18]] + * + * 步骤1: merged = [[1,3]] + * 步骤2: 检查[2,6],2 <= 3,重叠,合并为[1,6],merged = [[1,6]] + * 步骤3: 检查[8,10],8 > 6,不重叠,添加到结果,merged = [[1,6],[8,10]] + * 步骤4: 检查[15,18],15 > 10,不重叠,添加到结果,merged = [[1,6],[8,10],[15,18]] + * + * 结果:[[1,6],[8,10],[15,18]] + * + * 时间复杂度分析: + * - 排序:O(n log n),其中n为输入区间数组`intervals`的长度 + * - 遍历:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 结果列表:O(n) + * - 排序递归栈:O(log n) + * - 总空间复杂度:O(n) + * + * @param intervals 输入的区间数组 + * @return 合并后的不重叠区间数组 + */ +public int[][] merge(int[][] intervals) { + // 如果输入为空,直接返回空数组 + if (intervals.length == 0) return new int[0][0]; + + // 按照每个区间的起始值进行排序 + Arrays.sort(intervals); + + // 存储合并后的区间 + List merged = new ArrayList<>(); + + // 将第一个区间添加到合并列表中 + merged.add(intervals[0]); + + // 从第二个区间开始遍历 + for (int i = 1; i < intervals.length; i++) { + // 获取当前区间 + int[] current = intervals[i]; + + // 获取已合并区间中的最后一个区间 + int[] lastMerged = merged.get(merged.size() - 1); + + // 检查当前区间是否与最后一个合并区间重叠 + if (current[0] <= lastMerged[1]) { + // 如果重叠,合并区间 + lastMerged[1] = Math.max(lastMerged[1], current[1]); + } else { + // 如果不重叠,直接添加当前区间到合并列表 + merged.add(current); + } + } + + // 将 List 转换为二维数组并返回 + return merged.toArray(new int[merged.size()][]); + } + + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 输入区间数 + System.out.print("请输入区间的数量: "); + + // 读取区间数量 + int n = Integer.parseInt(scanner.nextLine()); + + // 创建二维数组存储区间 + int[][] intervals = new int[n][2]; + + // 输入区间 + System.out.println("请输入区间 (每个区间用空格分隔,格式为 start end):"); + + // 读取每个区间 + for (int i = 0; i < n; i++) { + // 按空格分割输入字符串 + String[] strArray = scanner.nextLine().split(" "); + + // 解析起始位置 + intervals[i][0] = Integer.parseInt(strArray[0]); + + // 解析结束位置 + intervals[i][1] = Integer.parseInt(strArray[1]); + } + + // 创建 Merge56 实例并合并区间 + Merge56 solution = new Merge56(); + + // 调用 merge 方法合并区间 + int[][] mergedIntervals = solution.merge(intervals); + + // 输出结果 + System.out.println("合并后的区间:"); + + // 遍历并打印合并后的区间 + for (int[] interval : mergedIntervals) { + System.out.println(Arrays.toString(interval)); + } + } + + + /** + * 方法2:不修改原数组的版本 + * + * 算法思路: + * 1. 创建原数组的副本以避免修改原数组 + * 2. 对副本进行排序和合并操作 + * + * 示例过程(以 intervals=[[1,3],[2,6],[8,10],[15,18]] 为例): + * + * 1. 创建副本: intervalList = [[1,3],[2,6],[8,10],[15,18]] + * 2. 排序: intervalList = [[1,3],[2,6],[8,10],[15,18]] (已有序) + * 3. 合并过程: + * merged = [[1,3]] + * 处理[2,6]: 2 <= 3, 重叠, 合并为[1,6] + * 处理[8,10]: 8 > 6, 不重叠, 添加到结果 + * 处理[15,18]: 15 > 10, 不重叠, 添加到结果 + * 4. 最终结果: [[1,6],[8,10],[15,18]] + * + * 时间复杂度分析: + * - 创建副本:O(n),其中n为输入区间数组`intervals`的长度 + * - 排序:O(n log n) + * - 合并:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 副本列表:O(n) + * - 结果列表:O(n) + * - 排序递归栈:O(log n) + * - 总空间复杂度:O(n) + * + * @param intervals 输入的区间数组 + * @return 合并后的不重叠区间数组 + */ + public int[][] mergeImmutable(int[][] intervals) { + // 边界条件检查 + if (intervals.length == 0) return new int[0][0]; + + // 创建区间列表的副本 + List intervalList = new ArrayList<>(); + // 遍历原数组,为每个区间创建副本并添加到列表中 + for (int[] interval : intervals) { + intervalList.add(new int[]{interval[0], interval[1]}); + } + + // 排序 + intervalList.sort((a, b) -> Integer.compare(a[0], b[0])); + + // 合并 + List merged = new ArrayList<>(); + // 添加第一个区间到结果列表 + merged.add(intervalList.get(0)); + + // 从第二个区间开始遍历 + for (int i = 1; i < intervalList.size(); i++) { + // 获取当前区间 + int[] current = intervalList.get(i); + // 获取结果列表中的最后一个区间 + int[] lastMerged = merged.get(merged.size() - 1); + + // 检查是否重叠 + if (current[0] <= lastMerged[1]) { + // 重叠则合并,更新结束位置为较大值 + lastMerged[1] = Math.max(lastMerged[1], current[1]); + } else { + // 不重叠则直接添加 + merged.add(current); + } + } + + // 将List转换为二维数组并返回 + return merged.toArray(new int[merged.size()][]); + } + + /** + * 方法3:使用栈实现 + * + * 算法思路: + * 1. 排序区间数组 + * 2. 使用栈存储已处理的区间 + * 3. 对于每个新区间,检查是否与栈顶区间重叠 + * 4. 重叠则合并,不重叠则入栈 + * + * 示例过程(以 intervals=[[1,3],[2,6],[8,10],[15,18]] 为例): + * + * 1. 排序后: [[1,3],[2,6],[8,10],[15,18]] + * 2. 栈操作过程: + * 压入[1,3]: stack = [[1,3]] + * 处理[2,6]: 2 <= 3, 重叠, 合并栈顶为[1,6] + * 处理[8,10]: 8 > 6, 不重叠, 压入[8,10], stack = [[1,6],[8,10]] + * 处理[15,18]: 15 > 10, 不重叠, 压入[15,18], stack = [[1,6],[8,10],[15,18]] + * 3. 最终结果: [[1,6],[8,10],[15,18]] + * + * 时间复杂度分析: + * - 排序:O(n log n),其中n为输入区间数组`intervals`的长度 + * - 遍历处理:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 栈空间:O(n) + * - 排序递归栈:O(log n) + * - 总空间复杂度:O(n) + * + * @param intervals 输入的区间数组 + * @return 合并后的不重叠区间数组 + */ + public int[][] mergeStack(int[][] intervals) { + // 边界条件检查 + if (intervals.length == 0) return new int[0][0]; + + // 排序 + Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); + + // 使用栈存储合并后的区间 + Stack stack = new Stack<>(); + // 将第一个区间压入栈 + stack.push(intervals[0]); + + // 从第二个区间开始处理 + for (int i = 1; i < intervals.length; i++) { + // 获取当前区间 + int[] current = intervals[i]; + // 获取栈顶区间(不弹出) + int[] top = stack.peek(); + + // 检查是否重叠 + if (current[0] <= top[1]) { + // 重叠,合并区间 + top[1] = Math.max(top[1], current[1]); + } else { + // 不重叠,将当前区间压入栈 + stack.push(current); + } + } + + // 转换为数组 + return stack.toArray(new int[stack.size()][]); + } + + /** + * 方法4:一次遍历优化版本 + * + * 算法思路: + * 在遍历过程中直接判断是否需要合并,避免了获取最后一个元素的操作 + * + * 示例过程(以 intervals=[[1,3],[2,6],[8,10],[15,18]] 为例): + * + * 1. 排序后: [[1,3],[2,6],[8,10],[15,18]] + * 2. 遍历处理: + * merged为空, 添加[1,3], merged = [[1,3]] + * 处理[2,6]: 3 >= 2, 重叠, 合并为[1,6] + * 处理[8,10]: 6 < 8, 不重叠, 添加[8,10], merged = [[1,6],[8,10]] + * 处理[15,18]: 10 < 15, 不重叠, 添加[15,18], merged = [[1,6],[8,10],[15,18]] + * 3. 最终结果: [[1,6],[8,10],[15,18]] + * + * 时间复杂度分析: + * - 排序:O(n log n),其中n为输入区间数组`intervals`的长度 + * - 遍历:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 结果列表:O(n) + * - 排序递归栈:O(log n) + * - 总空间复杂度:O(n) + * + * @param intervals 输入的区间数组 + * @return 合并后的不重叠区间数组 + */ + public int[][] mergeOptimized(int[][] intervals) { + if (intervals.length == 0) return new int[0][0]; + + // 排序 + Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); + + // 使用数组列表存储结果 + List merged = new ArrayList<>(); + + // 遍历所有区间 + for (int[] interval : intervals) { + // 如果结果列表为空或当前区间与上一个区间不重叠 + if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < interval[0]) { + // 直接添加当前区间 + merged.add(interval); + } else { + // 否则合并区间,更新上一个区间的结束位置 + merged.get(merged.size() - 1)[1] = Math.max( + merged.get(merged.size() - 1)[1], interval[1]); + } + } + + return merged.toArray(new int[merged.size()][]); + } +} diff --git a/algorithm/MergeKList23.java b/algorithm/MergeKList23.java new file mode 100644 index 0000000000000..c992105cf11de --- /dev/null +++ b/algorithm/MergeKList23.java @@ -0,0 +1,162 @@ +package com.funian.algorithm.algorithm; + +import java.util.PriorityQueue; +import java.util.Scanner; + +/** + * 合并K个升序链表(LeetCode 23) + * + * 时间复杂度:O(N * log k) + * - N是所有节点的总数,k是链表的数量 + * - 每个节点需要进行一次插入和删除操作,复杂度为O(log k) + * + * 空间复杂度:O(k) + * - 优先队列最多存储k个节点 + */ +public class MergeKList23 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode(int x) { val = x; } + } + + /** + * 使用优先队列(最小堆)合并K个升序链表 + * + * 算法思路: + * 1. 将K个链表的头节点放入最小堆中 + * 2. 每次取出值最小的节点,加入结果链表 + * 3. 然后将该节点的下一个节点(如果存在)加入堆中 + * 4. 重复步骤2-3直到堆为空 + * + * 执行过程分析(以 lists = [[1,4,5],[1,3,4],[2,6]] 为例): + * + * 初始状态: + * 堆:{1(来自list1), 1(来自list2), 2(来自list3)} + * result: null + * + * 第1次操作:取出1(来自list1) + * 堆:{1(来自list2), 2(来自list3), 4(来自list1)} + * result: 1 + * + * 第2次操作:取出1(来自list2) + * 堆:{2(来自list3), 3(来自list2), 4(来自list1)} + * result: 1->1 + * + * 第3次操作:取出2(来自list3) + * 堆:{3(来自list2), 4(来自list1), 6(来自list3)} + * result: 1->1->2 + * + * 以此类推,直到堆为空 + * 最终结果:1->1->2->3->4->4->5->6 + * + * 时间复杂度分析: + * - 初始化堆:O(k),其中k为链表数量 + * - 每个节点的插入和删除操作:O(log k) + * - 总共有N个节点,总时间复杂度:O(N * log k) + * + * 空间复杂度分析: + * - 优先队列存储最多k个节点:O(k) + * - 虚拟头节点和指针变量:O(1) + * - 总空间复杂度:O(k) + * + * @param lists K个升序链表的数组 + * @return 合并后的升序链表 + */ + public ListNode mergeKLists(ListNode[] lists) { + // 使用优先队列(最小堆)来帮助合并链表 + // 比较器定义:按照节点值进行比较,构建最小堆 + PriorityQueue minHeap = new PriorityQueue<>((a, b) -> a.val - b.val); + + // 将所有链表的头节点加入堆中(只加入非空节点) + for (ListNode list : lists) { + if (list != null) { + minHeap.offer(list); + } + } + + // 创建一个虚拟头节点,用于构建结果链表 + // 使用虚拟头节点可以简化链表操作,避免处理头节点的特殊情况 + ListNode dummy = new ListNode(0); + ListNode current = dummy; + + // 从堆中取出最小节点并加入结果链表 + // 当堆不为空时继续处理 + while (!minHeap.isEmpty()) { + ListNode node = minHeap.poll(); + current.next = node; + current = current.next; + + // 如果该节点还有下一个节点,则将下一个节点加入堆中 + // 这样可以保证每个链表的节点都能被处理 + if (node.next != null) { + minHeap.offer(node.next); + } + } + + // 返回合并后的链表头(跳过虚拟头节点) + return dummy.next; + } + + /** + * 主函数:处理用户输入并输出合并结果 + * + * 算法流程: + * 1. 读取用户输入的链表数量 + * 2. 依次读取每个链表的节点数和节点值 + * 3. 调用mergeKLists方法合并所有链表 + * 4. 输出合并后的链表 + */ + public static void main(String[] args) { + // 注意:原代码中这里有个错误,应该是MergeKList23而不是Solution + // 创建解决方案实例 + MergeKList23 solution = new MergeKList23(); + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 读取链表的数量 + System.out.print("请输入链表的数量:"); + // 读取链表数量 + int k = scanner.nextInt(); + + // 创建链表数组 + ListNode[] lists = new ListNode[k]; + + // 依次读取每个链表的信息 + for (int i = 0; i < k; i++) { + // 读取第i+1个链表的节点数 + System.out.print("请输入第 " + (i + 1) + " 个链表的节点数:"); + // 读取节点数 + int n = scanner.nextInt(); + + // 创建哑节点简化链表构建过程 + ListNode dummy = new ListNode(0); + ListNode current = dummy; + + // 读取第i+1个链表的节点值 + System.out.print("请输入第 " + (i + 1) + " 个链表的节点值(空格分隔):"); + for (int j = 0; j < n; j++) { + current.next = new ListNode(scanner.nextInt()); + current = current.next; + } + + // 将构建好的链表赋值给链表数组 + lists[i] = dummy.next; + } + + // 调用合并方法合并所有链表 + ListNode mergedList = solution.mergeKLists(lists); + + // 打印合并后的链表 + System.out.print("合并后的链表为:"); + while (mergedList != null) { + System.out.print(mergedList.val + " "); + mergedList = mergedList.next; + } + System.out.println(); + } +} diff --git a/algorithm/MergeTwoList21.java b/algorithm/MergeTwoList21.java new file mode 100644 index 0000000000000..01ef272fe5ddb --- /dev/null +++ b/algorithm/MergeTwoList21.java @@ -0,0 +1,264 @@ +package com.funian.algorithm.algorithm; + +/** + * 合并两个有序链表(LeetCode 21) + * + * 时间复杂度:O(m + n) + * - m 是第一个链表的长度,n 是第二个链表的长度 + * - 每个节点只被访问一次 + * - 总时间复杂度为两个链表长度之和 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量(dummy、current等) + * - 不使用与输入规模相关的额外空间 + */ +public class MergeTwoList21 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + + ListNode(int val) { + this.val = val; + } + } + + /** + * 合并两个有序链表 + * + * 算法思路: + * 使用双指针技术,依次比较两个链表的节点值 + * 将较小的节点连接到结果链表中 + * 最后将剩余的节点直接连接到结果链表 + * + * 执行过程分析(以 l1=[1,2,4], l2=[1,3,4] 为例): + * + * 初始状态: + * l1: 1 -> 2 -> 4 -> null + * l2: 1 -> 3 -> 4 -> null + * dummy -> null + * current -> dummy + * + * 第1次比较(1 vs 1): + * 选择l1的节点1 + * dummy -> 1 -> null + * current -> 1 + * l1: 2 -> 4 -> null + * l2: 1 -> 3 -> 4 -> null + * + * 第2次比较(2 vs 1): + * 选择l2的节点1 + * dummy -> 1 -> 1 -> null + * current -> 1(第二个) + * l1: 2 -> 4 -> null + * l2: 3 -> 4 -> null + * + * 第3次比较(2 vs 3): + * 选择l1的节点2 + * dummy -> 1 -> 1 -> 2 -> null + * current -> 2 + * l1: 4 -> null + * l2: 3 -> 4 -> null + * + * 第4次比较(4 vs 3): + * 选择l2的节点3 + * dummy -> 1 -> 1 -> 2 -> 3 -> null + * current -> 3 + * l1: 4 -> null + * l2: 4 -> null + * + * 第5次比较(4 vs 4): + * 选择l1的节点4(相等时选择l1) + * dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> null + * current -> 4 + * l1: null + * l2: 4 -> null + * + * l1为空,将l2剩余部分连接到结果链表: + * dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> null + * + * 返回 dummy.next,即 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> null + * + * 时间复杂度分析: + * - 遍历两个链表:O(m + n),其中m为第一个链表长度,n为第二个链表长度 + * - 每个节点只被访问一次 + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * - 不使用与输入规模相关的额外空间 + * + * @param l1 第一个有序链表的头节点 + * @param l2 第二个有序链表的头节点 + * @return 合并后的有序链表的头节点 + */ + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + // 创建哨兵节点,简化边界条件的处理 + ListNode dummy = new ListNode(0); + // current 指向结果链表的当前节点,初始指向哨兵节点 + ListNode current = dummy; + + // 遍历两个链表,直到其中一个为空 + while (l1 != null && l2 != null) { + if (l1.val <= l2.val) { + // 将l1的当前节点连接到结果链表 + current.next = l1; + // 移动l1指针到下一个节点 + l1 = l1.next; + } else { + // 将l2的当前节点连接到结果链表 + current.next = l2; + // 移动l2指针到下一个节点 + l2 = l2.next; + } + // 移动结果链表指针到下一个节点 + current = current.next; + } + + // 将剩余节点链接到结果链表中 + if (l1 != null) { + // 将l1的剩余节点连接到结果链表 + current.next = l1; + } else { + // 将l2的剩余节点连接到结果链表 + current.next = l2; + } + + // 返回合并后的链表的头节点 + return dummy.next; + } + + /** + * 递归解法(补充) + * + * 算法思路: + * 将问题分解为更小的子问题 + * 每次选择较小的节点作为当前节点,然后递归处理剩余部分 + * + * 执行过程分析(以 l1=[1,2,4], l2=[1,3,4] 为例): + * + * 1. mergeTwoListsRecursive(1->2->4, 1->3->4) + * 1 <= 1, 选择l1节点1, 递归调用mergeTwoListsRecursive(2->4, 1->3->4) + * + * 2. mergeTwoListsRecursive(2->4, 1->3->4) + * 2 > 1, 选择l2节点1, 递归调用mergeTwoListsRecursive(2->4, 3->4) + * + * 3. mergeTwoListsRecursive(2->4, 3->4) + * 2 < 3, 选择l1节点2, 递归调用mergeTwoListsRecursive(4, 3->4) + * + * 4. mergeTwoListsRecursive(4, 3->4) + * 4 > 3, 选择l2节点3, 递归调用mergeTwoListsRecursive(4, 4) + * + * 5. mergeTwoListsRecursive(4, 4) + * 4 <= 4, 选择l1节点4, 递归调用mergeTwoListsRecursive(null, 4) + * + * 6. mergeTwoListsRecursive(null, 4) + * l1为null, 返回l2(4) + * + * 回溯过程连接节点,最终得到: 1->1->2->3->4->4 + * + * 时间复杂度分析: + * - 递归深度:O(m + n),其中m为第一个链表长度,n为第二个链表长度 + * - 每层递归执行常数时间操作 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(m + n) + * + * @param l1 第一个有序链表的头节点 + * @param l2 第二个有序链表的头节点 + * @return 合并后的有序链表的头节点 + */ + public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) { + // 基础情况:其中一个链表为空 + if (l1 == null) return l2; + if (l2 == null) return l1; + + // 递归情况:选择较小的节点 + if (l1.val <= l2.val) { + // 递归处理l1的下一个节点和l2 + l1.next = mergeTwoListsRecursive(l1.next, l2); + // 返回l1作为当前节点 + return l1; + } else { + // 递归处理l1和l2的下一个节点 + l2.next = mergeTwoListsRecursive(l1, l2.next); + // 返回l2作为当前节点 + return l2; + } + } + + /** + * 辅助方法:创建链表(用于测试) + */ + public ListNode createList(int[] values) { + if (values.length == 0) return null; + // 创建头节点 + ListNode head = new ListNode(values[0]); + // current 指向当前节点 + ListNode current = head; + // 遍历数组剩余元素 + for (int i = 1; i < values.length; i++) { + // 创建新节点并连接 + current.next = new ListNode(values[i]); + // 移动当前节点指针 + current = current.next; + } + // 返回链表头节点 + return head; + } + + /** + * 辅助方法:打印链表(用于测试) + */ + public void printList(ListNode head) { + // current 指向当前节点,初始为头节点 + ListNode current = head; + while (current != null) { + // 打印当前节点值 + System.out.print(current.val); + // 检查是否有下一个节点 + if (current.next != null) { + // 打印箭头 + System.out.print(" -> "); + } + // 移动当前节点指针 + current = current.next; + } + // 打印结束标记并换行 + System.out.println(" -> null"); + } + + /** + * 测试方法和使用示例 + */ + public static void main(String[] args) { + // 创建解决方案实例 + MergeTwoList21 solution = new MergeTwoList21(); + + // 创建测试链表 + ListNode l1 = solution.createList(new int[]{1, 2, 4}); + ListNode l2 = solution.createList(new int[]{1, 3, 4}); + + // 打印原始链表 + System.out.println("链表1:"); + solution.printList(l1); + System.out.println("链表2:"); + solution.printList(l2); + + // 测试迭代解法 + ListNode merged = solution.mergeTwoLists(l1, l2); + System.out.println("合并后的链表(迭代):"); + solution.printList(merged); + + // 重新创建链表用于递归测试 + l1 = solution.createList(new int[]{1, 2, 4}); + l2 = solution.createList(new int[]{1, 3, 4}); + + // 测试递归解法 + merged = solution.mergeTwoListsRecursive(l1, l2); + System.out.println("合并后的链表(递归):"); + solution.printList(merged); + } +} diff --git a/algorithm/MinDistance72.java b/algorithm/MinDistance72.java new file mode 100644 index 0000000000000..a36ace5894051 --- /dev/null +++ b/algorithm/MinDistance72.java @@ -0,0 +1,212 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 编辑距离(LeetCode 72)- 动态规划 + * + * 时间复杂度:O(m * n) + * - m是word1的长度,n是word2的长度 + * - 需要填充m*n的DP表 + * + * 空间复杂度:O(m * n) + * - 需要m*n的DP表存储中间结果 + * - 可以优化到O(min(m,n)),但这里为了清晰起见使用完整DP表 + */ +public class MinDistance72 { + + /** + * 计算两个字符串之间的编辑距离 + * + * 算法思路: + * 使用动态规划,定义`dp[i][j]`表示word1的前i个字符转换为word2的前j个字符所需的最少操作数 + * 状态转移方程: + * 1. 如果word1[i-1] == word2[j-1],则dp[i][j] = dp[i-1][j-1] + * 2. 如果word1[i-1] != word2[j-1],则dp[i][j] = min( + * dp[i-1][j] + 1, // 删除word1[i-1] + * dp[i][j-1] + 1, // 插入word2[j-1] + * dp[i-1][j-1] + 1 // 替换word1[i-1]为word2[j-1] + * ) + * + * 执行过程分析(以word1="horse", word2="ros"为例): + * + * 初始化DP表(m=5, n=3): + * "" r o s + * "" 0 1 2 3 + * h 1 + * o 2 + * r 3 + * s 4 + * e 5 + * + * 填充DP表: + * "" r o s + * "" 0 1 2 3 + * h 1 1 2 3 + * o 2 2 1 2 + * r 3 2 2 2 + * s 4 3 3 2 + * e 5 4 4 3 + * + * 填充过程详解: + * dp[1][1]: h!=r, min(dp[0][1]+1, dp[1][0]+1, dp[0][0]+1) = min(2,2,1) = 1 + * dp[2][2]: o==o, dp[1][1] = 1 + * dp[3][1]: r==r, dp[2][0] = 2 + * ... + * + * 最终结果:dp[5][3] = 3 + * + * 时间复杂度分析: + * - 初始化DP表边界:O(m + n) + * - 填充DP表:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - DP表存储空间:O(m * n) + * + * @param word1 源字符串 + * @param word2 目标字符串 + * @return 编辑距离 + */ + public int minDistance(String word1, String word2) { + int m = word1.length(); + int n = word2.length(); + + // 创建DP表,dp[i][j]表示word1前i个字符转换为word2前j个字符的最少操作数 + int[][] dp = new int[m + 1][n + 1]; + + // 初始化边界条件 + // 空字符串转换为word2的前j个字符需要j次插入操作 + for (int j = 0; j <= n; j++) { + dp[0][j] = j; + } + + // word1的前i个字符转换为空字符串需要i次删除操作 + for (int i = 0; i <= m; i++) { + dp[i][0] = i; + } + + // 填充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], // 删除 + dp[i][j - 1]), // 插入 + dp[i - 1][j - 1] // 替换 + ) + 1; + } + } + } + + // 返回最终的编辑距离 + return dp[m][n]; + } + + /** + * 空间优化版本:只使用两行数组 + * + * 算法思路: + * 观察DP状态转移方程,发现计算当前行只需要前一行的数据, + * 因此可以只使用两行数组来存储数据,从而优化空间复杂度 + * + * 时间复杂度分析: + * - 初始化:O(n) + * - 填充DP表:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - 只使用两行数组:O(n) + * + * @param word1 源字符串 + * @param word2 目标字符串 + * @return 编辑距离 + */ + public int minDistanceOptimized(String word1, String word2) { + int m = word1.length(); + int n = word2.length(); + + // 只需要两行数组 + int[] prev = new int[n + 1]; + int[] curr = new int[n + 1]; + + // 初始化第一行 + for (int j = 0; j <= n; j++) { + prev[j] = j; + } + + // 填充DP表 + for (int i = 1; i <= m; i++) { + curr[0] = i; // 初始化当前行的第一个元素 + + for (int j = 1; j <= n; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + curr[j] = prev[j - 1]; + } else { + curr[j] = Math.min( + Math.min(prev[j], // 删除 + curr[j - 1]), // 插入 + prev[j - 1] // 替换 + ) + 1; + } + } + + // 交换prev和curr + int[] temp = prev; + prev = curr; + curr = temp; + } + + return prev[n]; + } + + /** + * 辅助方法:读取用户输入的字符串 + * + * 时间复杂度分析: + * - 读取和处理输入:O(1) + * + * 空间复杂度分析: + * - 存储两个字符串:O(m + n) + * + * @return 用户输入的两个字符串数组 + */ + public static String[] readWords() { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入第一个字符串: "); + String word1 = scanner.nextLine(); + System.out.print("请输入第二个字符串: "); + String word2 = scanner.nextLine(); + + return new String[]{word1, word2}; + } + + /** + * 主函数:处理用户输入并计算编辑距离 + */ + public static void main(String[] args) { + System.out.println("编辑距离计算"); + + // 读取用户输入的字符串 + String[] words = readWords(); + String word1 = words[0]; + String word2 = words[1]; + + System.out.println("第一个字符串: \"" + word1 + "\""); + System.out.println("第二个字符串: \"" + word2 + "\""); + + // 计算编辑距离 + MinDistance72 solution = new MinDistance72(); + int distance1 = solution.minDistance(word1, word2); + int distance2 = solution.minDistanceOptimized(word1, word2); + + // 输出结果 + System.out.println("标准动态规划方法编辑距离: " + distance1); + System.out.println("空间优化方法编辑距离: " + distance2); + } +} diff --git a/algorithm/MinPathSum64.java b/algorithm/MinPathSum64.java new file mode 100644 index 0000000000000..e26dab573f17e --- /dev/null +++ b/algorithm/MinPathSum64.java @@ -0,0 +1,237 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 最小路径和(LeetCode 64)- 动态规划 + * + * 时间复杂度:O(m * n) + * - m是网格的行数,n是网格的列数 + * - 需要遍历整个网格一次 + * + * 空间复杂度:O(m * n) + * - 需要m*n的DP表存储中间结果 + * - 可以优化到O(n)或O(1)(如果允许修改原网格) + */ +public class MinPathSum64 { + + /** + * 计算从左上角到右下角的最小路径和 + * + * 算法思路: + * 使用动态规划,定义`dp[i][j]`表示从左上角到位置(i,j)的最小路径和 + * 状态转移方程: + * `dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])` + * + * 边界条件: + * 1. 第一行:`dp[0][j] = dp[0][j-1] + grid[0][j]` + * 2. 第一列:`dp[i][0] = dp[i-1][0] + grid[i][0]` + * + * 执行过程分析(以`grid=[[1,3,1],[1,5,1],[4,2,1]]`为例): + * + * 原始网格: + * 1 3 1 + * 1 5 1 + * 4 2 1 + * + * 填充DP表: + * 初始状态: + * 1 3 1 + * 1 5 1 + * 4 2 1 + * + * 处理第一行: + * 1 4 5 + * 1 5 1 + * 4 2 1 + * + * 处理第一列: + * 1 4 5 + * 2 5 1 + * 6 2 1 + * + * 填充其余位置: + * `dp[1][1] = 5 + min(4,2) = 7` + * `dp[1][2] = 1 + min(5,7) = 6` + * `dp[2][1] = 2 + min(6,7) = 8` + * `dp[2][2] = 1 + min(6,8) = 7` + * + * 最终DP表: + * 1 4 5 + * 2 7 6 + * 6 8 7 + * + * 最小路径和:`dp[2][2] = 7` + * 对应路径:1→1→1→1→1 = 7(路径可以是右→右→下→下→下或右→下→下→右→下等) + * + * 时间复杂度分析: + * - 初始化边界:O(m + n) + * - 填充DP表:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - DP表存储空间:O(m * n) + * + * @param grid 包含非负整数的m×n网格 + * @return 从左上角到右下角的最小路径和 + */ + public int minPathSum(int[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int m = grid.length; // 行数 + int n = grid[0].length; // 列数 + + // 创建DP表,`dp[i][j]`表示从左上角到位置(i,j)的最小路径和 + int[][] dp = new int[m][n]; + + // 初始化起点 + 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++) { + // 当前位置的最小路径和 = 当前值 + min(从上面来, 从左边来) + dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]); + } + } + + // 返回右下角的最小路径和 + return dp[m - 1][n - 1]; + } + + /** + * 空间优化版本:直接修改原网格 + * + * 算法思路: + * 直接在原网格上进行动态规划计算,节省额外空间 + * + * 时间复杂度分析: + * - 初始化边界:O(m + n) + * - 填充网格:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - 直接使用原网格:O(1)额外空间 + * + * @param grid 包含非负整数的m×n网格 + * @return 从左上角到右下角的最小路径和 + */ + public int minPathSumOptimized(int[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int m = grid.length; + int n = grid[0].length; + + // 初始化第一行 + for (int j = 1; j < n; j++) { + grid[0][j] += grid[0][j - 1]; + } + + // 初始化第一列 + for (int i = 1; i < m; i++) { + grid[i][0] += grid[i - 1][0]; + } + + // 填充其余位置 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]); + } + } + + return grid[m - 1][n - 1]; + } + + /** + * 辅助方法:读取用户输入的网格 + * + * 时间复杂度分析: + * - 读取输入:O(m * n) + * + * 空间复杂度分析: + * - 存储网格:O(m * n) + * + * @return 用户输入的二维整数数组 + */ + public static int[][] readGrid() { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入网格的行数: "); + int m = scanner.nextInt(); + System.out.print("请输入网格的列数: "); + int n = scanner.nextInt(); + + int[][] grid = new int[m][n]; + System.out.println("请输入网格元素(按行输入,每行元素用空格分隔):"); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + return grid; + } + + /** + * 辅助方法:打印网格 + * + * 时间复杂度分析: + * - 打印网格:O(m * n) + * + * 空间复杂度分析: + * - 无额外空间使用:O(1) + * + * @param grid 二维整数数组 + */ + public static void printGrid(int[][] grid) { + for (int[] row : grid) { + for (int cell : row) { + System.out.print(cell + " "); + } + System.out.println(); + } + } + + /** + * 主函数:处理用户输入并计算最小路径和 + */ + public static void main(String[] args) { + System.out.println("最小路径和计算"); + + // 读取用户输入的网格 + int[][] grid = readGrid(); + + System.out.println("输入的网格:"); + printGrid(grid); + + // 计算最小路径和 + MinPathSum64 solution = new MinPathSum64(); + int result1 = solution.minPathSum(grid); + + // 为了演示优化版本,需要重新创建网格(因为原网格被修改了) + int[][] gridCopy = new int[grid.length][grid[0].length]; + for (int i = 0; i < grid.length; i++) { + System.arraycopy(grid[i], 0, gridCopy[i], 0, grid[i].length); + } + + int result2 = solution.minPathSumOptimized(gridCopy); + + // 输出结果 + System.out.println("标准动态规划方法最小路径和: " + result1); + System.out.println("空间优化方法最小路径和: " + result2); + } +} diff --git a/algorithm/MinStack155.java b/algorithm/MinStack155.java new file mode 100644 index 0000000000000..583724a34b71d --- /dev/null +++ b/algorithm/MinStack155.java @@ -0,0 +1,170 @@ +package com.funian.algorithm.algorithm; + +import java.util.Stack; + +/** + * 最小栈(LeetCode 155) + * + * 时间复杂度:O(1) + * - 所有操作(push, pop, top, getMin)都是常数时间复杂度 + * + * 空间复杂度:O(n) + * - 需要两个栈存储元素,最坏情况下需要2n的空间 + */ +public class MinStack155 { + + // 主栈用于存储所有元素 + // 按照正常的栈顺序存储所有推入的元素 + private Stack stack; + + // 辅助栈用于存储最小元素 + // 栈顶始终存储当前主栈中的最小元素 + private Stack minStack; + + // 初始化堆栈对象 + public MinStack155() { + // 创建主栈 + stack = new Stack<>(); + // 创建辅助栈(用于跟踪最小值) + minStack = new Stack<>(); + } + + /** + * 将元素 val 推入堆栈 + * + * 算法思路: + * 1. 将元素推入主栈 + * 2. 如果辅助栈为空,或者新元素小于等于辅助栈栈顶元素,则也将其推入辅助栈 + * + * 执行过程分析(依次推入 -2, 0, -3): + * + * push(-2): + * stack.push(-2) -> stack = [-2] + * minStack.isEmpty() = true + * minStack.push(-2) -> minStack = [-2] + * + * push(0): + * stack.push(0) -> stack = [-2, 0] + * 0 > -2,不推入辅助栈 + * minStack = [-2] + * + * push(-3): + * stack.push(-3) -> stack = [-2, 0, -3] + * -3 < -2,推入辅助栈 + * minStack.push(-3) -> minStack = [-2, -3] + * + * @param val 要推入的整数值 + */ + public void push(int val) { + // 将元素推入主栈(正常栈操作) + stack.push(val); + + // 维护辅助栈:如果辅助栈为空,或者 val 小于等于辅助栈的栈顶元素,将 val 推入辅助栈 + // 使用 <= 而不是 < 是为了处理重复的最小值 + if (minStack.isEmpty() || val <= minStack.peek()) { + minStack.push(val); + } + } + + /** + * 删除堆栈顶部的元素 + * + * 算法思路: + * 1. 如果主栈栈顶元素等于辅助栈栈顶元素,说明要删除的是当前最小元素,同时弹出两个栈的栈顶元素 + * 2. 否则只弹出主栈的栈顶元素 + * + * 执行过程分析(接上面的例子,当前状态:stack=[-2,0,-3], minStack=[-2,-3]): + * + * pop(): + * stack.peek() = -3, minStack.peek() = -3 + * 两者相等,说明-3是最小值 + * minStack.pop() -> minStack = [-2] + * stack.pop() -> stack = [-2, 0] + * + * pop(): + * stack.peek() = 0, minStack.peek() = -2 + * 两者不相等,只弹出主栈 + * stack.pop() -> stack = [-2] + */ + public void pop() { + // 关键边界条件:如果栈顶元素等于辅助栈的栈顶元素,同时弹出两个栈的栈顶元素 + // 使用equals而不是==是为了避免Integer对象比较的问题 + if (stack.peek().equals(minStack.peek())) { + // 弹出辅助栈栈顶(当前最小值被删除) + minStack.pop(); + } + // 弹出主栈栈顶 + stack.pop(); + } + + /** + * 获取堆栈顶部的元素 + * + * @return 堆栈顶部的元素 + */ + public int top() { + // 直接返回主栈栈顶元素 + return stack.peek(); + } + + /** + * 获取堆栈中的最小元素 + * + * @return 堆栈中的最小元素 + */ + public int getMin() { + // 直接返回辅助栈栈顶元素(即当前最小值) + return minStack.peek(); + } + + /** + * 方法2:使用单个栈和一个变量实现 + * + * 算法思路: + * 使用一个变量存储最小值,栈中存储与最小值的差值 + */ + static class MinStackSingle { + private Stack stack; + private long min; + + public MinStackSingle() { + stack = new Stack<>(); + } + + public void push(int val) { + if (stack.isEmpty()) { + stack.push(0L); + min = val; + } else { + // 存储与当前最小值的差值 + stack.push((long)val - min); + if (val < min) { + min = val; + } + } + } + + public void pop() { + if (stack.isEmpty()) return; + + long pop = stack.pop(); + if (pop < 0) { + // 弹出的是最小值,需要恢复之前的最小值 + min = min - pop; + } + } + + public int top() { + long top = stack.peek(); + if (top > 0) { + return (int)(top + min); + } else { + return (int)min; + } + } + + public int getMin() { + return (int)min; + } + } +} diff --git a/algorithm/MinWindow76.java b/algorithm/MinWindow76.java new file mode 100644 index 0000000000000..71798eb2a849c --- /dev/null +++ b/algorithm/MinWindow76.java @@ -0,0 +1,332 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * 最小覆盖子串(LeetCode 76) + * + * 时间复杂度:O(|s| + |t|) + * - |s| 是字符串 s 的长度,|t| 是字符串 t 的长度 + * - 需要遍历字符串 s 一次:O(|s|) + * - 需要遍历字符串 t 一次:O(|t|) + * - 每个字符最多被访问两次(一次被右指针,一次被左指针) + * + * 空间复杂度:O(|s| + |t|) + * - HashMap 存储字符串 t 中字符的频率:O(|t|) + * - HashMap 存储滑动窗口中字符的频率:最坏情况 O(|s|) + */ +public class MinWindow76 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入字符串 s + System.out.print("请输入字符串 s:"); + + // 读取字符串 s + String s = scanner.nextLine(); + + // 提示用户输入字符串 t + System.out.print("请输入字符串 t:"); + + // 读取字符串 t + String t = scanner.nextLine(); + + // 调用 minWindow 方法返回结果 + String result = minWindow(s, t); + + // 输出结果 + System.out.println("最小覆盖子串为:" + result); + } + + /** + * 找到字符串 s 中涵盖字符串 t 所有字符的最小子串 + * + * 算法思路: + * 使用滑动窗口技术,通过双指针维护一个窗口 + * 1. 右指针不断扩展窗口,直到窗口包含 t 中的所有字符 + * 2. 当窗口满足条件时,左指针尝试收缩窗口以找到最小解 + * 3. 维护一个有效字符计数器,记录窗口中满足字符数量要求的字符种类数 + * + * 示例过程(以 s="ADOBECODEBANC", t="ABC" 为例): + * + * 步骤 窗口 包含字符 有效字符数 是否满足 最小窗口 + * 1 [A) A:1 1 否 "" + * 2 [AD) A:1,D:1 1 否 "" + * 3 [ADO) A:1,D:1,O:1 1 否 "" + * 4 [ADOB) A:1,B:1,D:1,O:1 2 否 "" + * 5 [ADOBE) A:1,B:1,D:1,E:1,O:1 2 否 "" + * 6 [ADOBEC) A:1,B:1,C:1,... 3 是 "ADOBEC" + * 7 A[D OBEC) A:0,B:1,C:1,... 2 否 "ADOBEC" + * ...继续滑动直到找到更优解... + * 最终结果: "BANC" + * + * 时间复杂度分析: + * - 遍历字符串 t 统计字符频率:O(|t|),其中|t|为字符串`t`的长度 + * - 滑动窗口遍历字符串 s:O(|s|),其中|s|为字符串`s`的长度 + * - 每个字符最多被访问两次(右指针一次,左指针一次):O(|s|) + * - HashMap操作:O(1) + * - 总时间复杂度:O(|s| + |t|) + * + * 空间复杂度分析: + * - targetMap存储t中字符频率:O(|t|) + * - windowMap存储窗口中字符频率:最坏情况O(|s|) + * - 其他变量:O(1) + * - 总空间复杂度:O(|s| + |t|) + * + * @param s 主字符串 + * @param t 需要覆盖的字符集合 + * @return 最小覆盖子串,如果不存在则返回空字符串 + */ + public static String minWindow(String s, String t) { + // 边界条件检查:如果任一字符串为空,返回空字符串 + if (s == null || s.length() == 0 || t == null || t.length() == 0) { + return ""; + } + + // 记录 t 中每个字符的需求数量 + Map targetMap = new HashMap<>(); + for (char c : t.toCharArray()) { + targetMap.put(c, targetMap.getOrDefault(c, 0) + 1); + } + + // 滑动窗口所需的变量 + Map windowMap = new HashMap<>(); // 记录窗口中每个字符的实际数量 + int left = 0, right = 0; // 左右指针,表示窗口的范围 [left, right) + int valid = 0; // 符合条件的字符种类数(窗口中数量满足需求的字符种类) + int minLen = Integer.MAX_VALUE; // 最小窗口长度,初始化为最大整数值 + int start = 0; // 最小窗口的起始位置 + + // 右指针遍历字符串 s + while (right < s.length()) { + // 获取当前右边界字符 + char c = s.charAt(right); + // 扩展右边界 + right++; + + // 如果该字符在 t 中,则加入窗口进行处理 + if (targetMap.containsKey(c)) { + // 更新窗口中该字符的数量 + windowMap.put(c, windowMap.getOrDefault(c, 0) + 1); + // 如果窗口中的该字符数量和 t 中需求一致,则有效字符数 +1 + if (windowMap.get(c).equals(targetMap.get(c))) { + valid++; // 有效字符种类数增加 + } + } + + // 当窗口内的字符已经满足 t 中所有字符时,尝试收缩左边界 + while (valid == targetMap.size()) { + // 更新最小窗口长度和起始位置 + if (right - left < minLen) { + minLen = right - left; // 更新最小窗口长度 + start = left; // 更新最小窗口起始位置 + } + + // 获取左边界字符 + char d = s.charAt(left); + // 收缩左边界 + left++; + + // 如果该字符在 t 中,收缩窗口时需要进行处理 + if (targetMap.containsKey(d)) { + // 如果该字符数量减少到不再满足需求,则有效字符数 -1 + if (windowMap.get(d).equals(targetMap.get(d))) { + valid--; // 有效字符种类数减少 + } + // 更新窗口中的字符数量 + windowMap.put(d, windowMap.get(d) - 1); + } + } + } + + // 如果 minLen 没有被更新过,说明没有找到符合条件的子串,返回空字符串 + return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); + } + + /** + * 方法2:优化版本(使用数组代替HashMap) + * + * 算法思路: + * 使用数组代替HashMap提高访问速度,适用于ASCII字符 + * + * 示例过程(以 s="ADOBECODEBANC", t="ABC" 为例): + * + * 1. 初始化: + * targetCount数组统计t中字符频率: targetCount['A']=1, targetCount['B']=1, targetCount['C']=1 + * required=3 (t中不同字符数) + * + * 2. 滑动窗口过程: + * right=0, c='A': windowCount['A']++, formed=1 + * right=1, c='D': targetCount['D']=0, 跳过 + * right=2, c='O': targetCount['O']=0, 跳过 + * right=3, c='B': windowCount['B']++, formed=2 + * right=4, c='E': targetCount['E']=0, 跳过 + * right=5, c='C': windowCount['C']++, formed=3 == required + * + * 进入收缩阶段: window="ADOBEC", len=6, start=0 + * left=0, d='A': windowCount['A']--, formed=2 + * left=1, d='D': targetCount['D']=0, 跳过 + * left=2, d='O': targetCount['O']=0, 跳过 + * left=3, d='B': windowCount['B']--, formed=1 + * ... + * + * 时间复杂度分析: + * - 遍历字符串 t 统计字符频率:O(|t|) + * - 滑动窗口遍历字符串 s:O(|s|) + * - 数组访问:O(1) + * - 总时间复杂度:O(|s| + |t|) + * + * 空间复杂度分析: + * - 两个字符计数数组:O(128) = O(1) + * - 其他变量:O(1) + * - 总空间复杂度:O(1) + * + * @param s 主字符串 + * @param t 需要覆盖的字符集合 + * @return 最小覆盖子串,如果不存在则返回空字符串 + */ + public static String minWindowOptimized(String s, String t) { + if (s == null || s.length() == 0 || t == null || t.length() == 0) { + return ""; + } + + // 使用数组记录字符频率(假设为ASCII字符) + int[] targetCount = new int[128]; + int[] windowCount = new int[128]; + + // 统计t中字符频率 + for (char c : t.toCharArray()) { + targetCount[c]++; + } + + // 统计t中不同字符的数量 + int required = 0; + for (int count : targetCount) { + if (count > 0) { + required++; + } + } + + int left = 0, right = 0; + int formed = 0; // 窗口中满足频率要求的字符种类数 + int minLen = Integer.MAX_VALUE; + int start = 0; + + while (right < s.length()) { + char c = s.charAt(right); + right++; + + if (targetCount[c] > 0) { + windowCount[c]++; + if (windowCount[c] == targetCount[c]) { + formed++; + } + } + + while (formed == required) { + if (right - left < minLen) { + minLen = right - left; + start = left; + } + + char d = s.charAt(left); + left++; + + if (targetCount[d] > 0) { + if (windowCount[d] == targetCount[d]) { + formed--; + } + windowCount[d]--; + } + } + } + + return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); + } + + /** + * 方法3:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 遍历所有可能的子串,检查是否包含t的所有字符 + * + * 示例过程(以 s="ADOBECODEBANC", t="ABC" 为例): + * + * i=0: 子串"A"不包含ABC; "AD"不包含ABC; ...; "ADOBEC"包含ABC,记录 + * i=1: 子串"D"不包含ABC; "DO"不包含ABC; ...继续检查 + * ... + * 需要检查所有O(|s|²)个子串,每个子串需要O(|s|)时间统计字符 + * + * 时间复杂度分析: + * - 外层循环遍历起始位置:O(|s|) + * - 内层循环遍历结束位置:O(|s|) + * - containsAll方法检查字符包含:O(|t|) + * - substring方法创建子串:O(|s|) + * - 总时间复杂度:O(|s|³) + * + * 空间复杂度分析: + * - targetMap存储t中字符频率:O(|t|) + * - windowMap存储窗口中字符频率:O(|s|) + * - minWindow存储结果子串:O(|s|) + * - 总空间复杂度:O(|s| + |t|) + * + * @param s 主字符串 + * @param t 需要覆盖的字符集合 + * @return 最小覆盖子串,如果不存在则返回空字符串 + */ + public static String minWindowBruteForce(String s, String t) { + if (s == null || s.length() == 0 || t == null || t.length() == 0) { + return ""; + } + + // 统计t中字符频率 + Map targetMap = new HashMap<>(); + for (char c : t.toCharArray()) { + targetMap.put(c, targetMap.getOrDefault(c, 0) + 1); + } + + String minWindow = ""; + int minLen = Integer.MAX_VALUE; + + // 遍历所有可能的子串 + for (int i = 0; i < s.length(); i++) { + Map windowMap = new HashMap<>(); + for (int j = i; j < s.length(); j++) { + char c = s.charAt(j); + windowMap.put(c, windowMap.getOrDefault(c, 0) + 1); + + // 检查当前窗口是否包含t的所有字符 + if (containsAll(windowMap, targetMap)) { + int currentLen = j - i + 1; + if (currentLen < minLen) { + minLen = currentLen; + minWindow = s.substring(i, j + 1); + } + break; // 找到满足条件的窗口后可以跳出内层循环 + } + } + } + + return minWindow; + } + + /** + * 辅助方法:检查窗口是否包含目标字符串的所有字符 + * + * @param windowMap 窗口字符频率 + * @param targetMap 目标字符频率 + * @return 如果窗口包含所有目标字符返回true,否则返回false + */ + private static boolean containsAll(Map windowMap, Map targetMap) { + for (Map.Entry entry : targetMap.entrySet()) { + char c = entry.getKey(); + int count = entry.getValue(); + if (windowMap.getOrDefault(c, 0) < count) { + return false; + } + } + return true; + } +} diff --git a/algorithm/MoveZero283.java b/algorithm/MoveZero283.java new file mode 100644 index 0000000000000..b13001cd1077f --- /dev/null +++ b/algorithm/MoveZero283.java @@ -0,0 +1,226 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 移动零(LeetCode 283)- 优化版本 + * + * 时间复杂度:O(n) + * - 只需要遍历数组一次 + * - 每个元素最多被访问和交换一次 + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 通过原地交换完成操作,不需要额外的存储空间 + */ +public class MoveZero283 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("请输入数组元素(用空格分隔):"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] str = line.split(" "); + + // 获取数组长度 + int n = str.length; + + // 创建整型数组 + int[] nums = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(str[i]); + } + + // 调用 moveZeroes 方法移动零元素 + moveZeroes(nums); + + // 输出结果 + System.out.println("处理后的数组:"); + for (int num : nums) { + System.out.print(num + " "); + } + System.out.println(); + } + + /** + * 将数组中的所有 0 移动到数组的末尾,同时保持非零元素的相对顺序 + * 使用双指针优化算法,只需要一次遍历 + * + * 算法思路: + * 使用双指针技术,left指针指向下一个非零元素应该放置的位置 + * right指针遍历数组,遇到非零元素就与left指针位置交换 + * 这样保证了非零元素的相对顺序,并将零元素移到末尾 + * + * 示例过程(以数组 [0,1,0,3,12] 为例): + * + * 初始状态: + * nums = [0, 1, 0, 3, 12] + * L + * R + * + * right=0: nums[0]=0,跳过 + * right=1: nums[1]=1≠0,交换nums[0]和nums[1],left++ + * nums = [1, 0, 0, 3, 12] + * L + * R + * right=2: nums[2]=0,跳过 + * right=3: nums[3]=3≠0,交换nums[1]和nums[3],left++ + * nums = [1, 3, 0, 0, 12] + * L + * R + * right=4: nums[4]=12≠0,交换nums[2]和nums[4],left++ + * nums = [1, 3, 12, 0, 0] + * L + * R + * + * 最终结果: [1, 3, 12, 0, 0] + * + * 时间复杂度分析: + * - 单次遍历数组:O(n),其中n为输入数组`nums`的长度 + * - 每次交换操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了几个变量存储索引和临时值:O(1) + * - 原地操作,不需要额外数组:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + */ + public static void moveZeroes(int[] nums) { + // 使用双指针技术 + // left 指针指向下一个非零元素应该放置的位置 + int left = 0; + + // 遍历数组 + for (int right = 0; right < nums.length; right++) { + // 如果右指针指向的元素不为0 + if (nums[right] != 0) { + // 将右指针指向的非零元素与左指针指向的位置交换 + int temp = nums[left]; + nums[left] = nums[right]; + nums[right] = temp; + + // 左指针向前移动一位 + left++; + } + } + } + + /** + * 方法2:两次遍历法 + * + * 算法思路: + * 1. 第一次遍历:将所有非零元素移动到数组前面 + * 2. 第二次遍历:将剩余位置填充为0 + * + * 示例过程(以数组 [0,1,0,3,12] 为例): + * + * 初始状态: + * nums = [0, 1, 0, 3, 12] + * + * 第一次遍历: + * i=0: nums[0]=0,跳过 + * i=1: nums[1]=1≠0,nums[0]=1,index=1 + * i=2: nums[2]=0,跳过 + * i=3: nums[3]=3≠0,nums[1]=3,index=2 + * i=4: nums[4]=12≠0,nums[2]=12,index=3 + * 结果: nums = [1, 3, 12, 3, 12],index=3 + * + * 第二次遍历: + * index=3: nums[3]=0,index=4 + * index=4: nums[4]=0,index=5 + * 结果: nums = [1, 3, 12, 0, 0] + * + * 时间复杂度分析: + * - 第一次遍历:O(n),其中n为输入数组`nums`的长度 + * - 第二次遍历:O(n-k),其中k为非零元素个数 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了一个索引变量:O(1) + * - 原地操作:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + */ + public static void moveZeroesTwoPass(int[] nums) { + // 第一次遍历:将所有非零元素移动到数组前面 + int index = 0; + + // 遍历数组,将非零元素依次放到前面 + for (int i = 0; i < nums.length; i++) { + if (nums[i] != 0) { + nums[index++] = nums[i]; + } + } + + // 第二次遍历:将剩余位置填充为0 + while (index < nums.length) { + nums[index++] = 0; + } + } + + /** + * 方法3:优化的交换法(减少不必要的交换) + * + * 算法思路: + * 只有当left和right不相等时才进行交换,避免自己与自己交换 + * + * 示例过程(以数组 [0,1,0,3,12] 为例): + * + * 初始状态: + * nums = [0, 1, 0, 3, 12] + * L + * R + * + * right=0: nums[0]=0,跳过 + * right=1: nums[1]=1≠0,left≠right,交换nums[0]和nums[1],left++ + * nums = [1, 0, 0, 3, 12] + * L + * R + * right=2: nums[2]=0,跳过 + * right=3: nums[3]=3≠0,left≠right,交换nums[1]和nums[3],left++ + * nums = [1, 3, 0, 0, 12] + * L + * R + * right=4: nums[4]=12≠0,left≠right,交换nums[2]和nums[4],left++ + * nums = [1, 3, 12, 0, 0] + * L + * R + * + * 时间复杂度分析: + * - 单次遍历数组:O(n),其中n为输入数组`nums`的长度 + * - 交换操作减少,但仍为O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了几个变量存储索引和临时值:O(1) + * - 原地操作:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + */ + public static void moveZeroesOptimized(int[] nums) { + int left = 0; + + for (int right = 0; right < nums.length; right++) { + if (nums[right] != 0) { + // 只有当left和right不相等时才进行交换 + if (left != right) { + int temp = nums[left]; + nums[left] = nums[right]; + nums[right] = temp; + } + left++; + } + } + } +} diff --git a/algorithm/NextPermutation31.java b/algorithm/NextPermutation31.java new file mode 100644 index 0000000000000..2b7c7959c7feb --- /dev/null +++ b/algorithm/NextPermutation31.java @@ -0,0 +1,196 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 下一个排列(LeetCode 31) + * + * 时间复杂度:O(n) + * - 需要遍历数组常数次 + * - 最坏情况下需要遍历整个数组 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 原地修改数组 + */ +public class NextPermutation31 { + + /** + * 主函数:处理用户输入并计算下一个排列 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用[nextPermutation](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/NextPermutation31.java#L137-L163)方法计算下一个排列 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入整数数组 + System.out.print("请输入整数数组(用空格分隔):"); + String line = scanner.nextLine(); + String[] strArray = line.split(" "); + + // 将字符串数组转换为整数数组 + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); // 转换字符串为整数 + } + + // 保存原始数组用于输出 + int[] original = Arrays.copyOf(nums, nums.length); + + // 调用 nextPermutation 方法找出下一个排列 + nextPermutation(nums); + + // 输出结果 + System.out.print("原排列:"); + for (int num : original) { + System.out.print(num + " "); + } + System.out.println(); + + System.out.print("下一个排列为:"); + for (int num : nums) { + System.out.print(num + " "); // 打印下一个排列 + } + System.out.println(); + } + + /** + * 计算下一个排列 + * + * 算法思路: + * 1. 从右往左找到第一个递减对 (i, i+1),即 nums[i] < nums[i+1] + * 2. 从右往左找到第一个大于 nums[i] 的元素 nums[j] + * 3. 交换 nums[i] 和 nums[j] + * 4. 反转 i+1 到末尾的部分,使其变为最小排列 + * + * 执行过程分析(以数组 [1,2,3] 为例): + * + * 初始数组: [1, 2, 3] + * + * 步骤1:找到逆序对 + * 从右往左检查:3>2,2>1,都满足递增 + * 找到第一对递减元素:无,i = -1 + * + * 步骤2:i < 0,直接跳到步骤4 + * + * 步骤4:反转整个数组 [3, 2, 1] + * + * 执行过程分析(以数组 [1,3,2] 为例): + * + * 初始数组: [1, 3, 2] + * + * 步骤1:找到逆序对 + * 从右往左检查:2<3,找到递减对 + * i = 1 (对应元素3) + * + * 步骤2:寻找替换元素 + * 从右往左找到第一个大于 nums[1]=3 的元素 + * nums[2]=2 < 3,继续 + * nums[1]=3 = 3,不满足 + * nums[0]=1 < 3,不满足 + * 实际上应该找到从右往左第一个大于nums[i]的元素,这里是不存在的 + * 让我们重新分析 [1,3,2]: + * + * 步骤1:找到递减对 + * 从右往左检查:2<3,找到递减对 + * i = 1 (对应元素3) + * + * 步骤2:寻找替换元素 + * 从右往左找到第一个大于 nums[1]=3 的元素 + * 但实际上2<3,没有找到 + * + * 让我们用 [1,2,3,4] -> [1,2,4,3] 为例: + * 步骤1:从右往左找到第一个 nums[i] < nums[i+1],i=2 (元素3) + * 步骤2:从右往左找到第一个大于3的元素,j=3 (元素4) + * 步骤3:交换3和4,得到 [1,2,4,3] + * 步骤4:反转i+1到末尾,即反转[3],得到 [1,2,4,3] + * + * 再以 [1,3,2] 为例: + * 步骤1:从右往左找到第一个 nums[i] < nums[i+1],i=0 (元素1) + * 步骤2:从右往左找到第一个大于1的元素,j=2 (元素2) + * 步骤3:交换1和2,得到 [2,3,1] + * 步骤4:反转i+1到末尾,即反转[3,1]得到[1,3],最终 [2,1,3] + * + * 时间复杂度分析: + * - 查找递减对:O(n) + * - 查找替换元素:O(n) + * - 交换元素:O(1) + * - 反转数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 原地修改数组:O(1) + * + * @param nums 整数数组 + */ + public static void nextPermutation(int[] nums) { + int n = nums.length; + int i = n - 2; + + // 步骤1:从右往左找到第一个递减对 (i, i+1),即 nums[i] < nums[i+1] + // 如果整个数组都是递减的,则i最终会变为-1 + while (i >= 0 && nums[i] >= nums[i + 1]) { + i--; + } + + // 如果找到了递减对 + if (i >= 0) { + // 步骤2:从右往左找到第一个大于 nums[i] 的元素 nums[j] + int j = n - 1; + while (nums[j] <= nums[i]) { + j--; + } + // 步骤3:交换 nums[i] 和 nums[j] + swap(nums, i, j); + } + + // 步骤4:反转从 i+1 到末尾的部分 + // 如果i=-1(整个数组递减),则反转整个数组 + reverse(nums, i + 1, n - 1); + } + + /** + * 交换数组中两个元素的辅助方法 + * + * 时间复杂度分析: + * - 交换操作:O(1) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param nums 数组 + * @param i 第一个元素的索引 + * @param j 第二个元素的索引 + */ + public static void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + /** + * 反转数组从 start 到 end 的辅助方法 + * + * 时间复杂度分析: + * - 反转操作:O(n),其中n为(end-start+1)/2 + * + * 空间复杂度分析: + * - 原地操作:O(1) + * + * @param nums 数组 + * @param start 起始索引 + * @param end 结束索引 + */ + public static void reverse(int[] nums, int start, int end) { + while (start < end) { + swap(nums, start, end); + start++; + end--; + } + } +} diff --git a/algorithm/NumIsLands200.java b/algorithm/NumIsLands200.java new file mode 100644 index 0000000000000..bce2bafe19790 --- /dev/null +++ b/algorithm/NumIsLands200.java @@ -0,0 +1,254 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 岛屿数量(LeetCode 200) + * + * 时间复杂度:O(M×N) + * - M和N是网格的行数和列数 + * - 最坏情况下需要访问每个单元格一次 + * + * 空间复杂度:O(M×N) + * - 最坏情况下递归调用栈深度为M×N(整个网格都是陆地) + */ +public class NumIsLands200 { + + /** + * 计算二维网格中的岛屿数量 + * 岛屿由 '1' 表示,水由 '0' 表示 + * 岛屿在水平和垂直方向上相连 + * + * 算法思路: + * 使用深度优先搜索(DFS)遍历网格 + * 1. 遍历网格中的每个单元格 + * 2. 当发现未访问的陆地('1')时,岛屿计数加1 + * 3. 使用DFS将与当前陆地相连的所有陆地标记为已访问('0') + * 4. 继续遍历直到处理完所有单元格 + * + * @param grid 二维字符数组,表示地图 + * @return 岛屿的数量 + */ + public int numIslands(char[][] grid) { + // 检查边界条件 + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int rows = grid.length; + int cols = grid[0].length; + int islandCount = 0; + + // 遍历网格中的每个单元格 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 如果发现一个未访问的陆地单元格 + if (grid[i][j] == '1') { + islandCount++; + // 使用DFS将与当前陆地相连的所有陆地标记为已访问 + dfs(grid, i, j); + } + } + } + + return islandCount; + } + + /** + * 深度优先搜索,将与当前陆地相连的所有陆地标记为已访问('0') + * + * @param grid 二维字符数组 + * @param row 当前行索引 + * @param col 当前列索引 + */ + private void dfs(char[][] grid, int row, int col) { + int rows = grid.length; + int cols = grid[0].length; + + // 检查边界条件和是否已访问 + // 剪枝:如果越界或者当前单元格是水('0'),直接返回 + if (row < 0 || col < 0 || row >= rows || col >= cols || grid[row][col] == '0') { + return; + } + + // 标记当前单元格为已访问(将陆地'1'改为水'0') + // 这样避免了使用额外的visited数组 + grid[row][col] = '0'; + + // 递归访问四个相邻单元格(上下左右) + dfs(grid, row + 1, col); + dfs(grid, row - 1, col); + dfs(grid, row, col + 1); + dfs(grid, row, col - 1); + } + + /** + * 方法2:广度优先搜索(BFS)解法 + * + * 算法思路: + * 使用队列进行广度优先搜索 + * 1. 发现陆地时,将位置加入队列 + * 2. 从队列中取出位置,检查四个方向的相邻位置 + * 3. 将相邻的陆地加入队列并标记为已访问 + * + * @param grid 二维字符数组 + * @return 岛屿的数量 + */ + public int numIslandsBFS(char[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int rows = grid.length; + int cols = grid[0].length; + int islandCount = 0; + + // 四个方向的偏移量 + int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == '1') { + islandCount++; + + // 使用BFS标记相连的陆地 + java.util.Queue queue = new java.util.LinkedList<>(); + queue.offer(new int[]{i, j}); + grid[i][j] = '0'; + + while (!queue.isEmpty()) { + int[] current = queue.poll(); + int row = current[0]; + int col = current[1]; + + // 检查四个方向 + for (int[] dir : directions) { + int newRow = row + dir[0]; + int newCol = col + dir[1]; + + if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && + grid[newRow][newCol] == '1') { + grid[newRow][newCol] = '0'; + queue.offer(new int[]{newRow, newCol}); + } + } + } + } + } + } + + return islandCount; + } + + /** + * 方法3:并查集解法 + * + * 算法思路: + * 使用并查集数据结构,将相邻的陆地合并到同一个集合中 + * 最终岛屿数量等于并查集中不同集合的数量 + * + * @param grid 二维字符数组 + * @return 岛屿的数量 + */ + public int numIslandsUnionFind(char[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + int rows = grid.length; + int cols = grid[0].length; + UnionFind uf = new UnionFind(grid); + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == '1') { + // 检查下方和右方的相邻陆地 + if (i + 1 < rows && grid[i + 1][j] == '1') { + uf.union(i * cols + j, (i + 1) * cols + j); + } + if (j + 1 < cols && grid[i][j + 1] == '1') { + uf.union(i * cols + j, i * cols + j + 1); + } + } + } + } + + return uf.getCount(); + } + + /** + * 并查集辅助类 + */ + class UnionFind { + private int count; + private int[] parent; + private int[] rank; + + public UnionFind(char[][] grid) { + int rows = grid.length; + int cols = grid[0].length; + parent = new int[rows * cols]; + rank = new int[rows * cols]; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == '1') { + parent[i * cols + j] = i * cols + j; + ++count; + } + rank[i * cols + j] = 0; + } + } + } + + public int find(int i) { + if (parent[i] != i) { + parent[i] = find(parent[i]); // 路径压缩 + } + return parent[i]; + } + + public void union(int x, int y) { + int rootx = find(x); + int rooty = find(y); + if (rootx != rooty) { + // 按秩合并 + if (rank[rootx] > rank[rooty]) { + parent[rooty] = rootx; + } else if (rank[rootx] < rank[rooty]) { + parent[rootx] = rooty; + } else { + parent[rooty] = rootx; + rank[rootx] += 1; + } + --count; + } + } + + public int getCount() { + return count; + } + } + + /** + * 主函数:处理用户输入并计算岛屿数量 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + // 读取网格的行数和列数 + int m = scanner.nextInt(); + int n = scanner.nextInt(); + scanner.nextLine(); + + // 创建网格并读取输入 + char[][] grid = new char[m][n]; + for (int i = 0; i < m; i++) { + String line = scanner.nextLine(); + grid[i] = line.toCharArray(); + } + + NumIsLands200 solution = new NumIsLands200(); + int islandCount = solution.numIslands(grid); + System.out.println(islandCount); + } +} diff --git a/algorithm/NumSquares279.java b/algorithm/NumSquares279.java new file mode 100644 index 0000000000000..c3dc2a8d8224d --- /dev/null +++ b/algorithm/NumSquares279.java @@ -0,0 +1,223 @@ +package com.funian.algorithm.algorithm; + +/** + * 完全平方数(LeetCode 279)- 动态规划 + * + * 时间复杂度:O(n * √n) + * - 外层循环运行n次 + * - 内层循环最多运行√n次(j*j ≤ i) + * + * 空间复杂度:O(n) + * - 需要长度为n+1的DP数组 + */ +import java.util.Scanner; +import java.util.Arrays; + +public class NumSquares279 { + + /** + * 主函数:处理用户输入并计算完全平方数的最少数量 + * + * 算法流程: + * 1. 读取用户输入的整数n + * 2. 调用 [numSquares](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/NumSquares279.java#L107-L134)方法计算完全平方数的最少数量 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.print("请输入一个整数 n:"); + int n = scanner.nextInt(); + + int result = numSquares(n); + System.out.println("和为 " + n + " 的完全平方数的最少数量是:" + result); + + // 演示其他解法 + NumSquares279 solution = new NumSquares279(); + int result2 = solution.numSquaresBFS(n); + System.out.println("BFS方法结果: " + result2); + } + + /** + * 计算和为n的完全平方数的最少数量 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示和为i的完全平方数的最少数量 + * 状态转移方程: + * `dp[i] = min(dp[i - j*j] + 1)` for all j where j*j <= i + * + * 执行过程分析(以n=12为例): + * + * 初始化DP数组: + * dp = [0, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX] + * + * 填充DP数组: + * dp[1] = min(dp[0] + 1) = 1 (1 = 1²) + * dp[2] = min(dp[1] + 1) = 2 (2 = 1² + 1²) + * dp[3] = min(dp[2] + 1) = 3 (3 = 1² + 1² + 1²) + * dp[4] = min(dp[3] + 1, dp[0] + 1) = 1 (4 = 2²) + * dp[5] = min(dp[4] + 1, dp[1] + 1) = 2 (5 = 2² + 1²) + * dp[6] = min(dp[5] + 1, dp[2] + 1) = 3 (6 = 2² + 1² + 1²) + * dp[7] = min(dp[6] + 1, dp[3] + 1) = 4 (7 = 2² + 1² + 1² + 1²) + * dp[8] = min(dp[7] + 1, dp[4] + 1) = 2 (8 = 2² + 2²) + * dp[9] = min(dp[8] + 1, dp[5] + 1, dp[0] + 1) = 1 (9 = 3²) + * dp[10] = min(dp[9] + 1, dp[6] + 1, dp[1] + 1) = 2 (10 = 3² + 1²) + * dp[11] = min(dp[10] + 1, dp[7] + 1, dp[2] + 1) = 3 (11 = 3² + 1² + 1²) + * dp[12] = min(dp[11] + 1, dp[8] + 1, dp[3] + 1) = 3 (12 = 2² + 2² + 2²) + * + * 最终结果:dp[12] = 3 + * + * 时间复杂度分析: + * - 初始化DP数组:O(n) + * - 填充DP数组:O(n * √n) + * - 总时间复杂度:O(n * √n) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * + * @param n 目标整数 + * @return 和为n的完全平方数的最少数量 + */ + public static int numSquares(int n) { + // dp[i] 表示和为 i 的完全平方数的最少数量 + int[] dp = new int[n + 1]; + + // 初始化 dp 数组,设置为最大值 + // dp[0] = 0(和为0需要0个完全平方数) + for (int i = 1; i <= n; i++) { + dp[i] = Integer.MAX_VALUE; + } + + // 动态规划填充 dp 数组 + for (int i = 1; i <= n; i++) { + // 尝试所有可能的完全平方数 j*j,其中 j*j <= i + for (int j = 1; j * j <= i; j++) { + // 状态转移方程: + // 和为i的最少数量 = min(和为(i-j*j)的最少数量 + 1) + dp[i] = Math.min(dp[i], dp[i - j * j] + 1); + } + } + + return dp[n]; // 返回和为 n 的最少数量 + } + + + /** + * 方法2:广度优先搜索(BFS)解法 + * + * 算法思路: + * 将问题看作图论问题,每个数字是一个节点 + * 如果两个数字相差一个完全平方数,则它们之间有边 + * 使用BFS找到从n到0的最短路径 + * + * 时间复杂度分析: + * - BFS遍历:O(n * √n) + * - 队列操作:O(1) + * - 总时间复杂度:O(n * √n) + * + * 空间复杂度分析: + * - 队列存储空间:O(n) + * - visited数组存储空间:O(n) + * + * @param n 目标整数 + * @return 和为n的完全平方数的最少数量 + */ + public int numSquaresBFS(int n) { + // 使用BFS寻找最短路径 + java.util.Queue queue = new java.util.LinkedList<>(); + boolean[] visited = new boolean[n + 1]; + + queue.offer(n); + visited[n] = true; + + int level = 0; + + while (!queue.isEmpty()) { + int size = queue.size(); + level++; + + for (int i = 0; i < size; i++) { + int current = queue.poll(); + + // 尝试减去所有可能的完全平方数 + for (int j = 1; j * j <= current; j++) { + int next = current - j * j; + + if (next == 0) { + return level; + } + + if (!visited[next]) { + queue.offer(next); + visited[next] = true; + } + } + } + } + + return level; + } + + /** + * 方法3:数学解法(四平方和定理) + * + * 算法思路: + * 根据拉格朗日四平方和定理,任何自然数都可以表示为四个整数的平方和 + * 根据勒让德三平方和定理,如果n不等于4^k(8m+7)的形式,则可以表示为三个平方数之和 + * + * 时间复杂度分析: + * - 检查完全平方数:O(√n) + * - 检查4^k(8m+7)形式:O(log n) + * - 总时间复杂度:O(√n) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param n 目标整数 + * @return 和为n的完全平方数的最少数量 + */ + public int numSquaresMath(int n) { + // 情况1:n本身就是完全平方数 + if (isPerfectSquare(n)) { + return 1; + } + + // 情况2:可以表示为两个完全平方数之和 + for (int i = 1; i * i <= n; i++) { + if (isPerfectSquare(n - i * i)) { + return 2; + } + } + + // 情况3:检查是否为4^k(8m+7)的形式 + // 如果是,则需要4个完全平方数 + int temp = n; + while (temp % 4 == 0) { + temp /= 4; + } + if (temp % 8 == 7) { + return 4; + } + + // 其他情况需要3个完全平方数 + return 3; + } + + /** + * 辅助方法:判断是否为完全平方数 + * + * 时间复杂度分析: + * - 计算平方根:O(1) + * - 验证平方:O(1) + * - 总时间复杂度:O(1) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param n 待判断的数 + * @return 如果是完全平方数返回true,否则返回false + */ + private boolean isPerfectSquare(int n) { + int sqrt = (int) Math.sqrt(n); + return sqrt * sqrt == n; + } +} diff --git a/algorithm/OrangesRotting994.java b/algorithm/OrangesRotting994.java new file mode 100644 index 0000000000000..3fb4beb216355 --- /dev/null +++ b/algorithm/OrangesRotting994.java @@ -0,0 +1,229 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Queue; +import java.util.LinkedList; + +/** + * 腐烂的橘子(LeetCode 994) + * + * 时间复杂度:O(M×N) + * - M和N是网格的行数和列数 + * - 最坏情况下需要访问每个单元格常数次 + * + * 空间复杂度:O(M×N) + * - 队列最多存储所有单元格 + */ +public class OrangesRotting994 { + + /** + * 计算腐烂所有橘子所需的最短时间 + * + * 算法思路: + * 使用多源广度优先搜索(BFS) + * 1. 首先将所有腐烂的橘子加入队列作为初始源点 + * 2. 同时进行BFS,每一轮代表一分钟 + * 3. 将新鲜橘子腐烂并加入队列 + * 4. 当队列为空时,检查是否还有新鲜橘子 + * + * @param grid 二维网格,0表示空单元格,1表示新鲜橘子,2表示腐烂橘子 + * @return 腐烂所有橘子所需的最短分钟数,如果无法腐烂所有橘子返回-1 + */ + public int orangesRotting(int[][] grid) { + // 边界条件检查:空网格 + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return -1; + } + + int rows = grid.length; + int cols = grid[0].length; + Queue queue = new LinkedList<>(); + int freshCount = 0; + + // 初始化:将所有腐烂橘子加入队列,统计新鲜橘子数量 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == 2) { + queue.offer(new int[]{i, j}); + } else if (grid[i][j] == 1) { + freshCount++; + } + } + } + + // 如果没有新鲜橘子,直接返回0(无需腐烂时间) + if (freshCount == 0) { + return 0; + } + + // 定义四个方向的偏移量:下、上、右、左 + int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int minutes = 0; + + // 多源BFS:同时从所有腐烂橘子开始腐烂过程 + while (!queue.isEmpty()) { + int size = queue.size(); + boolean rotten = false; + + // 处理当前层级的所有腐烂橘子(同一分钟内腐烂的橘子) + for (int i = 0; i < size; i++) { + int[] current = queue.poll(); + int row = current[0]; + int col = current[1]; + + // 检查四个方向的相邻单元格 + for (int[] dir : directions) { + int newRow = row + dir[0]; + int newCol = col + dir[1]; + + // 检查边界条件和是否为新鲜橘子 + if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && + grid[newRow][newCol] == 1) { + grid[newRow][newCol] = 2; // 腐烂新鲜橘子 + queue.offer(new int[]{newRow, newCol}); // 加入队列用于下一轮BFS + freshCount--; // 减少新鲜橘子计数 + rotten = true; // 标记本轮有橘子腐烂 + } + } + } + + // 只有当有新的橘子被腐烂时才增加时间 + if (rotten) { + minutes++; + } + } + + // 检查是否还有新鲜橘子 + return freshCount == 0 ? minutes : -1; + } + + /** + * 方法2:简化版BFS + * + * @param grid 二维网格 + * @return 腐烂所有橘子所需的最短分钟数 + */ + public int orangesRottingSimple(int[][] grid) { + // 边界条件检查 + if (grid == null || grid.length == 0) return 0; + + int rows = grid.length; + int cols = grid[0].length; + Queue queue = new LinkedList<>(); + int freshCount = 0; + + // 初始化:将所有腐烂橘子加入队列,统计新鲜橘子数量 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (grid[i][j] == 2) { + queue.offer(new int[]{i, j}); + } else if (grid[i][j] == 1) { + freshCount++; + } + } + } + + // 如果没有新鲜橘子,直接返回0 + if (freshCount == 0) return 0; + + int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + int time = 0; + + // BFS过程:当队列不为空且还有新鲜橘子时继续 + while (!queue.isEmpty() && freshCount > 0) { + time++; // 时间增加1分钟 + int size = queue.size(); + + // 处理当前层级的所有节点 + for (int i = 0; i < size; i++) { + int[] curr = queue.poll(); + + // 检查四个方向 + for (int[] dir : directions) { + int x = curr[0] + dir[0]; + int y = curr[1] + dir[1]; + + // 检查边界和是否为新鲜橘子 + if (x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == 1) { + grid[x][y] = 2; // 腐烂新鲜橘子 + queue.offer(new int[]{x, y}); // 将新腐烂的橘子加入队列 + freshCount--; // 减少新鲜橘子计数 + } + } + } + } + + // 如果所有新鲜橘子都被腐烂,返回时间;否则返回-1 + return freshCount == 0 ? time : -1; + } + + /** + * 辅助方法:读取用户输入的网格 + * + * @param rows 行数 + * @param cols 列数 + * @return 二维网格 + */ + public static int[][] readGrid(int rows, int cols) { + Scanner scanner = new Scanner(System.in); + int[][] grid = new int[rows][cols]; + + System.out.println("请输入网格元素(0:空, 1:新鲜橘子, 2:腐烂橘子):"); + + // 逐行读取网格元素 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + return grid; + } + + /** + * 辅助方法:打印网格 + * + * @param grid 二维网格 + */ + public static void printGrid(int[][] grid) { + // 遍历网格的每一行 + for (int[] row : grid) { + for (int cell : row) { + System.out.print(cell + " "); + } + System.out.println(); + } + } + + /** + * 主函数:处理用户输入并计算腐烂所有橘子的时间 + */ + public static void main(String[] args) { + System.out.println("腐烂的橘子"); + Scanner scanner = new Scanner(System.in); + + System.out.print("请输入网格行数和列数(用空格分隔):"); + int rows = scanner.nextInt(); + int cols = scanner.nextInt(); + + // 读取网格数据 + int[][] grid = readGrid(rows, cols); + + System.out.println("初始网格:"); + printGrid(grid); + + OrangesRotting994 solution = new OrangesRotting994(); + + // 使用第一种方法计算腐烂时间 + int result1 = solution.orangesRotting(grid); + + // 重新读取网格用于第二种方法(因为第一种方法会修改原网格) + int[][] grid2 = readGrid(rows, cols); + + // 使用第二种方法计算腐烂时间 + int result2 = solution.orangesRottingSimple(grid2); + + System.out.println("方法1结果: " + result1 + " 分钟"); + System.out.println("方法2结果: " + result2 + " 分钟"); + } +} diff --git a/algorithm/Partition131.java b/algorithm/Partition131.java new file mode 100644 index 0000000000000..6f66850fae918 --- /dev/null +++ b/algorithm/Partition131.java @@ -0,0 +1,265 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; + +/** + * 分割回文串(LeetCode 131) + * + * 时间复杂度:O(N * 2^N) + * - 最坏情况下有2^N个分割方案,每个方案需要O(N)时间验证和构造 + * + * 空间复杂度:O(N) + * - 递归调用栈深度为N + * - 需要额外的列表存储当前路径和预处理的回文信息 + */ +public class Partition131 { + + /** + * 分割字符串使得每个子串都是回文串 + * + * 算法思路: + * 使用回溯算法,通过递归和状态重置生成所有可能的分割方案 + * 1. 预处理:使用动态规划计算所有子串是否为回文 + * 2. 回溯:从起始位置开始,尝试每个可能的分割点 + * 3. 只有当前子串是回文时才继续递归 + * 4. 找到完整分割方案后添加到结果中 + * + * @param s 输入字符串 + * @return 所有可能的分割方案 + */ + public List> partition(String s) { + // 创建结果列表 + List> result = new ArrayList<>(); + // 创建路径列表 + List path = new ArrayList<>(); + + // 预处理:计算所有子串是否为回文 + boolean[][] isPalindrome = preprocess(s); + + // 开始回溯 + backtrack(s, 0, path, isPalindrome, result); + + // 返回结果 + return result; + } + + /** + * 预处理:计算所有子串是否为回文 + * + * 算法思路: + * 使用动态规划,`dp[i][j]`表示`s.substring(i,j+1)`是否为回文 + * 状态转移方程: + * `dp[i][j]` = `(s.charAt(i) == s.charAt(j)) && (j-i <= 2 || dp[i+1][j-1])` + * + * @param s 输入字符串 + * @return 二维布尔数组,`dp[i][j]`表示`s[i..j]`是否为回文 + */ + private boolean[][] preprocess(String s) { + // 获取字符串长度 + int n = s.length(); + // 创建DP表 + boolean[][] dp = new boolean[n][n]; + + // 从下到上,从左到右填充DP表 + for (int i = n - 1; i >= 0; i--) { + for (int j = i; j < n; j++) { + // 检查首尾字符是否相同 + if (s.charAt(i) == s.charAt(j)) { + // 如果长度小于等于3,或者内部子串是回文 + if (j - i <= 2 || dp[i + 1][j - 1]) { + // 标记为回文 + dp[i][j] = true; + } + } + } + } + + // 返回DP表 + return dp; + } + + /** + * 回溯辅助方法 + * + * 算法思路: + * 递归地尝试所有可能的分割点,只在子串为回文时继续递归 + * + * @param s 输入字符串 + * @param start 当前处理的起始位置 + * @param path 当前构建的分割方案 + * @param isPalindrome 预处理的回文信息 + * @param result 存储所有分割方案的结果列表 + */ + private void backtrack(String s, int start, List path, + boolean[][] isPalindrome, List> result) { + // 递归终止条件:已处理完整个字符串 + if (start == s.length()) { + // 将当前路径添加到结果中(需要创建新列表避免引用问题) + result.add(new ArrayList<>(path)); + // 返回 + return; + } + + // 尝试每个可能的分割点 + for (int i = start; i < s.length(); i++) { + // 只有当前子串是回文时才继续 + if (isPalindrome[start][i]) { + // 做选择:将当前子串添加到路径中 + path.add(s.substring(start, i + 1)); + + // 递归:处理剩余部分 + backtrack(s, i + 1, path, isPalindrome, result); + + // 撤销选择:回溯,移除当前子串 + path.remove(path.size() - 1); + } + } + } + + /** + * 方法2:不预处理,直接判断回文 + * + * 算法思路: + * 不预先计算回文信息,而是在回溯过程中直接判断子串是否为回文 + * + * @param s 输入字符串 + * @return 所有可能的分割方案 + */ + public List> partitionNoPreprocess(String s) { + // 创建结果列表 + List> result = new ArrayList<>(); + // 创建路径列表 + List path = new ArrayList<>(); + // 调用不预处理的回溯方法 + backtrackNoPreprocess(s, 0, path, result); + // 返回结果 + return result; + } + + /** + * 不预处理的回溯辅助方法 + * + * 算法思路: + * 在回溯过程中直接判断子串是否为回文 + * + * @param s 输入字符串 + * @param start 当前处理的起始位置 + * @param path 当前构建的分割方案 + * @param result 存储所有分割方案的结果列表 + */ + private void backtrackNoPreprocess(String s, int start, List path, + List> result) { + // 检查是否已处理完整个字符串 + if (start == s.length()) { + // 添加路径副本到结果列表 + result.add(new ArrayList<>(path)); + // 返回 + return; + } + + // 遍历所有可能的分割点 + for (int i = start; i < s.length(); i++) { + // 直接判断子串是否为回文 + if (isPalindrome(s, start, i)) { + // 添加子串到路径 + path.add(s.substring(start, i + 1)); + // 递归处理剩余部分 + backtrackNoPreprocess(s, i + 1, path, result); + // 移除路径中的最后一个元素 + path.remove(path.size() - 1); + } + } + } + + /** + * 判断子串是否为回文 + * + * 算法思路: + * 使用双指针法判断子串是否为回文 + * + * @param s 字符串 + * @param left 左边界 + * @param right 右边界 + * @return 是否为回文 + */ + private boolean isPalindrome(String s, int left, int right) { + // 当左指针小于右指针时循环 + while (left < right) { + // 比较左右字符 + if (s.charAt(left) != s.charAt(right)) { + // 返回false + return false; + } + // 左指针右移 + left++; + // 右指针左移 + right--; + } + // 返回true + return true; + } + + /** + * 辅助方法:读取用户输入的字符串 + * + * @return 用户输入的字符串 + */ + public static String readString() { + // 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // 打印提示信息 + System.out.print("请输入字符串: "); + // 读取并返回字符串 + return scanner.nextLine(); + } + + /** + * 辅助方法:打印分割方案 + * + * @param result 分割方案列表 + */ + public static void printPartitions(List> result) { + // 打印标题 + System.out.println("所有分割方案:"); + // 遍历所有分割方案 + for (int i = 0; i < result.size(); i++) { + // 打印分割方案 + System.out.println((i + 1) + ": " + result.get(i)); + } + // 打印总计数量 + System.out.println("总共 " + result.size() + " 个方案"); + } + + /** + * 主函数:处理用户输入并生成所有回文分割方案 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("分割回文串"); + + // 读取用户输入的字符串 + String s = readString(); + // 打印输入字符串 + System.out.println("输入字符串: \"" + s + "\""); + + // 生成所有分割方案 + Partition131 solution = new Partition131(); + // 调用partition方法 + List> result1 = solution.partition(s); + // 调用partitionNoPreprocess方法 + List> result2 = solution.partitionNoPreprocess(s); + + // 输出结果 + // 打印预处理方法结果标题 + System.out.println("预处理方法结果:"); + // 调用printPartitions方法打印结果 + printPartitions(result1); + + // 打印直接判断方法结果标题 + System.out.println("\n直接判断方法结果:"); + // 调用printPartitions方法打印结果 + printPartitions(result2); + } +} diff --git a/algorithm/Partition763.java b/algorithm/Partition763.java new file mode 100644 index 0000000000000..adeb01dad73fe --- /dev/null +++ b/algorithm/Partition763.java @@ -0,0 +1,232 @@ +package com.funian.algorithm.algorithm; + +/** + * 划分字母区间(LeetCode 763) + * + * 时间复杂度:O(n) + * - n是字符串长度 + * - 需要遍历字符串两次 + * + * 空间复杂度:O(1) + * - 只使用了固定大小的数组(26个字母) + * - 结果列表的空间复杂度为O(k),k为片段数量 + */ +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class Partition763 { + + /** + * 主函数:处理用户输入并计算划分字母区间的长度 + * + * 算法流程: + * 1. 读取用户输入的字符串 + * 2. 调用 [partitionLabels](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/Partition763.java#L93-L129)方法计算每个片段的长度 + * 3. 输出结果 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入字符串:"); + String s = scanner.nextLine(); + + List result = partitionLabels(s); + System.out.println("每个字符串片段的长度:" + result); + } + + /** + * 将字符串划分为尽可能多的片段,同一字母最多出现在一个片段中 + * + * 算法思路: + * 1. 首先遍历字符串,记录每个字符最后出现的位置 + * 2. 再次遍历字符串,维护当前片段的结束位置 + * 3. 对于每个字符,更新当前片段的结束位置为max(当前结束位置, 该字符最后出现位置) + * 4. 当遍历到当前片段结束位置时,说明找到了一个完整的片段 + * + * 执行过程分析(以`s="ababcbacadefegdehijhklij"`为例): + * + * 第一步:记录每个字符最后出现的位置 + * a:8, b:5, c:7, d:14, e:15, f:11, g:13, h:19, i:22, j:23, k:20, l:21 + * + * 第二步:确定片段边界 + * + * 片段1的确定过程: + * i=0, char='a': currentEnd = max(0, 8) = 8 + * i=1, char='b': currentEnd = max(8, 5) = 8 + * i=2, char='a': currentEnd = max(8, 8) = 8 + * i=3, char='b': currentEnd = max(8, 5) = 8 + * i=4, char='c': currentEnd = max(8, 7) = 8 + * i=5, char='b': currentEnd = max(8, 5) = 8 + * i=6, char='a': currentEnd = max(8, 8) = 8 + * i=7, char='c': currentEnd = max(8, 7) = 8 + * i=8, char='a': currentEnd = max(8, 8) = 8 + * i=8 == currentEnd=8,找到第一个片段[0,8],长度=9 + * + * 片段2的确定过程: + * partitionStart = 9 + * i=9, char='d': currentEnd = max(8, 14) = 14 + * i=10, char='e': currentEnd = max(14, 15) = 15 + * i=11, char='f': currentEnd = max(15, 11) = 15 + * i=12, char='e': currentEnd = max(15, 15) = 15 + * i=13, char='g': currentEnd = max(15, 13) = 15 + * i=14, char='d': currentEnd = max(15, 14) = 15 + * i=15, char='e': currentEnd = max(15, 15) = 15 + * i=15 == currentEnd=15,找到第二个片段[9,15],长度=7 + * + * 片段3的确定过程: + * partitionStart = 16 + * i=16, char='h': currentEnd = max(15, 19) = 19 + * i=17, char='i': currentEnd = max(19, 22) = 22 + * i=18, char='j': currentEnd = max(22, 23) = 23 + * i=19, char='h': currentEnd = max(23, 19) = 23 + * i=20, char='k': currentEnd = max(23, 20) = 23 + * i=21, char='l': currentEnd = max(23, 21) = 23 + * i=22, char='i': currentEnd = max(23, 22) = 23 + * i=23, char='j': currentEnd = max(23, 23) = 23 + * i=23 == currentEnd=23,找到第三个片段[16,23],长度=8 + * + * 最终结果:[9, 7, 8] + * 对应片段:"ababcbaca" | "defegde" | "hijhklij" + * + * 时间复杂度分析: + * - 记录字符最后位置:O(n) + * - 确定片段边界:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - lastIndex数组:O(1)(固定26个字母) + * - result列表:O(k)(k为片段数量) + * - 总空间复杂度:O(1) + * + * @param s 输入字符串 + * @return 每个字符串片段的长度列表 + */ + public static List partitionLabels(String s) { + // 存储每个字符最后出现的位置 + // 数组大小为26,对应26个小写字母 + int[] lastIndex = new int[26]; + List result = new ArrayList<>(); + int n = s.length(); + + // 记录每个字符最后出现的位置 + // 遍历字符串,更新每个字符的最后位置 + for (int i = 0; i < n; i++) { + lastIndex[s.charAt(i) - 'a'] = i; + } + + // 当前片段的结束位置 + int currentEnd = 0; + // 当前片段的开始位置 + int partitionStart = 0; + + // 遍历字符串,确定片段的边界 + for (int i = 0; i < n; i++) { + // 更新当前片段的结束位置为当前字符最后出现位置和当前结束位置的最大值 + // 这确保了当前片段包含了所有需要的字符 + currentEnd = Math.max(currentEnd, lastIndex[s.charAt(i) - 'a']); + + // 如果当前索引到达片段的结束位置 + // 说明当前片段已经包含了所有需要的字符,可以划分 + if (i == currentEnd) { + // 计算片段长度并添加到结果列表 + result.add(currentEnd - partitionStart + 1); + // 更新下一个片段的开始位置 + partitionStart = i + 1; + } + } + + return result; + } + + /** + * 扩展方法:返回实际的字符串片段而不仅仅是长度 + * + * 算法思路: + * 与 [partitionLabels](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/Partition763.java#L93-L129)方法类似,但返回实际的字符串片段而不是长度 + * + * 时间复杂度分析: + * - 记录字符最后位置:O(n) + * - 确定片段边界:O(n) + * - substring操作:O(k*m)(k为片段数,m为平均片段长度) + * - 总时间复杂度:O(n + k*m) + * + * 空间复杂度分析: + * - lastIndex数组:O(1)(固定26个字母) + * - result列表:O(n)(存储所有片段) + * - 总空间复杂度:O(n) + * + * @param s 输入字符串 + * @return 字符串片段列表 + */ + public List partitionLabelsStrings(String s) { + int[] lastIndex = new int[26]; + List result = new ArrayList<>(); + + // 记录每个字符最后出现的位置 + for (int i = 0; i < s.length(); i++) { + lastIndex[s.charAt(i) - 'a'] = i; + } + + int currentEnd = 0; + int partitionStart = 0; + + for (int i = 0; i < s.length(); i++) { + currentEnd = Math.max(currentEnd, lastIndex[s.charAt(i) - 'a']); + + if (i == currentEnd) { + // 添加实际的字符串片段 + result.add(s.substring(partitionStart, currentEnd + 1)); + partitionStart = i + 1; + } + } + + return result; + } + + /** + * 扩展方法:验证划分结果的正确性 + * + * 算法思路: + * 1. 检查重新拼接的片段是否等于原字符串 + * 2. 检查每个片段中的字符是否不会出现在其他片段中 + * + * 时间复杂度分析: + * - 重新拼接字符串:O(n) + * - 验证字符分布:O(k²*m)(k为片段数,m为平均片段长度) + * - 总时间复杂度:O(n + k²*m) + * + * 空间复杂度分析: + * - StringBuilder:O(n) + * - 总空间复杂度:O(n) + * + * @param s 原始字符串 + * @param partitions 字符串片段列表 + * @return 如果划分正确返回true,否则返回false + */ + public boolean validatePartitions(String s, List partitions) { + // 重新拼接片段,应该等于原字符串 + StringBuilder sb = new StringBuilder(); + for (String partition : partitions) { + sb.append(partition); + } + + if (!sb.toString().equals(s)) { + return false; + } + + // 检查每个片段中的字符是否不会出现在其他片段中 + for (int i = 0; i < partitions.size(); i++) { + String currentPartition = partitions.get(i); + for (char c : currentPartition.toCharArray()) { + // 检查该字符是否出现在其他片段中 + for (int j = 0; j < partitions.size(); j++) { + if (i != j && partitions.get(j).indexOf(c) != -1) { + return false; + } + } + } + } + + return true; + } +} diff --git a/algorithm/PathSum437.java b/algorithm/PathSum437.java new file mode 100644 index 0000000000000..64480d8f25ebe --- /dev/null +++ b/algorithm/PathSum437.java @@ -0,0 +1,330 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 路径总和III(LeetCode 437) + * + * 时间复杂度:O(n²) + * - n是二叉树中的节点数 + * - 对于每个节点,都需要向下遍历其子树 + * + * 空间复杂度:O(h) + * - h是二叉树的高度 + * - 递归调用栈的深度 + */ +public class PathSum437 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + /** + * 计算路径总和等于目标值的路径数量 + * + * 算法思路: + * 对于每个节点,我们计算以该节点为起点的路径中和为targetSum的路径数量 + * 然后递归地对所有节点执行此操作 + * + * 执行过程分析(以二叉树 [10,5,-3,3,2,null,11,3,-2,null,1],targetSum=8 为例): + * + * 10 + * / \ + * 5 -3 + * / \ \ + * 3 2 11 + * / \ \ + * 3 -2 1 + * + * 满足条件的路径: + * 1. 5 -> 3 (和为8) + * 2. 5 -> 2 -> 1 (和为8) + * 3. -3 -> 11 (和为8) + * + * 总共3条路径 + * + * 时间复杂度分析: + * - 对每个节点调用 [pathSumFrom](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/PathSum437.java#L101-L124) 方法:O(n) + * - 每次 [pathSumFrom](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/PathSum437.java#L101-L124) 调用需要遍历子树:O(n) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),h为树的高度 + * + * @param root 二叉树的根节点 + * @param targetSum 目标和 + * @return 路径数量 + */ + public int pathSum(TreeNode root, int targetSum) { + // 如果根节点为空,返回0 + if (root == null) { + return 0; + } + + // 计算以当前节点为起点的路径数量 + 递归计算左右子树的路径数量 + return pathSumFrom(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum); + } + + /** + * 计算以指定节点为起点的路径中和为targetSum的路径数量 + * + * 算法思路: + * 从当前节点开始向下遍历,累加路径和,如果等于目标和则计数加1 + * + * 时间复杂度分析: + * - 遍历以node为根的子树:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @param targetSum 剩余目标和 + * @return 路径数量 + */ + private int pathSumFrom(TreeNode node, long targetSum) { + // 如果节点为空,返回0 + if (node == null) { + return 0; + } + + // 检查当前节点值是否等于剩余目标和 + // 如果等于,则找到一条路径,计数为1,否则为0 + int count = (node.val == targetSum) ? 1 : 0; + + // 递归计算左右子树中满足条件的路径数量 + return count + pathSumFrom(node.left, targetSum - node.val) + pathSumFrom(node.right, targetSum - node.val); + } + + /** + * 优化解法:使用前缀和 + * + * 算法思路: + * 使用HashMap记录从根节点到当前节点路径上的前缀和及其出现次数 + * 对于当前节点,检查是否存在前缀和等于当前前缀和减去目标和的节点 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * - HashMap操作平均时间复杂度:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashMap存储前缀和:O(n) + * - 递归调用栈:O(h) + * - 总空间复杂度:O(n) + * + * @param root 二叉树的根节点 + * @param targetSum 目标和 + * @return 路径数量 + */ + public int pathSumOptimized(TreeNode root, int targetSum) { + // 创建HashMap存储前缀和及其出现次数 + Map prefixSumMap = new HashMap<>(); + // 初始化前缀和为0的出现次数为1 + prefixSumMap.put(0L, 1); + + // 调用递归辅助方法 + return pathSumHelper(root, 0, targetSum, prefixSumMap); + } + + /** + * 使用前缀和的递归辅助方法 + * + * 算法思路: + * 1. 计算当前节点的前缀和 + * 2. 检查是否存在前缀和等于当前前缀和减去目标和的节点 + * 3. 更新前缀和映射 + * 4. 递归处理左右子树 + * 5. 回溯时恢复前缀和映射 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * - HashMap操作平均时间复杂度:O(1) + * + * 空间复杂度分析: + * - 递归调用栈:O(h) + * + * @param node 当前节点 + * @param currentSum 当前前缀和 + * @param targetSum 目标和 + * @param prefixSumMap 前缀和映射 + * @return 路径数量 + */ + private int pathSumHelper(TreeNode node, long currentSum, int targetSum, Map prefixSumMap) { + // 如果节点为空,返回0 + if (node == null) { + return 0; + } + + // 计算当前节点的前缀和 + currentSum += node.val; + + // 检查是否存在前缀和等于currentSum - targetSum的节点 + int count = prefixSumMap.getOrDefault(currentSum - targetSum, 0); + + // 更新前缀和映射 + prefixSumMap.put(currentSum, prefixSumMap.getOrDefault(currentSum, 0) + 1); + + // 递归处理左右子树 + count += pathSumHelper(node.left, currentSum, targetSum, prefixSumMap); + count += pathSumHelper(node.right, currentSum, targetSum, prefixSumMap); + + // 回溯时恢复前缀和映射 + prefixSumMap.put(currentSum, prefixSumMap.get(currentSum) - 1); + + return count; + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // while (!queue.isEmpty()) 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并计算路径总和 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 提示用户输入目标和 + * 4. 调用两种方法计算路径数量 + * 5. 打印计算结果 + */ + public static void main(String[] args) { + System.out.println("路径总和III"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空"); + return; + } + + // 打印创建的二叉树 + System.out.println("创建的二叉树:"); + printLevelOrder(root); + + // 输入目标和 + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入目标和 + System.out.print("请输入目标和: "); + // 读取目标和 + int targetSum = scanner.nextInt(); + + // 计算路径数量 + // 创建解决方案实例 + PathSum437 solution = new PathSum437(); + int result1 = solution.pathSum(root, targetSum); + int result2 = solution.pathSumOptimized(root, targetSum); + + // 打印结果 + System.out.println("基础方法计算的路径数量: " + result1); + System.out.println("优化方法计算的路径数量: " + result2); + } +} diff --git a/algorithm/Permutation46.java b/algorithm/Permutation46.java new file mode 100644 index 0000000000000..e38f46b8a6bec --- /dev/null +++ b/algorithm/Permutation46.java @@ -0,0 +1,284 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * 全排列(LeetCode 46) + * + * 时间复杂度:O(n! × n) + * - 共有n!个排列,每个排列需要O(n)时间构造 + * + * 空间复杂度:O(n) + * - 递归调用栈深度为n + * - 需要额外的数组存储当前路径和使用状态 + */ +public class Permutation46 { + + /** + * 生成数组的所有全排列 + * + * 算法思路: + * 使用回溯算法,通过递归和状态重置生成所有可能的排列 + * 1. 使用一个布尔数组记录每个元素是否已被使用 + * 2. 递归构建排列,每次选择一个未使用的元素 + * 3. 当排列长度等于原数组长度时,得到一个完整排列 + * 4. 回溯时重置状态,尝试其他可能 + * + * 执行过程分析(以nums=[1,2,3]为例): + * + * 递归树: + * [] + * / | \ + * [1] [2] [3] + * / \ / \ / \ + * [1,2] [1,3][2,1][2,3][3,1][3,2] + * | | | | | | + * [1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1] + * + * 详细执行过程: + * + * 第一条路径:[1] -> [1,2] -> [1,2,3] + * 1. choose(1): used=[T,F,F], path=[1] + * 2. choose(2): used=[T,T,F], path=[1,2] + * 3. choose(3): used=[T,T,T], path=[1,2,3] + * 4. path.size() == nums.length,添加[1,2,3]到结果 + * 5. backtrack: remove(3), used=[T,T,F] + * 6. backtrack: remove(2), used=[T,F,F] + * 7. choose(3): used=[T,F,T], path=[1,3] + * 8. choose(2): used=[T,T,T], path=[1,3,2] + * 9. path.size() == nums.length,添加[1,3,2]到结果 + * ... + * + * 最终结果:[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] + * + * 时间复杂度分析: + * - 递归深度为n,每层需要O(n)时间遍历所有元素 + * - 总共有n!个排列 + * - 总时间复杂度:O(n! × n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * - path列表存储:O(n) + * - used数组存储:O(n) + * - result列表存储所有排列:O(n! × n) + * - 总空间复杂度:O(n! × n) + * + * @param nums 不含重复数字的数组 + * @return 所有可能的全排列 + */ + public List> permute(int[] nums) { + List> result = new ArrayList<>(); + List path = new ArrayList<>(); + boolean[] used = new boolean[nums.length]; + + // 开始回溯 + backtrack(nums, path, used, result); + + return result; + } + + /** + * 回溯辅助方法 + * + * 算法思路: + * 1. 当路径长度等于数组长度时,说明得到一个完整排列 + * 2. 否则遍历所有元素,选择未使用的元素加入路径 + * 3. 递归处理剩余位置 + * 4. 回溯时撤销选择 + * + * 时间复杂度分析: + * - 递归深度为n + * - 每层需要O(n)时间遍历所有元素 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * + * @param nums 原数组 + * @param path 当前构建的排列 + * @param used 元素使用状态数组 + * @param result 存储所有排列的结果列表 + */ + private void backtrack(int[] nums, List path, boolean[] used, List> result) { + // 递归终止条件:当前路径长度等于数组长度 + if (path.size() == nums.length) { + // 将当前路径添加到结果中(需要创建新列表避免引用问题) + result.add(new ArrayList<>(path)); + return; + } + + // 尝试每个未使用的元素 + for (int i = 0; i < nums.length; i++) { + // 如果元素已被使用,跳过 + if (used[i]) { + continue; + } + + // 做选择:将元素添加到当前路径 + path.add(nums[i]); + used[i] = true; + + // 递归:继续构建排列 + backtrack(nums, path, used, result); + + // 撤销选择:回溯,移除元素并重置状态 + path.remove(path.size() - 1); + used[i] = false; + } + } + + /** + * 方法2:交换元素的回溯法 + * + * 算法思路: + * 通过交换数组元素的位置生成排列 + * 1. 固定前缀,递归处理后缀 + * 2. 通过交换生成不同的排列 + * + * 时间复杂度分析: + * - 与 [permute](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/Permutation46.java#L57-L67) 方法相同:O(n! × n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * - list列表存储:O(n) + * - result列表存储所有排列:O(n! × n) + * - 总空间复杂度:O(n! × n) + * + * @param nums 不含重复数字的数组 + * @return 所有可能的全排列 + */ + public List> permuteSwap(int[] nums) { + List> result = new ArrayList<>(); + List list = new ArrayList<>(); + for (int num : nums) { + list.add(num); + } + backtrackSwap(list, 0, result); + return result; + } + + /** + * 交换回溯辅助方法 + * + * 算法思路: + * 1. 当start等于列表长度时,得到一个完整排列 + * 2. 否则从start位置开始,与后续每个位置交换 + * 3. 递归处理start+1位置 + * 4. 回溯时交换回来恢复状态 + * + * 时间复杂度分析: + * - 递归深度为n + * - 每层需要O(n)时间遍历所有元素 + * - 总时间复杂度:O(n! × n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * + * @param list 当前排列列表 + * @param start 当前处理的起始位置 + * @param result 存储所有排列的结果列表 + */ + private void backtrackSwap(List list, int start, List> result) { + // 递归终止条件 + if (start == list.size()) { + result.add(new ArrayList<>(list)); + return; + } + + // 尝试每个位置的元素 + for (int i = start; i < list.size(); i++) { + // 交换元素 + swap(list, start, i); + + // 递归处理下一个位置 + backtrackSwap(list, start + 1, result); + + // 回溯:交换回来 + swap(list, start, i); + } + } + + /** + * 交换列表中两个元素 + * + * 时间复杂度分析: + * - 常数时间操作:O(1) + * + * @param list 列表 + * @param i 第一个元素索引 + * @param j 第二个元素索引 + */ + private void swap(List list, int i, int j) { + int temp = list.get(i); + list.set(i, list.get(j)); + list.set(j, temp); + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 处理输入字符串:O(m),m为输入字符数 + * - 转换为整数:O(n),n为元素个数 + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入不重复的整数数组(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 辅助方法:打印排列结果 + * + * 时间复杂度分析: + * - 遍历所有排列:O(n! × n) + * + * @param result 排列结果 + */ + public static void printPermutations(List> result) { + System.out.println("所有排列:"); + for (int i = 0; i < result.size(); i++) { + System.out.println((i + 1) + ": " + result.get(i)); + } + System.out.println("总共 " + result.size() + " 个排列"); + } + + /** + * 主函数:处理用户输入并生成全排列 + */ + public static void main(String[] args) { + System.out.println("全排列"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("输入数组: " + Arrays.toString(nums)); + + // 生成全排列 + Permutation46 solution = new Permutation46(); + List> result1 = solution.permute(nums); + List> result2 = solution.permuteSwap(nums); + + // 输出结果 + System.out.println("方法1结果:"); + printPermutations(result1); + + System.out.println("\n方法2结果:"); + printPermutations(result2); + } +} diff --git a/algorithm/PermuteNumber46.java b/algorithm/PermuteNumber46.java new file mode 100644 index 0000000000000..432b0aa31ca02 --- /dev/null +++ b/algorithm/PermuteNumber46.java @@ -0,0 +1,129 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 全排列生成器 + * + * 时间复杂度:O(n! × n) + * - n个不同元素的全排列总数为n! + * - 生成每个排列需要n次操作(添加元素) + * - 回溯过程中每个排列需要进行n次递归调用 + * - 总时间复杂度为 O(n! × n) + * + * 空间复杂度:O(n! × n) + * - 结果存储需要 O(n! × n) 空间(n!个排列,每个排列n个元素) + * - 递归调用栈深度为 O(n) + * - visited数组需要 O(n) 空间 + * - tempList临时列表需要 O(n) 空间 + * - 总空间复杂度主要由结果存储决定,为 O(n! × n) + */ +public class PermuteNumber46 { + /** + * 主函数:处理用户输入并输出全排列结果 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用permute方法生成全排列 + * 3. 输出所有可能的排列 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于输入 + Scanner scanner = new Scanner(System.in); + System.out.print("请输入不含重复数字的数组元素,以空格分隔:"); + + // 读取一行输入并按空格分隔,生成字符串数组 + String line = scanner.nextLine(); + String[] str = line.split(" "); + int[] nums = new int[str.length]; + + // 将输入的字符串数组转换为整数数组 + for (int i = 0; i < str.length; i++) { + nums[i] = Integer.parseInt(str[i]); + } + + // 调用 permute 方法生成全排列 + List> result = permute(nums); + + // 输出所有可能的全排列 + System.out.println("所有可能的全排列为:"); + for (List permutation : result) { + System.out.println(permutation); + } + } + + /** + * 生成给定整数数组的所有可能全排列 + * + * 算法思路: + * 使用回溯算法生成全排列。通过递归和回溯的方式,尝试所有可能的元素组合。 + * 对于n个元素的数组,共有n!种不同的排列方式。 + * + * @param nums 输入的整数数组 + * @return 包含所有全排列的列表 + */ + public static List> permute(int[] nums) { + List> result = new ArrayList<>(); // 用于存储所有排列结果 + boolean[] visited = new boolean[nums.length]; // 标记数组元素是否已被访问 + backtrack(result, new ArrayList<>(), nums, visited); // 调用回溯方法构建排列 + return result; // 返回结果 + } + + /** + * 回溯算法核心函数,用于递归构建所有排列 + * + * 算法执行过程分析(以输入 [1,2,3] 为例): + * + * 决策树结构: + * [] + * / | \ + * [1] [2] [3] + * / \ / \ / \ + * [1,2] [1,3][2,1][2,3][3,1][3,2] + * / | | | | \ + * [1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1] + * + * 执行步骤详解: + * 1. 初始调用: backtrack(result, [], [1,2,3], [false,false,false]) + * 2. 第一层选择: + * - 选择1: tempList=[1], visited=[true,false,false] + * - 选择2: tempList=[2], visited=[false,true,false] + * - 选择3: tempList=[3], visited=[false,false,true] + * 3. 以选择1为例继续: + * - backtrack(result, [1], [1,2,3], [true,false,false]) + * - 第二层可选: 2或3 + * - 选择2: tempList=[1,2], visited=[true,true,false] + * - 选择3: tempList=[1,3], visited=[true,false,true] + * 4. 继续深入直到形成完整排列: + * - [1,2,3] 和 [1,3,2] 被添加到结果中 + * - 然后通过回溯尝试其他起始数字 + * 5. 最终结果: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] + * + * @param result 最终的排列结果列表 + * @param tempList 临时存储当前排列的列表 + * @param nums 输入的整数数组 + * @param visited 标记数组中的元素是否已被使用 + */ + private static void backtrack(List> result, List tempList, int[] nums, boolean[] visited) { + // 基础情况:如果临时列表的大小等于输入数组的大小,表示找到一个完整的排列 + if (tempList.size() == nums.length) { + result.add(new ArrayList<>(tempList)); // 将临时列表添加到结果中(注意:需要深拷贝) + } else { + // 遍历输入数组中的每个元素 + for (int i = 0; i < nums.length; i++) { + if (visited[i]) continue; // 如果当前数字已被访问,跳过 + + // 做选择阶段 + visited[i] = true; // 标记当前数字为已访问 + tempList.add(nums[i]); // 将当前数字添加到临时列表 + + // 递归调用,继续构建排列 + backtrack(result, tempList, nums, visited); + + // 撤销选择,回溯:移除最后添加的数字 + tempList.remove(tempList.size() - 1); + visited[i] = false; // 恢复当前数字为未访问,准备尝试下一个排列 + } + } + } +} diff --git a/algorithm/ProductExceptSelf238.java b/algorithm/ProductExceptSelf238.java new file mode 100644 index 0000000000000..c1842f4aadc4c --- /dev/null +++ b/algorithm/ProductExceptSelf238.java @@ -0,0 +1,260 @@ +import java.util.Scanner; + +/** + * 除自身以外数组的乘积(LeetCode 238) + * + * 时间复杂度:O(n) + * - 需要遍历数组两次 + * - 第一次遍历计算前缀积:O(n) + * - 第二次遍历计算后缀积并更新结果:O(n) + * - 总时间复杂度:O(n) + O(n) = O(n) + * + * 空间复杂度:O(1) + * - 不考虑输出数组,只使用了常数级别的额外空间 + * - 后缀积使用一个变量存储,空间复杂度为O(1) + */ +public class ProductExceptSelf238 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 读取输入的数组 + System.out.print("请输入数组元素,以空格分隔:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] str = line.split(" "); + + // 获取数组长度 + int n = str.length; + + // 创建整型数组 + int[] nums = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(str[i]); + } + + // 调用 productExceptSelf 方法计算结果 + int[] result = productExceptSelf(nums); + + // 输出结果 + System.out.print("结果数组为:"); + for (int num : result) { + System.out.print(num + " "); + } + System.out.println(); // 换行 + } + + /** + * 计算除自身以外数组的乘积 + * 对于数组中每个元素,计算除该元素外其余所有元素的乘积 + * + * 算法思路: + * 1. 对于每个位置 i,结果等于该位置左边所有元素的乘积乘以右边所有元素的乘积 + * 2. 分两步计算: + * - 第一步:计算每个位置的前缀积(左边所有元素的乘积) + * - 第二步:从右向左遍历,计算后缀积并乘以前缀积得到最终结果 + * + * 示例过程(以数组 [1,2,3,4] 为例): + * 数组: [1, 2, 3, 4] + * 索引: 0 1 2 3 + * + * 步骤1 - 计算前缀积: + * answer[0] = 1 (左边无元素) + * answer[1] = 1 * 1 = 1 (左边元素: 1) + * answer[2] = 1 * 2 = 2 (左边元素: 1,2) + * answer[3] = 2 * 3 = 6 (左边元素: 1,2,3) + * 前缀积数组: [1, 1, 2, 6] + * + * 步骤2 - 计算后缀积并更新结果: + * i=3: answer[3] = 6 * 1 = 6, suffixProduct = 1 * 4 = 4 + * i=2: answer[2] = 2 * 4 = 8, suffixProduct = 4 * 3 = 12 + * i=1: answer[1] = 1 * 12 = 12, suffixProduct = 12 * 2 = 24 + * i=0: answer[0] = 1 * 24 = 24, suffixProduct = 24 * 1 = 24 + * + * 最终结果: [24, 12, 8, 6] + * 验证: [2*3*4, 1*3*4, 1*2*4, 1*2*3] = [24, 12, 8, 6] + * + * 时间复杂度分析: + * - 计算前缀积:O(n),其中n为输入数组`nums`的长度 + * - 计算后缀积并更新结果:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 结果数组:O(n) + * - 后缀积变量:O(1) + * - 总空间复杂度:O(1)(不考虑输出数组) + * + * @param nums 输入的整数数组 + * @return 除自身以外数组的乘积数组 + */ + public static int[] productExceptSelf(int[] nums) { + // 获取数组长度 + int n = nums.length; + + // 创建结果数组 + int[] answer = new int[n]; + + // 1. 计算前缀积 + answer[0] = 1; // 第一个元素的左边没有元素,前缀积为 1 + // 从第二个元素开始,计算每个位置的前缀积 + for (int i = 1; i < n; i++) { + // 当前元素的前缀积等于前一个位置的前缀积乘以前一个元素 + answer[i] = answer[i - 1] * nums[i - 1]; + } + + // 2. 计算后缀积并更新结果 + int suffixProduct = 1; // 初始化后缀积为1 + // 从右向左遍历数组 + for (int i = n - 1; i >= 0; i--) { + // 当前结果等于前缀积乘以后缀积 + answer[i] = answer[i] * suffixProduct; + // 更新后缀积(为下一个位置准备) + suffixProduct *= nums[i]; + } + + // 返回结果数组 + return answer; + } + + + /** + * 方法2:使用两个数组分别存储前缀积和后缀积 + * + * 算法思路: + * 1. 创建前缀积数组和后缀积数组 + * 2. 分别计算前缀积和后缀积 + * 3. 结果为对应位置前缀积与后缀积的乘积 + * + * 示例过程(以数组 [1,2,3,4] 为例): + * + * 1. 计算前缀积数组: + * prefix[0] = 1 + * prefix[1] = 1 * 1 = 1 + * prefix[2] = 1 * 2 = 2 + * prefix[3] = 2 * 3 = 6 + * prefix = [1, 1, 2, 6] + * + * 2. 计算后缀积数组: + * suffix[3] = 1 + * suffix[2] = 1 * 4 = 4 + * suffix[1] = 4 * 3 = 12 + * suffix[0] = 12 * 2 = 24 + * suffix = [24, 12, 4, 1] + * + * 3. 计算结果数组: + * answer[0] = prefix[0] * suffix[0] = 1 * 24 = 24 + * answer[1] = prefix[1] * suffix[1] = 1 * 12 = 12 + * answer[2] = prefix[2] * suffix[2] = 2 * 4 = 8 + * answer[3] = prefix[3] * suffix[3] = 6 * 1 = 6 + * answer = [24, 12, 8, 6] + * + * 时间复杂度分析: + * - 计算前缀积数组:O(n),其中n为输入数组`nums`的长度 + * - 计算后缀积数组:O(n) + * - 计算结果数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 前缀积数组:O(n) + * - 后缀积数组:O(n) + * - 结果数组:O(n) + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @return 除自身以外数组的乘积数组 + */ + public static int[] productExceptSelfTwoArrays(int[] nums) { + int n = nums.length; + int[] prefix = new int[n]; + int[] suffix = new int[n]; + int[] answer = new int[n]; + + // 计算前缀积数组 + prefix[0] = 1; + for (int i = 1; i < n; i++) { + prefix[i] = prefix[i - 1] * nums[i - 1]; + } + + // 计算后缀积数组 + suffix[n - 1] = 1; + for (int i = n - 2; i >= 0; i--) { + suffix[i] = suffix[i + 1] * nums[i + 1]; + } + + // 计算结果数组 + for (int i = 0; i < n; i++) { + answer[i] = prefix[i] * suffix[i]; + } + + return answer; + } + + /** + * 方法3:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 对于每个元素,遍历整个数组计算除该元素外所有元素的乘积 + * + * 示例过程(以数组 [1,2,3,4] 为例): + * + * i=0: product = 1 + * j=1: product = 1 * 2 = 2 + * j=2: product = 2 * 3 = 6 + * j=3: product = 6 * 4 = 24 + * answer[0] = 24 + * + * i=1: product = 1 + * j=0: product = 1 * 1 = 1 + * j=2: product = 1 * 3 = 3 + * j=3: product = 3 * 4 = 12 + * answer[1] = 12 + * + * i=2: product = 1 + * j=0: product = 1 * 1 = 1 + * j=1: product = 1 * 2 = 2 + * j=3: product = 2 * 4 = 8 + * answer[2] = 8 + * + * i=3: product = 1 + * j=0: product = 1 * 1 = 1 + * j=1: product = 1 * 2 = 2 + * j=2: product = 2 * 3 = 6 + * answer[3] = 6 + * + * 最终结果: [24, 12, 8, 6] + * + * 时间复杂度分析: + * - 外层循环:O(n),其中n为输入数组`nums`的长度 + * - 内层循环:O(n) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 结果数组:O(n) + * - 临时变量product:O(1) + * - 总空间复杂度:O(1)(不考虑输出数组) + * + * @param nums 输入的整数数组 + * @return 除自身以外数组的乘积数组 + */ + public static int[] productExceptSelfBruteForce(int[] nums) { + int n = nums.length; + int[] answer = new int[n]; + + for (int i = 0; i < n; i++) { + int product = 1; + for (int j = 0; j < n; j++) { + if (i != j) { + product *= nums[j]; + } + } + answer[i] = product; + } + + return answer; + } +} diff --git a/algorithm/RemoveElementFromList19.java b/algorithm/RemoveElementFromList19.java new file mode 100644 index 0000000000000..f76dfb03a00ce --- /dev/null +++ b/algorithm/RemoveElementFromList19.java @@ -0,0 +1,225 @@ +package com.funian.algorithm.algorithm; + +import java.util.List; + +/** + * 删除链表的倒数第N个节点(LeetCode 19) + * + * 时间复杂度:O(L) + * - L 是链表的长度 + * - 需要遍历链表一次(双指针法)或两次(计算长度法) + * - 总体时间复杂度为 O(L) + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 不使用与输入规模相关的额外空间 + */ +public class RemoveElementFromList19 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode() {} + ListNode(int val) { this.val = val; } + ListNode(int val, ListNode next) { this.val = val; this.next = next; } + } + + /** + * 方法1:计算链表长度法 + * + * 算法思路: + * 1. 先遍历链表计算长度 + * 2. 根据长度和n计算要删除的节点的正数位置 + * 3. 再次遍历到该位置并删除节点 + * + * 执行过程分析(以链表 1->2->3->4->5,n=2 为例): + * 1. 计算链表长度:L = 5 + * 2. 计算要删除节点的正数位置:5 - 2 + 1 = 4(即第4个节点) + * 3. 遍历到第3个节点(删除节点的前一个节点) + * 4. 修改指针跳过第4个节点:3->next = 3->next->next(即5) + * 5. 结果:1->2->3->5 + * + * 时间复杂度分析: + * - 第一次遍历计算长度:O(L),其中L为链表长度 + * - 第二次遍历找到删除位置:O(L-n) + * - 总时间复杂度:O(L) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param head 链表的头节点 + * @param n 倒数第n个节点 + * @return 删除节点后的链表头节点 + */ + public ListNode removeNthFromEndWithLength(ListNode head, int n) { + // 创建哑节点,简化头节点删除的情况 + ListNode dummy = new ListNode(0, head); + + // 第一次遍历:计算链表长度 + int length = 0; + ListNode current = head; + while (current != null) { + length++; + current = current.next; + } + + // 计算要删除节点的正数位置 + int positionFromStart = length - n; + + // 第二次遍历:找到要删除节点的前一个节点 + current = dummy; + for (int i = 0; i < positionFromStart; i++) { + current = current.next; + } + + // 删除节点:跳过要删除的节点 + current.next = current.next.next; + + // 返回结果链表的头节点 + return dummy.next; + } + + /** + * 方法2:双指针法(一次遍历) + * + * 算法思路: + * 1. 使用两个指针,第一个指针先移动n+1步 + * 2. 然后两个指针同时移动,直到第一个指针到达末尾 + * 3. 此时第二个指针指向要删除节点的前一个节点 + * + * 执行过程分析(以链表 1->2->3->4->5,n=2 为例): + * + * 初始状态: + * dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null + * first -> dummy + * second -> dummy + * + * first指针先移动n+1=3步: + * first -> 2 + * second -> dummy + * + * 两个指针同时移动直到first到达末尾: + * 第1步:first -> 3, second -> 1 + * 第2步:first -> 4, second -> 2 + * 第3步:first -> 5, second -> 3 + * 第4步:first -> null, second -> 4 + * + * 此时second指向要删除节点(4)的前一个节点(3) + * 执行删除操作:3->next = 3->next->next(即5) + * 结果:1->2->3->5 + * + * 时间复杂度分析: + * - 遍历链表一次:O(L),其中L为链表长度 + * - 总时间复杂度:O(L) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param head 链表的头节点 + * @param n 倒数第n个节点 + * @return 删除节点后的链表头节点 + */ + public ListNode removeNthFromEndWithTwoPointers(ListNode head, int n) { + // 创建哑节点,简化头节点删除的情况 + ListNode dummy = new ListNode(0, head); + + // 初始化两个指针 + ListNode first = dummy; + ListNode second = dummy; + + // 第一个指针先移动n+1步 + for (int i = 0; i <= n; i++) { + first = first.next; + } + + // 两个指针同时移动,直到第一个指针到达末尾 + while (first != null) { + first = first.next; + second = second.next; + } + + // 删除倒数第n个节点 + second.next = second.next.next; + + // 返回结果链表的头节点 + return dummy.next; + } + + /** + * 辅助方法:创建链表(用于测试) + */ + public ListNode createList(int[] values) { + if (values.length == 0) return null; + // 创建头节点 + ListNode head = new ListNode(values[0]); + // current 当前节点指针 + ListNode current = head; + // for (int i = 1; i < values.length; i++) 遍历数组剩余元素 + for (int i = 1; i < values.length; i++) { + current.next = new ListNode(values[i]); + current = current.next; + } + // 返回链表头节点 + return head; + } + + /** + * 辅助方法:打印链表(用于测试) + */ + public void printList(ListNode head) { + // current 当前节点指针,初始指向头节点 + ListNode current = head; + while (current != null) { + System.out.print(current.val); + if (current.next != null) { + System.out.print(" -> "); + } + current = current.next; + } + System.out.println(" -> null"); + } + + /** + * 测试方法和使用示例 + */ + public static void main(String[] args) { + // 创建解决方案实例 + RemoveElementFromList19 solution = new RemoveElementFromList19(); + + // 创建测试链表: 1 -> 2 -> 3 -> 4 -> 5 + ListNode head = solution.createList(new int[]{1, 2, 3, 4, 5}); + + // 打印原始链表 + System.out.println("原始链表:"); + solution.printList(head); + + // 测试删除倒数第2个节点 + ListNode result1 = solution.removeNthFromEndWithLength(head, 2); + System.out.println("删除倒数第2个节点后(计算长度法):"); + solution.printList(result1); + + // 重新创建链表进行测试 + head = solution.createList(new int[]{1, 2, 3, 4, 5}); + // solution.removeNthFromEndWithTwoPointers(head, 2) 使用双指针法删除倒数第2个节点 + ListNode result2 = solution.removeNthFromEndWithTwoPointers(head, 2); + System.out.println("删除倒数第2个节点后(双指针法):"); + solution.printList(result2); + + // 测试删除头节点 + head = solution.createList(new int[]{1, 2, 3, 4, 5}); + // solution.removeNthFromEndWithTwoPointers(head, 5) 使用双指针法删除倒数第5个节点(头节点) + ListNode result3 = solution.removeNthFromEndWithTwoPointers(head, 5); + System.out.println("删除倒数第5个节点后(删除头节点):"); + solution.printList(result3); + + // 测试删除尾节点 + head = solution.createList(new int[]{1, 2, 3, 4, 5}); + // solution.removeNthFromEndWithTwoPointers(head, 1) 使用双指针法删除倒数第1个节点(尾节点) + ListNode result4 = solution.removeNthFromEndWithTwoPointers(head, 1); + System.out.println("删除倒数第1个节点后(删除尾节点):"); + solution.printList(result4); + } +} diff --git a/algorithm/ReverseKGroup25.java b/algorithm/ReverseKGroup25.java new file mode 100644 index 0000000000000..75328d566c2fb --- /dev/null +++ b/algorithm/ReverseKGroup25.java @@ -0,0 +1,199 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * K个一组翻转链表(LeetCode 25) + * + * 时间复杂度:O(n) + * - n 是链表的长度 + * - 需要遍历链表一次,每个节点最多被访问常数次 + * - 总时间复杂度为 O(n) + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 不使用与输入规模相关的额外空间 + */ +public class ReverseKGroup25 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode(int x) { val = x; } + } + + /** + * K个一组翻转链表 + * + * 算法思路: + * 1. 使用哑节点简化头节点处理 + * 2. 每次确定k个节点的范围 + * 3. 翻转这k个节点 + * 4. 连接翻转后的子链表与前后部分 + * 5. 继续处理下一组 + * + * 执行过程分析(以链表 1->2->3->4->5,k=2 为例): + * + * 初始状态: + * dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null + * + * 第一组翻转(节点1和2): + * 1. 确定范围:1 -> 2 + * 2. 翻转:2 -> 1 + * 3. 连接:dummy -> 2 -> 1 -> 3 -> 4 -> 5 -> null + * + * 第二组翻转(节点3和4): + * 1. 确定范围:3 -> 4 + * 2. 翻转:4 -> 3 + * 3. 连接:dummy -> 2 -> 1 -> 4 -> 3 -> 5 -> null + * + * 第三组(不足k个节点): + * 1. 节点数不足k个,不翻转 + * 2. 最终结果:2 -> 1 -> 4 -> 3 -> 5 -> null + * + * 时间复杂度分析: + * - 遍历链表确定每组范围:O(n),其中n为链表长度 + * - 翻转每组k个节点:O(k) × O(n/k) = O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * + * @param head 链表的头节点 + * @param k 每组翻转的节点数 + * @return 翻转后的链表头节点 + */ + public ListNode reverseKGroup(ListNode head, int k) { + // 创建哑节点,简化头节点处理 + ListNode dummy = new ListNode(0); + dummy.next = head; + // prevGroupEnd 指向已处理部分的最后一个节点 + ListNode prevGroupEnd = dummy; + + // 无限循环,直到处理完所有节点 + while (true) { + // kGroupStart 当前组的第一个节点 + ListNode kGroupStart = prevGroupEnd.next; + // kGroupEnd 当前组的最后一个节点 + ListNode kGroupEnd = prevGroupEnd; + + // 确定当前组的结束节点 + for (int i = 0; i < k; i++) { + kGroupEnd = kGroupEnd.next; + // 检查是否不足k个节点 + if (kGroupEnd == null) { + // 不足k个节点,返回结果 + return dummy.next; + } + } + + // nextGroupStart 下一组的第一个节点 + ListNode nextGroupStart = kGroupEnd.next; + // 断开当前组 + kGroupEnd.next = null; + + // 反转当前组 + ListNode reversedGroup = reverse(kGroupStart); + // 连接已处理部分和当前组 + prevGroupEnd.next = reversedGroup; + // 连接当前组和下一组 + kGroupStart.next = nextGroupStart; + + // 更新 prevGroupEnd 为当前组的最后一个节点(翻转前的第一个节点) + prevGroupEnd = kGroupStart; + } + } + + /** + * 翻转链表 + * + * 算法思路: + * 使用三个指针(prev、curr、nextTemp)逐个翻转节点指向 + * + * 时间复杂度分析: + * - 遍历链表一次:O(m),其中m为链表长度 + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * + * @param head 要翻转的链表头节点 + * @return 翻转后的链表头节点 + */ + private ListNode reverse(ListNode head) { + // prev 指向前一个节点,初始为null + ListNode prev = null; + // curr 指向当前节点,初始为头节点 + ListNode curr = head; + while (curr != null) { + // nextTemp 暂存下一个节点 + ListNode nextTemp = curr.next; + // 反转当前节点的指向 + curr.next = prev; + // 移动prev到当前节点 + prev = curr; + // 移动curr到下一个节点 + curr = nextTemp; + } + // 返回新的头节点 + return prev; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 输入链表节点数 + System.out.print("请输入链表节点数:"); + // 读取节点数 + int n = scanner.nextInt(); + + // 创建链表 + ListNode head = null; + ListNode tail = null; + + // 输入链表节点值 + System.out.println("请输入链表节点值(以空格分隔):"); + for (int i = 0; i < n; i++) { + // 读取节点值 + int value = scanner.nextInt(); + // 创建新节点 + ListNode newNode = new ListNode(value); + // 检查是否为第一个节点 + if (head == null) { + // 设置头节点 + head = newNode; + // 设置尾节点 + tail = newNode; + } else { + // 连接新节点 + tail.next = newNode; + // 更新尾节点 + tail = newNode; + } + } + + // 输入k值 + System.out.print("请输入每组翻转的节点数 k:"); + // 读取k值 + int k = scanner.nextInt(); + + // 创建解决方案实例并调用方法 + ReverseKGroup25 solution = new ReverseKGroup25(); + ListNode newHead = solution.reverseKGroup(head, k); + + // 打印结果链表 + System.out.println("翻转后的链表节点值为:"); + // curr 当前节点指针,初始指向新链表头节点 + ListNode curr = newHead; + while (curr != null) { + // 打印当前节点值 + System.out.print(curr.val + " "); + // 移动到下一个节点 + curr = curr.next; + } + // 关闭scanner + scanner.close(); + } +} diff --git a/algorithm/ReverseList206.java b/algorithm/ReverseList206.java new file mode 100644 index 0000000000000..2fcff4ba70ee7 --- /dev/null +++ b/algorithm/ReverseList206.java @@ -0,0 +1,280 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Stack; + +/** + * 链表反转算法(LeetCode 206) + * + * 时间复杂度:O(n) + * - 只需要遍历链表一次 + * - 每个节点只访问一次 + * - 总时间复杂度为 O(n),其中 n 是链表的长度 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量(prev、curr、next) + * - 没有使用与输入规模相关的额外空间 + * - 递归调用栈深度为常数级别 + */ + + +public class ReverseList206 { + + // 定义链表节点类 + static class ListNode { + int val; + ListNode next; + + ListNode(int val) { + this.val = val; + this.next = null; + } + } + + /** + * 主函数:处理用户输入并输出链表反转结果 + * + * 算法流程: + * 1. 读取用户输入的链表长度和元素 + * 2. 构建原始链表 + * 3. 调用reverseList方法反转链表 + * 4. 输出反转后的链表 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 提示用户输入链表的长度 + System.out.print("请输入链表的长度: "); + // 读取链表长度 + int n = scanner.nextInt(); + if (n <= 0) { + System.out.println("链表长度必须大于0"); + return; + } + + // 提示用户输入链表的元素 + System.out.println("请输入链表的元素:"); + // 创建链表头节点 + ListNode head = new ListNode(scanner.nextInt()); + // 当前节点指针 + ListNode current = head; + + // 读取剩余元素并构建链表 + for (int i = 1; i < n; i++) { + // 读取下一个元素并创建节点 + current.next = new ListNode(scanner.nextInt()); + // 移动当前节点指针 + current = current.next; + } + + // 调用反转链表的方法 + ListNode reversedHead = reverseList(head); + + // 输出反转后的链表 + System.out.println("反转后的链表:"); + printList(reversedHead); + + scanner.close(); + } + + /** + * 反转链表的核心方法 + * + * 算法思路: + * 使用三个指针(prev、curr、next)逐个反转链表中的节点指向关系 + * 通过迭代方式实现链表反转,不使用额外的存储空间 + * + * 执行过程分析(以链表 1->2->3->4->NULL 为例): + * + * 初始状态: + * prev = null, curr = 1->2->3->4->NULL + * + * 第1次迭代: + * next = 2->3->4->NULL + * curr.next = null (1->null) + * prev = 1->null + * curr = 2->3->4->NULL + * + * 第2次迭代: + * next = 3->4->NULL + * curr.next = 1->null (2->1->null) + * prev = 2->1->null + * curr = 3->4->NULL + * + * 第3次迭代: + * next = 4->NULL + * curr.next = 2->1->null (3->2->1->null) + * prev = 3->2->1->null + * curr = 4->NULL + * + * 第4次迭代: + * next = null + * curr.next = 3->2->1->null (4->3->2->1->null) + * prev = 4->3->2->1->null + * curr = null + * + * 循环结束,返回 prev(即 4->3->2->1->NULL) + * + * 时间复杂度分析: + * - 遍历链表一次:O(n),其中n为链表节点数 + * - 每个节点只访问一次 + * + * 空间复杂度分析: + * - 只使用了常数个额外变量(prev、curr、next):O(1) + * - 没有使用与输入规模相关的额外空间 + * + * @param head 原始链表的头节点 + * @return 反转后链表的头节点 + */ + public static ListNode reverseList(ListNode head) { + // 指向前一个节点,初始为null + ListNode prev = null; + // 指向当前节点,初始为头节点 + ListNode curr = head; + + // 当当前节点不为null时继续循环 + while (curr != null) { + // 暂存下一个节点,防止丢失 + ListNode next = curr.next; + // 当前节点的next指向前一个节点 + curr.next = prev; + // prev前移,指向当前节点 + prev = curr; + // curr前移,指向下一个节点 + curr = next; + } + + // 返回新链表的头节点(原链表的最后一个节点) + return prev; + } + + /** + * 打印链表的方法 + * + * 算法思路: + * 从头节点开始,依次遍历每个节点并打印其值 + * 直到遇到null为止 + * + * @param head 链表的头节点 + */ + public static void printList(ListNode head) { + // 当前节点指针,初始指向头节点 + ListNode curr = head; + // 当前节点不为空时继续 + while (curr != null) { + // 打印当前节点的值和空格 + System.out.print(curr.val + " "); + // 移动到下一个节点 + curr = curr.next; + } + // 换行 + System.out.println(); + } + + /** + * 方法2:递归实现链表反转 + * + * 算法思路: + * 1. 递归到链表末尾 + * 2. 在回溯过程中反转节点指向 + * + * 示例过程(以链表 1->2->3->4->NULL 为例): + * + * 1. 递归调用过程: + * reverseListRecursive(1) -> reverseListRecursive(2) -> reverseListRecursive(3) -> reverseListRecursive(4) + * 到达基础情况: head=4, head.next=null, 返回4 + * + * 2. 回溯过程: + * head=3: head.next.next=3 (4->3), head.next=null (3->null), 返回4 + * head=2: head.next.next=2 (3->2), head.next=null (2->null), 返回4 + * head=1: head.next.next=1 (2->1), head.next=null (1->null), 返回4 + * + * 3. 最终结果: 4->3->2->1->NULL + * + * 时间复杂度分析: + * - 递归深度为n:O(n),其中n为链表节点数 + * - 每层递归执行常数时间操作 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * + * @param head 原始链表的头节点 + * @return 反转后链表的头节点 + */ + public static ListNode reverseListRecursive(ListNode head) { + // 基础情况:空链表或只有一个节点 + if (head == null || head.next == null) { + return head; + } + + // 递归反转剩余部分 + ListNode newHead = reverseListRecursive(head.next); + + // 反转当前节点和下一个节点的连接 + head.next.next = head; + head.next = null; + + return newHead; + } + + /** + * 方法3:使用栈实现链表反转 + * + * 算法思路: + * 1. 将链表所有节点压入栈中 + * 2. 从栈中弹出节点重新构建链表 + * + * 示例过程(以链表 1->2->3->4->NULL 为例): + * + * 1. 压栈过程: + * stack: [1, 2, 3, 4] (4在栈顶) + * + * 2. 构建新链表: + * newHead = stack.pop() = 4 + * current = 4 + * pop 3: current.next = 3, current = 3 + * pop 2: current.next = 2, current = 2 + * pop 1: current.next = 1, current = 1 + * current.next = null + * + * 3. 最终结果: 4->3->2->1->NULL + * + * 时间复杂度分析: + * - 遍历链表压栈:O(n),其中n为链表节点数 + * - 从栈中弹出构建新链表:O(n) + * + * 空间复杂度分析: + * - 栈存储所有节点:O(n) + * + * @param head 原始链表的头节点 + * @return 反转后链表的头节点 + */ + public static ListNode reverseListWithStack(ListNode head) { + if (head == null) return null; + + // 使用栈存储节点 + java.util.Stack stack = new java.util.Stack<>(); + ListNode current = head; + + // 将所有节点压入栈中 + while (current != null) { + stack.push(current); + current = current.next; + } + + // 构建新链表 + ListNode newHead = stack.pop(); + current = newHead; + + // 从栈中弹出节点并连接 + while (!stack.isEmpty()) { + current.next = stack.pop(); + current = current.next; + } + + // 设置最后一个节点的next为null + current.next = null; + + return newHead; + } +} \ No newline at end of file diff --git a/algorithm/ReverseList234.java b/algorithm/ReverseList234.java new file mode 100644 index 0000000000000..e4e0afdb3ce46 --- /dev/null +++ b/algorithm/ReverseList234.java @@ -0,0 +1,332 @@ +package com.funian.algorithm.algorithm; + +import java.util.List; +import java.util.Scanner; + +/** + * 回文链表(LeetCode 234) + * + * 时间复杂度:O(n) + * - 找到中间节点:O(n/2) + * - 反转后半部分:O(n/2) + * - 比较两部分:O(n/2) + * - 恢复链表:O(n/2) + * - 总时间复杂度:O(n) + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用递归或额外的数据结构 + */ +public class ReverseList234 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + + ListNode(int val) { + this.val = val; + } + + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 提示用户输入链表节点数 + System.out.print("请输入链表的节点数: "); + // 读取节点数 + int n = scanner.nextInt(); + + // 创建链表 + // 链表头节点 + ListNode head = null; + // 当前节点 + ListNode current = null; + System.out.println("请输入链表的节点值: "); + // 读取节点值并构建链表 + for (int i = 0; i < n; i++) { + // 读取节点值 + int val = scanner.nextInt(); + // 创建新节点 + ListNode node = new ListNode(val); + // 如果是第一个节点 + if (head == null) { + head = node; + current = node; + } else { + // 连接节点 + current.next = node; + current = node; + } + } + + // 创建解决方案实例 + ReverseList234 solution = new ReverseList234(); + + // 调用 isPalindrome 方法检查是否为回文链表 + boolean result = solution.isPalindrome(head); + + // 输出结果 + if (result) { + // 打印是回文链表的信息 + System.out.println("该链表是回文链表"); + } else { + // 打印不是回文链表的信息 + System.out.println("该链表不是回文链表"); + } + + scanner.close(); + } + + /** + * 判断链表是否为回文链表 + * + * 算法思路: + * 1. 使用快慢指针找到链表的中间节点 + * 2. 反转链表的后半部分 + * 3. 比较前半部分和反转后的后半部分 + * 4. 恢复链表结构(可选) + * + * 示例过程(以链表 1->2->2->1 为例): + * + * 原链表: 1 -> 2 -> 2 -> 1 + * + * 步骤1 - 找到中间节点: + * 快指针: 1 -> 2 -> 2 (每次走两步) + * 慢指针: 1 -> 2 (每次走一步) + * 中间节点: 2 (第一个2) + * + * 步骤2 - 反转后半部分: + * 原后半部分: 2 -> 1 + * 反转后: 1 -> 2 + * + * 步骤3 - 比较两部分: + * 前半部分: 1 -> 2 + * 后半部分: 1 -> 2 + * 比较结果: 相等,是回文链表 + * + * 步骤4 - 恢复链表: + * 将后半部分再次反转恢复原状 + * + * 时间复杂度分析: + * - 找到中间节点:O(n/2),其中n为链表长度 + * - 反转后半部分:O(n/2) + * - 比较两部分:O(n/2) + * - 恢复链表:O(n/2) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * - 没有使用递归或额外的数据结构 + * + * @param head 链表的头节点 + * @return 如果是回文链表返回true,否则返回false + */ + public boolean isPalindrome(ListNode head) { + // 边界条件检查:空链表被认为是回文链表 + if (head == null) return true; + + // 使用快慢指针找到链表的中间节点 + ListNode firstHalfEnd = endOfFirstHalf(head); + // 反转链表的后半部分 + ListNode secondHalfStart = reverseList(firstHalfEnd.next); + + // 检查链表是否回文 + ListNode p1 = head; + ListNode p2 = secondHalfStart; + boolean result = true; + // 比较两个部分的节点值 + while (result && p2 != null) { + if (p1.val != p2.val) { + result = false; // 发现不相等的节点值,不是回文链表 + } + p1 = p1.next; + p2 = p2.next; + } + + // 恢复链表结构(可选,保持原链表不变) + firstHalfEnd.next = reverseList(secondHalfStart); + + // 返回比较结果 + return result; + } + + /** + * 反转链表 + * 使用迭代方法反转链表 + * + * 时间复杂度分析: + * - 遍历链表一次:O(m),其中m为被反转部分的长度 + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param head 需要反转的链表头节点 + * @return 反转后的链表头节点 + */ + private ListNode reverseList(ListNode head) { + // prev 指向前一个节点,初始为null + ListNode prev = null; + // curr 指向当前节点,初始为头节点 + ListNode curr = head; + // 遍历链表,逐个反转节点指向 + while (curr != null) { + // next 保存下一个节点 + ListNode next = curr.next; + // curr.next = prev 反转当前节点的指向 + curr.next = prev; + // prev = curr 移动prev指针 + prev = curr; + // curr = next 移动curr指针 + curr = next; + } + // 返回新的头节点 + return prev; + } + + /** + * 使用快慢指针找到链表的中间节点 + * + * 时间复杂度分析: + * - 快慢指针遍历:O(n/2),其中n为链表长度 + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param head 链表的头节点 + * @return 链表前半部分的最后一个节点 + */ + private ListNode endOfFirstHalf(ListNode head) { + // fast 快指针,每次移动两步 + ListNode fast = head; + // slow 慢指针,每次移动一步 + ListNode slow = head; + // 当快指针还能继续移动时 + while (fast.next != null && fast.next.next != null) { + // slow = slow.next 慢指针移动一步 + slow = slow.next; + // fast = fast.next.next 快指针移动两步 + fast = fast.next.next; + } + // 返回前半部分的最后一个节点 + return slow; + } + + + /** + * 方法2:使用数组存储值 + * + * 算法思路: + * 1. 遍历链表,将节点值存储到数组中 + * 2. 使用双指针从两端向中间比较数组元素 + * + * 示例过程(以链表 1->2->2->1 为例): + * + * 1. 遍历链表计算长度: length=4 + * 2. 创建数组: values=[1,2,2,1] + * 3. 双指针比较: + * left=0, right=3: values[0]=1, values[3]=1, 相等 + * left=1, right=2: values[1]=2, values[2]=2, 相等 + * left=2, right=1: left>=right, 结束 + * 4. 返回true + * + * 时间复杂度分析: + * - 计算链表长度:O(n),其中n为链表长度 + * - 存储到数组:O(n) + * - 双指针比较:O(n/2) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 数组存储节点值:O(n) + * + * @param head 链表的头节点 + * @return 如果是回文链表返回true,否则返回false + */ + public boolean isPalindromeWithArray(ListNode head) { + if (head == null) return true; + + // 计算链表长度 + int length = 0; + ListNode current = head; + while (current != null) { + length++; + current = current.next; + } + + // 将链表值存储到数组中 + int[] values = new int[length]; + current = head; + for (int i = 0; i < length; i++) { + values[i] = current.val; + current = current.next; + } + + // 使用双指针比较数组元素 + int left = 0; + int right = length - 1; + while (left < right) { + if (values[left] != values[right]) { + return false; + } + left++; + right--; + } + + return true; + } + + + /** + * 方法3:使用递归 + * + * 算法思路: + * 1. 使用递归到达链表末尾 + * 2. 在回溯过程中比较节点值 + * + * 示例过程(以链表 1->2->2->1 为例): + * + * 1. 递归调用过程: + * recursivelyCheck(1) -> recursivelyCheck(2) -> recursivelyCheck(2) -> recursivelyCheck(1) -> null + * + * 2. 回溯比较过程: + * currentNode=1, frontPointer=1: 1==1, frontPointer移动到2 + * currentNode=2, frontPointer=2: 2==2, frontPointer移动到2 + * currentNode=2, frontPointer=2: 2==2, frontPointer移动到1 + * currentNode=1, frontPointer=1: 1==1, frontPointer移动到null + * + * 3. 返回true + * + * 时间复杂度分析: + * - 递归深度:O(n),其中n为链表长度 + * - 每层递归执行常数时间操作 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * + * @param head 链表的头节点 + * @return 如果是回文链表返回true,否则返回false + */ + private ListNode frontPointer; + + public boolean isPalindromeRecursive(ListNode head) { + frontPointer = head; + return recursivelyCheck(head); + } + + private boolean recursivelyCheck(ListNode currentNode) { + if (currentNode != null) { + if (!recursivelyCheck(currentNode.next)) { + return false; + } + if (currentNode.val != frontPointer.val) { + return false; + } + frontPointer = frontPointer.next; + } + return true; + } +} diff --git a/algorithm/RightSideView199.java b/algorithm/RightSideView199.java new file mode 100644 index 0000000000000..718dcd0a857b1 --- /dev/null +++ b/algorithm/RightSideView199.java @@ -0,0 +1,308 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +/** + * 二叉树的右视图(LeetCode 199) + * + * 时间复杂度:O(n) + * - n是二叉树中的节点数 + * - 每个节点都需要被访问一次 + * + * 空间复杂度:O(w) + * - w是二叉树的最大宽度 + * - 队列最多存储一层的所有节点 + */ +public class RightSideView199 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int val) { + this.val = val; + } + } + + /** + * 获取二叉树的右视图 + * + * 算法思路: + * 使用层序遍历(BFS),每层只记录最右边的节点值 + * 在每层遍历时,将最后一个访问的节点值加入结果列表 + * + * 执行过程分析(以二叉树 [1,2,3,null,5,null,4] 为例): + * + * 1 <- 右视图节点: 1 + * / \ + * 2 3 <- 右视图节点: 3 + * \ \ + * 5 4 <- 右视图节点: 4 + * + * 层序遍历过程: + * 第1层: [1] -> 记录1 + * 第2层: [2,3] -> 记录3 + * 第3层: [5,4] -> 记录4 + * + * 右视图结果: [1,3,4] + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n),其中n为二叉树节点数 + * + * 空间复杂度分析: + * - 队列最多存储一层的节点数:O(w),其中w为树的最大宽度 + * - 结果列表存储右视图节点:O(h),其中h为树的高度 + * - 总空间复杂度:O(w) + * + * @param root 二叉树的根节点 + * @return 从右侧看到的节点值列表 + */ + public List rightSideView(TreeNode root) { + // 创建结果列表,用于存储右视图节点值 + List result = new ArrayList<>(); + + // 如果根节点为空,直接返回空结果 + if (root == null) { + return result; + } + + // 使用队列进行广度优先搜索 + Queue queue = new LinkedList<>(); + queue.offer(root); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 记录当前层的节点数 + int size = queue.size(); + + // 处理当前层的所有节点 + for (int i = 0; i < size; i++) { + // 弹出队列中的节点 + TreeNode node = queue.poll(); + + // 如果是当前层的最后一个节点,将其值加入结果列表 + if (i == size - 1) { + result.add(node.val); + } + + // 将下一层的节点加入队列 + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } + } + + return result; + } + + /** + * 递归解法 + * + * 算法思路: + * 使用深度优先搜索(DFS),优先遍历右子树 + * 对于每个深度,只记录第一次访问到的节点值(即该层最右边的节点) + * + * 执行过程分析(以二叉树 [1,2,3,null,5,null,4] 为例): + * + * 1 <- 深度0: 记录1 + * / \ + * 2 3 <- 深度1: 记录3(右子树优先) + * \ \ + * 5 4 <- 深度2: 记录4 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h),其中h为树的高度 + * - 结果列表存储右视图节点:O(h) + * - 总空间复杂度:O(h) + * + * @param root 二叉树的根节点 + * @return 从右侧看到的节点值列表 + */ + public List rightSideViewRecursive(TreeNode root) { + // 创建结果列表,用于存储右视图节点值 + List result = new ArrayList<>(); + rightSideViewHelper(root, 0, result); + return result; + } + + /** + * 递归辅助方法 + * + * 算法思路: + * 1. 如果节点为空,直接返回 + * 2. 如果当前深度等于结果列表大小,说明是该层第一次访问,记录节点值 + * 3. 优先递归处理右子树,再处理左子树 + * + * 时间复杂度分析: + * - 每个节点访问一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(h) + * + * @param node 当前节点 + * @param depth 当前深度 + * @param result 结果列表 + */ + private void rightSideViewHelper(TreeNode node, int depth, List result) { + // 基础情况:如果节点为空,直接返回 + if (node == null) { + return; + } + + // 如果当前深度等于结果列表大小,说明是该层第一次访问 + if (result.size() == depth) { + result.add(node.val); + } + + // 优先递归处理右子树,再处理左子树 + rightSideViewHelper(node.right, depth + 1, result); + rightSideViewHelper(node.left, depth + 1, result); + } + + /** + * 辅助方法:根据数组创建二叉树(用于测试) + * + * 算法思路: + * 按层序遍历的方式构建二叉树 + * 使用队列来维护当前需要处理子节点的节点 + */ + public static TreeNode createTree() { + // 创建Scanner对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入二叉树节点值 + System.out.println("请输入二叉树节点值,按层序遍历输入,null表示空节点,用空格分隔:"); + // 读取用户输入的一行 + String input = scanner.nextLine(); + // 按空格分割输入字符串得到节点值数组 + String[] values = input.split(" "); + + // 检查输入是否为空 + if (values.length == 0 || "null".equals(values[0]) || values[0].isEmpty()) { + return null; + } + + // 创建根节点 + TreeNode root = new TreeNode(Integer.parseInt(values[0])); + // 创建队列用于构建二叉树 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + + // 数组索引,从1开始处理子节点 + int i = 1; + // 当队列不为空且索引未越界时继续处理 + while (!queue.isEmpty() && i < values.length) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + + // 处理左子节点 + // 检查左子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建左子节点 + node.left = new TreeNode(Integer.parseInt(values[i])); + // 将左子节点加入队列 + queue.offer(node.left); + } + // 索引递增 + i++; + + // 处理右子节点 + // 检查右子节点是否存在且不为null + if (i < values.length && !"null".equals(values[i]) && !values[i].isEmpty()) { + // 创建右子节点 + node.right = new TreeNode(Integer.parseInt(values[i])); + // 将右子节点加入队列 + queue.offer(node.right); + } + // 索引递增 + i++; + } + + // 返回构建完成的二叉树根节点 + return root; + } + + /** + * 辅助方法:层序遍历打印二叉树 + * + * 算法思路: + * 使用队列进行层序遍历,依次打印每个节点的值 + */ + public static void printLevelOrder(TreeNode root) { + // 检查根节点是否为空 + if (root == null) { + // 如果为空,打印提示信息并返回 + System.out.println("空树"); + return; + } + + // 创建队列用于层序遍历 + Queue queue = new LinkedList<>(); + // 将根节点加入队列 + queue.offer(root); + // 打印提示信息 + System.out.print("层序遍历结果: "); + + // 当队列不为空时继续遍历 + while (!queue.isEmpty()) { + // 从队列中取出一个节点 + TreeNode node = queue.poll(); + if (node == null) { + System.out.print("null "); + } else { + System.out.print(node.val + " "); + queue.offer(node.left); + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 主函数:处理用户输入并获取二叉树的右视图 + * + * 程序执行流程: + * 1. 提示用户输入二叉树节点值 + * 2. 根据输入构建二叉树 + * 3. 打印构建的二叉树 + * 4. 调用两种方法获取右视图 + * 5. 打印右视图结果 + */ + public static void main(String[] args) { + System.out.println("二叉树右视图"); + + // 创建二叉树 + TreeNode root = createTree(); + + // 检查创建的二叉树是否为空 + if (root == null) { + // 如果为空,打印提示信息并退出 + System.out.println("创建的二叉树为空"); + return; + } + + // 打印创建的二叉树 + System.out.println("创建的二叉树:"); + printLevelOrder(root); + + // 获取右视图 + // 创建解决方案实例 + RightSideView199 solution = new RightSideView199(); + List result1 = solution.rightSideView(root); + List result2 = solution.rightSideViewRecursive(root); + + // 打印结果 + System.out.println("迭代方法右视图结果: " + result1); + System.out.println("递归方法右视图结果: " + result2); + } +} diff --git a/algorithm/RobMax198.java b/algorithm/RobMax198.java new file mode 100644 index 0000000000000..51b6a86947b54 --- /dev/null +++ b/algorithm/RobMax198.java @@ -0,0 +1,209 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 打家劫舍(LeetCode 198)- 动态规划 + * + * 时间复杂度:O(n) + * - n是房屋数量 + * - 需要遍历数组一次 + * + * 空间复杂度:O(n) 或 O(1) + * - 标准DP解法需要O(n)空间存储DP数组 + * - 空间优化版本只需要O(1)空间 + */ +public class RobMax198 { + + /** + * 计算能偷窃到的最高金额 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示到第i个房屋时能偷窃到的最高金额 + * 状态转移方程: + * `dp[i] = max(dp[i-1], dp[i-2] + nums[i])` + * (要么不偷第i个房屋,金额为`dp[i-1]`;要么偷第i个房屋,金额为`dp[i-2]` + `nums[i]`) + * + * 边界条件: + * `dp[0]` = `nums[0]`(只有一个房屋) + * `dp[1]` = max(`nums[0]`, `nums[1]`)(有两个房屋,选择金额较高的) + * + * 执行过程分析(以`nums=[2,7,9,3,1]`为例): + * + * 初始化: + * dp[0] = 2 + * dp[1] = max(2, 7) = 7 + * + * 填充DP数组: + * dp[2] = max(dp[1], dp[0] + nums[2]) = max(7, 2+9) = max(7, 11) = 11 + * dp[3] = max(dp[2], dp[1] + nums[3]) = max(11, 7+3) = max(11, 10) = 11 + * dp[4] = max(dp[3], dp[2] + nums[4]) = max(11, 11+1) = max(11, 12) = 12 + * + * 最优策略:偷第1、3、5个房屋(金额2+9+1=12) + * + * 时间复杂度分析: + * - 边界情况处理:O(1) + * - 初始化DP数组:O(1) + * - 填充DP数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * + * @param nums 每个房屋存放金额的数组 + * @return 能偷窃到的最高金额 + */ + public int rob(int[] nums) { + // 边界情况处理 + if (nums == null || nums.length == 0) return 0; + if (nums.length == 1) return nums[0]; + if (nums.length == 2) return Math.max(nums[0], nums[1]); + + // 创建DP数组,dp[i]表示到第i个房屋时能偷窃到的最高金额 + int[] dp = new int[nums.length]; + + // 初始化边界条件 + dp[0] = nums[0]; + dp[1] = Math.max(nums[0], nums[1]); + + // 填充DP数组 + for (int i = 2; i < nums.length; i++) { + // 状态转移方程: + // 要么不偷第i个房屋(dp[i-1]) + // 要么偷第i个房屋(dp[i-2] + nums[i]) + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); + } + + // 返回到最后一个房屋时能偷窃到的最高金额 + return dp[nums.length - 1]; + } + + /** + * 空间优化版本:只使用两个变量 + * + * 算法思路: + * 观察状态转移方程,发现每次只需要前两个状态值 + * 可以用两个变量代替整个DP数组 + * + * 时间复杂度分析: + * - 边界情况处理:O(1) + * - 初始化变量:O(1) + * - 循环计算:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 每个房屋存放金额的数组 + * @return 能偷窃到的最高金额 + */ + public int robOptimized(int[] nums) { + // 边界情况处理 + if (nums == null || nums.length == 0) return 0; + if (nums.length == 1) return nums[0]; + + // 只需要两个变量存储前两个状态 + int prev2 = nums[0]; // dp[i-2] + int prev1 = Math.max(nums[0], nums[1]); // dp[i-1] + + // 如果只有两个房屋,直接返回较大值 + if (nums.length == 2) return prev1; + + // 从第三个房屋开始计算 + for (int i = 2; i < nums.length; i++) { + int current = Math.max(prev1, prev2 + nums[i]); + prev2 = prev1; + prev1 = current; + } + + return prev1; + } + + /** + * 方法3:另一种DP思路 + * + * 算法思路: + * 定义两个状态: + * - `rob[i]`:偷第i个房屋时能获得的最大金额 + * - `notRob[i]`:不偷第i个房屋时能获得的最大金额 + * + * 状态转移方程: + * `rob[i]` = `notRob[i-1]` + `nums[i]` + * `notRob[i]` = max(`rob[i-1]`, `notRob[i-1]`) + * + * 时间复杂度分析: + * - 边界情况处理:O(1) + * - 初始化状态:O(1) + * - 循环计算:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 每个房屋存放金额的数组 + * @return 能偷窃到的最高金额 + */ + public int robAlternative(int[] nums) { + if (nums == null || nums.length == 0) return 0; + + int rob = nums[0]; // 偷第一个房屋 + int notRob = 0; // 不偷第一个房屋 + + for (int i = 1; i < nums.length; i++) { + int newRob = notRob + nums[i]; // 偷当前房屋 + int newNotRob = Math.max(rob, notRob); // 不偷当前房屋 + rob = newRob; + notRob = newNotRob; + } + + return Math.max(rob, notRob); + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 读取和处理输入:O(n) + * + * 空间复杂度分析: + * - 存储数组:O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入每个房屋的金额(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 主函数:处理用户输入并计算最大偷窃金额 + */ + public static void main(String[] args) { + System.out.println("打家劫舍问题"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("房屋金额: " + Arrays.toString(nums)); + + // 计算最大偷窃金额 + RobMax198 solution = new RobMax198(); + int result1 = solution.rob(nums); + int result2 = solution.robOptimized(nums); + int result3 = solution.robAlternative(nums); + + // 输出结果 + System.out.println("标准动态规划方法结果: " + result1); + System.out.println("空间优化方法结果: " + result2); + System.out.println("状态机方法结果: " + result3); + } +} diff --git a/algorithm/RotateArray189.java b/algorithm/RotateArray189.java new file mode 100644 index 0000000000000..7a98ba15277ae --- /dev/null +++ b/algorithm/RotateArray189.java @@ -0,0 +1,236 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 旋转数组(LeetCode 189) + * + * 时间复杂度:O(n) + * - 需要翻转整个数组:O(n) + * - 需要翻转前k个元素:O(k) + * - 需要翻转后n-k个元素:O(n-k) + * - 总时间复杂度:O(n) + O(k) + O(n-k) = O(n) + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 翻转操作是原地进行的,不需要额外的存储空间 + */ +public class RotateArray189 { + + /** + * 主函数测试 + * 测试数组 [1,2,3,4,5,6,7] 向右旋转 3 位的结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("请输入数组元素(用空格分隔):"); + String line = scanner.nextLine(); + String[] strArray = line.split(" "); + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + // 提示用户输入旋转步数 + System.out.print("请输入旋转步数 k:"); + int k = scanner.nextInt(); + + // 调用 rotate 方法将数组向右旋转 k 位 + rotate(nums, k); + + // 输出旋转后的数组 + System.out.print("旋转后的数组:"); + for (int i = 0; i < nums.length; i++) { + System.out.print(nums[i] + " "); + } + System.out.println(); + } + + /** + * 旋转数组 + * 将数组中的元素向右移动 k 个位置 + * + * 算法思路: + * 1. 先将整个数组翻转 + * 2. 再将前 k 个元素翻转 + * 3. 最后将后 n-k 个元素翻转 + * + * 示例过程(以数组 [1,2,3,4,5,6,7],k=3 为例): + * 原数组: [1,2,3,4,5,6,7] + * 步骤1 - 翻转整个数组: [7,6,5,4,3,2,1] + * 步骤2 - 翻转前k个元素: [5,6,7,4,3,2,1] + * 步骤3 - 翻转后n-k个元素: [5,6,7,1,2,3,4] + * 结果: [5,6,7,1,2,3,4] (相当于原数组向右移动3位) + * + * 时间复杂度分析: + * - 翻转整个数组:O(n),其中n为输入数组`nums`的长度 + * - 翻转前k个元素:O(k) + * - 翻转后n-k个元素:O(n-k) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外变量:O(1) + * - 翻转操作是原地进行的:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + * @param k 向右旋转的步数 + */ + public static void rotate(int[] nums, int k) { + // 获取数组长度 + int n = nums.length; + + // 处理 k 大于数组长度的情况,取模运算 + k = k % n; + + // 步骤1:翻转整个数组 + reverse(nums, 0, n - 1); + + // 步骤2:翻转前 k 个元素 + reverse(nums, 0, k - 1); + + // 步骤3:翻转后 n-k 个元素 + reverse(nums, k, n - 1); + } + + + /** + * 翻转数组指定范围内的元素 + * 使用双指针技术,从两端向中间交换元素 + * + * 时间复杂度分析: + * - 双指针遍历范围:O((end-start+1)/2) = O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外变量:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + * @param start 翻转范围的起始索引(包含) + * @param end 翻转范围的结束索引(包含) + */ + public static void reverse(int[] nums, int start, int end) { + // 双指针从两端向中间移动 + while (start < end) { + // 交换 start 和 end 位置的元素 + int temp = nums[start]; + nums[start] = nums[end]; + nums[end] = temp; + + // 移动指针 + start++; + end--; + } + } + + /** + * 方法2:使用额外数组 + * + * 算法思路: + * 创建新数组,将原数组元素按旋转规则放置到新位置,再复制回原数组 + * + * 示例过程(以数组 [1,2,3,4,5,6,7],k=3 为例): + * + * 1. 初始化: nums=[1,2,3,4,5,6,7], k=3 + * 2. 创建新数组: rotated=[0,0,0,0,0,0,0] + * 3. 按规则放置元素: + * i=0: rotated[(0+3)%7] = rotated[3] = nums[0] = 1 + * i=1: rotated[(1+3)%7] = rotated[4] = nums[1] = 2 + * i=2: rotated[(2+3)%7] = rotated[5] = nums[2] = 3 + * i=3: rotated[(3+3)%7] = rotated[6] = nums[3] = 4 + * i=4: rotated[(4+3)%7] = rotated[0] = nums[4] = 5 + * i=5: rotated[(5+3)%7] = rotated[1] = nums[5] = 6 + * i=6: rotated[(6+3)%7] = rotated[2] = nums[6] = 7 + * 4. 复制回原数组: nums=[5,6,7,1,2,3,4] + * + * 时间复杂度分析: + * - 遍历数组两次:O(n),其中n为输入数组`nums`的长度 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 需要额外数组存储结果:O(n) + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @param k 向右旋转的步数 + */ + public static void rotateExtraArray(int[] nums, int k) { + int n = nums.length; + k = k % n; + + // 创建新数组存储旋转后的结果 + int[] rotated = new int[n]; + + // 将原数组元素放到新位置 + for (int i = 0; i < n; i++) { + rotated[(i + k) % n] = nums[i]; + } + + // 将结果复制回原数组 + for (int i = 0; i < n; i++) { + nums[i] = rotated[i]; + } + } + + /** + * 方法3:循环替换 + * + * 算法思路: + * 通过循环替换的方式,将每个元素直接放到最终位置 + * + * 示例过程(以数组 [1,2,3,4,5,6],k=2 为例): + * + * 1. 初始化: nums=[1,2,3,4,5,6], k=2, n=6 + * 2. 循环替换过程: + * start=0: + * current=0, prev=1 + * next=(0+2)%6=2, temp=nums[2]=3, nums[2]=1, prev=3, current=2 + * next=(2+2)%6=4, temp=nums[4]=5, nums[4]=3, prev=5, current=4 + * next=(4+2)%6=0, temp=nums[0]=1, nums[0]=5, prev=1, current=0 + * 回到起始位置,结束本轮循环 + * count=3 < n=6, 继续 + * start=1: + * current=1, prev=2 + * next=(1+2)%6=3, temp=nums[3]=4, nums[3]=2, prev=4, current=3 + * next=(3+2)%6=5, temp=nums[5]=6, nums[5]=4, prev=6, current=5 + * next=(5+2)%6=1, temp=nums[1]=2, nums[1]=6, prev=2, current=1 + * 回到起始位置,结束本轮循环 + * count=6 = n=6, 结束 + * 3. 最终结果: nums=[5,6,1,2,3,4] + * + * 时间复杂度分析: + * - 每个元素只被移动一次:O(n),其中n为输入数组`nums`的长度 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外变量:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 输入的整数数组 + * @param k 向右旋转的步数 + */ + public static void rotateCyclic(int[] nums, int k) { + int n = nums.length; + k = k % n; + + int count = 0; + for (int start = 0; count < n; start++) { + int current = start; + int prev = nums[start]; + + // do-while循环进行元素替换 + do { + int next = (current + k) % n; + int temp = nums[next]; + nums[next] = prev; + prev = temp; + current = next; + count++; + } while (start != current); // 当回到起始位置时结束循环 + } + } +} diff --git a/algorithm/RotateMatrix48.java b/algorithm/RotateMatrix48.java new file mode 100644 index 0000000000000..059d118781893 --- /dev/null +++ b/algorithm/RotateMatrix48.java @@ -0,0 +1,260 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 旋转图像(LeetCode 48) + * + * 时间复杂度:O(n²) + * - 矩阵转置需要遍历上三角矩阵:O(n²/2) + * - 反转每一行需要遍历矩阵的一半:O(n²/2) + * - 总时间复杂度为O(n²) + * + * 空间复杂度:O(1) + * - 原地旋转,只使用了常数级别的额外空间 + * - 没有使用与输入矩阵大小相关的额外存储空间 + */ +public class RotateMatrix48 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 输入矩阵的大小 n + System.out.print("请输入矩阵的大小 n:"); + + // 读取矩阵大小 + int n = scanner.nextInt(); + + // 初始化矩阵 + int[][] matrix = new int[n][n]; + + // 提示用户输入矩阵元素 + System.out.println("请输入矩阵的元素:"); + + // 读取矩阵元素 + // 外层循环遍历行 + for (int i = 0; i < n; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + matrix[i][j] = scanner.nextInt(); + } + } + + // 调用 rotate 方法将矩阵顺时针旋转90度 + rotate(matrix); + + // 输出旋转后的矩阵 + System.out.println("旋转后的矩阵是:"); + + // 遍历并打印旋转后的矩阵 + // 外层循环遍历行 + for (int i = 0; i < n; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + System.out.print(matrix[i][j] + " "); + } + System.out.println(); + } + } + + /** + * 将矩阵顺时针旋转90度 + * + * 算法思路: + * 通过两个步骤实现旋转: + * 1. 矩阵转置(沿主对角线翻转) + * 2. 反转每一行 + * + * 数学原理: + * 矩阵顺时针旋转90度的变换公式: + * 原位置(i,j) → 新位置(j, n-1-i) + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 原矩阵: + * [1 2 3] + * [4 5 6] + * [7 8 9] + * + * 步骤1 - 矩阵转置(沿主对角线翻转): + * [1 4 7] + * [2 5 8] + * [3 6 9] + * + * 步骤2 - 反转每一行: + * [7 4 1] + * [8 5 2] + * [9 6 3] + * + * 最终结果(顺时针旋转90度): + * [7 4 1] + * [8 5 2] + * [9 6 3] + * + * 时间复杂度分析: + * - 矩阵转置:O(n²),其中n为矩阵的边长,需要遍历上三角矩阵 + * - 反转每一行:O(n²),需要对每一行进行反转操作 + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 原地操作,只使用常数额外空间:O(1) + * + * @param matrix 输入的n×n二维整数矩阵 + */ + public static void rotate(int[][] matrix) { + // 获取矩阵大小 + int n = matrix.length; + + // 第一步:矩阵转置(沿主对角线翻转) + // 只需要处理上三角矩阵(j >= i),避免重复交换 + // 外层循环遍历行 + for (int i = 0; i < n; i++) { + // 内层循环遍历列(从对角线开始) + for (int j = i; j < n; j++) { + // 交换 matrix[i][j] 和 matrix[j][i] + int temp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = temp; + } + } + + // 第二步:反转每一行 + // 外层循环遍历行 + for (int i = 0; i < n; i++) { + // 只需要交换前半部分和后半部分的元素 + // 内层循环遍历列(只需要处理前半部分) + for (int j = 0; j < n / 2; j++) { + // 交换 matrix[i][j] 和 matrix[i][n-1-j] + int temp = matrix[i][j]; + matrix[i][j] = matrix[i][n - 1 - j]; + matrix[i][n - 1 - j] = temp; + } + } + } + + /** + * 方法2:一次循环实现旋转 + * + * 算法思路: + * 直接按照旋转规律进行元素交换,一圈一圈地处理 + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 1. 初始化: n=3, layer从0到1 + * + * 2. layer=0时: + * first=0, last=2 + * i=0: offset=0 + * top=matrix[0][0]=1 + * matrix[0][0]=matrix[2][0]=7 + * matrix[2][0]=matrix[2][2]=9 + * matrix[2][2]=matrix[0][2]=3 + * matrix[0][2]=top=1 + * 矩阵变为: [[7,2,1],[4,5,6],[9,8,3]] + * i=1: offset=1 + * top=matrix[0][1]=2 + * matrix[0][1]=matrix[1][0]=4 + * matrix[1][0]=matrix[2][1]=8 + * matrix[2][1]=matrix[1][2]=6 + * matrix[1][2]=top=2 + * 矩阵变为: [[7,4,1],[8,5,2],[9,6,3]] + * + * 3. layer=1时: + * first=1, last=1, first>=last,循环结束 + * + * 最终结果: [[7,4,1],[8,5,2],[9,6,3]] + * + * 时间复杂度分析: + * - 处理每一圈:O(n) + * - 圈数:O(n/2) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 原地操作,只使用常数额外空间:O(1) + * + * @param matrix 输入的n×n二维整数矩阵 + */ + public static void rotateInPlace(int[][] matrix) { + int n = matrix.length; + + // 圈数为 n/2 + for (int layer = 0; layer < n / 2; layer++) { + int first = layer; + int last = n - 1 - layer; + + // 处理当前圈的每条边 + for (int i = first; i < last; i++) { + int offset = i - first; + + // 保存top元素 + int top = matrix[first][i]; + + // left -> top + matrix[first][i] = matrix[last - offset][first]; + + // bottom -> left + matrix[last - offset][first] = matrix[last][last - offset]; + + // right -> bottom + matrix[last][last - offset] = matrix[i][last]; + + // top -> right + matrix[i][last] = top; + } + } + } + + /** + * 方法3:使用额外空间 + * + * 算法思路: + * 创建新矩阵,按照旋转规律将原矩阵元素复制到新矩阵中 + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 1. 初始化: n=3, rotated = [[0,0,0],[0,0,0],[0,0,0]] + * + * 2. 复制元素过程: + * i=0,j=0: rotated[0][2] = matrix[0][0] = 1 + * i=0,j=1: rotated[1][2] = matrix[0][1] = 2 + * i=0,j=2: rotated[2][2] = matrix[0][2] = 3 + * i=1,j=0: rotated[0][1] = matrix[1][0] = 4 + * i=1,j=1: rotated[1][1] = matrix[1][1] = 5 + * i=1,j=2: rotated[2][1] = matrix[1][2] = 6 + * i=2,j=0: rotated[0][0] = matrix[2][0] = 7 + * i=2,j=1: rotated[1][0] = matrix[2][1] = 8 + * i=2,j=2: rotated[2][0] = matrix[2][2] = 9 + * + * rotated = [[7,4,1],[8,5,2],[9,6,3]] + * + * 3. 复制回原矩阵: matrix = [[7,4,1],[8,5,2],[9,6,3]] + * + * 时间复杂度分析: + * - 复制元素:O(n²),需要遍历原矩阵的每个元素 + * - 复制回原矩阵:O(n²),需要将新矩阵的每个元素复制回原矩阵 + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 需要额外矩阵存储结果:O(n²) + * + * @param matrix 输入的n×n二维整数矩阵 + */ + public static void rotateWithExtraSpace(int[][] matrix) { + int n = matrix.length; + int[][] rotated = new int[n][n]; + + // 按照旋转规律复制元素 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + rotated[j][n - 1 - i] = matrix[i][j]; + } + } + + // 将结果复制回原矩阵 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + matrix[i][j] = rotated[i][j]; + } + } + } +} diff --git a/algorithm/Search33.java b/algorithm/Search33.java new file mode 100644 index 0000000000000..a68fdc081717d --- /dev/null +++ b/algorithm/Search33.java @@ -0,0 +1,231 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 搜索旋转排序数组(LeetCode 33) + * + * 时间复杂度:O(log n) + * - 使用二分查找,每次将搜索范围减半 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class Search33 { + + /** + * 主函数:处理用户输入并搜索目标值 + * + * 算法流程: + * 1. 读取用户输入的旋转排序数组和目标值 + * 2. 调用search方法查找目标值的下标 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组长度 + System.out.print("请输入数组长度:"); + int n = scanner.nextInt(); + + // 创建数组并读取元素 + int[] nums = new int[n]; + System.out.println("请输入数组元素(用空格分隔):"); + for (int i = 0; i < n; i++) { + nums[i] = scanner.nextInt(); // 读取数组元素 + } + + // 提示用户输入目标值 + System.out.print("请输入目标值:"); + int target = scanner.nextInt(); + + // 调用 search 方法查找目标值的下标 + int result = search(nums, target); + + // 输出结果 + System.out.println("目标值 " + target + " 的下标:" + result); + } + + /** + * 在旋转排序数组中搜索目标值 + * + * 算法思路: + * 使用修改版的二分查找 + * 1. 数组被旋转后,至少有一半是有序的 + * 2. 每次判断哪一半是有序的 + * 3. 判断目标值是否在有序的那一半中 + * 4. 根据判断结果调整搜索范围 + * + * @param nums 旋转排序数组(无重复元素) + * @param target 目标值 + * @return 目标值的下标,如果不存在返回-1 + */ + public static int search(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; // 返回目标值的下标 + } + + // 判断哪部分是有序的 + // 关键判断条件:nums[left] <= nums[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; + } + } + } + + // 如果未找到目标值,返回 -1 + return -1; + } + + /** + * 方法2:递归版本 + * + * 算法思路: + * 使用递归实现修改版的二分查找 + * + * @param nums 旋转排序数组 + * @param target 目标值 + * @return 目标值的下标,如果不存在返回-1 + */ + public int searchRecursive(int[] nums, int target) { + // 调用递归辅助方法 + return searchHelper(nums, 0, nums.length - 1, target); + } + + private int searchHelper(int[] nums, int left, int right, int target) { + // 检查递归终止条件 + if (left > right) { + // 返回-1表示未找到 + return -1; + } + + // 计算中间索引 + int mid = left + (right - left) / 2; + + // 检查中间元素是否等于目标值 + if (nums[mid] == target) { + // 返回目标值下标 + return mid; + } + + // 判断左半部分是否有序 + if (nums[left] <= nums[mid]) { + // 判断目标值是否在左半部分有序区间内 + if (nums[left] <= target && target < nums[mid]) { + // 递归搜索左半部分 + return searchHelper(nums, left, mid - 1, target); + } else { + // 递归搜索右半部分 + return searchHelper(nums, mid + 1, right, target); + } + } else { + // 判断目标值是否在右半部分有序区间内 + if (nums[mid] < target && target <= nums[right]) { + // 递归搜索右半部分 + return searchHelper(nums, mid + 1, right, target); + } else { + // 递归搜索左半部分 + return searchHelper(nums, left, mid - 1, target); + } + } + } + + /** + * 方法3:先找到旋转点,再进行二分查找 + * + * 算法思路: + * 1. 先找到旋转点(最小元素位置) + * 2. 在两个有序部分分别进行二分查找 + * + * @param nums 旋转排序数组 + * @param target 目标值 + * @return 目标值的下标,如果不存在返回-1 + */ + public int searchFindPivot(int[] nums, int target) { + // 检查数组是否为空 + if (nums == null || nums.length == 0) { + // 返回-1 + return -1; + } + + // 找到旋转点 + int pivot = findPivot(nums); + + // 在两个有序部分分别进行二分查找 + int result1 = Arrays.binarySearch(nums, 0, pivot + 1, target); + int result2 = Arrays.binarySearch(nums, pivot + 1, nums.length, target); + + // 检查是否在左半部分找到 + if (result1 >= 0) { + // 返回左半部分查找结果 + return result1; + } else if (result2 >= 0) { + // 返回右半部分查找结果 + return result2; + } else { + // 返回-1表示未找到 + return -1; + } + } + + /** + * 找到旋转点(最小元素的索引) + * + * 算法思路: + * 使用二分查找找到最小元素的位置 + */ + private int findPivot(int[] nums) { + // 初始化左指针 + int left = 0; + // 初始化右指针 + int right = nums.length - 1; + + // 当左指针小于右指针时循环 + while (left < right) { + // 计算中间索引 + int mid = left + (right - left) / 2; + // 比较中间元素和右边界元素 + if (nums[mid] > nums[right]) { + // 更新左指针 + left = mid + 1; + } else { + // 更新右指针 + right = mid; + } + } + + // 返回旋转点索引 + return left; + } +} diff --git a/algorithm/SearchExist79.java b/algorithm/SearchExist79.java new file mode 100644 index 0000000000000..76f52ea94345f --- /dev/null +++ b/algorithm/SearchExist79.java @@ -0,0 +1,201 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 单词搜索(LeetCode 79) + * + * 时间复杂度:O(M×N×4^L) + * - M和N是网格的行数和列数 + * - L是单词长度 + * - 最坏情况下需要从每个单元格开始进行深度为L的搜索 + * + * 空间复杂度:O(L) + * - 递归调用栈深度最大为L(单词长度) + * - 使用原地修改标记已访问单元格,不需要额外空间 + */ +public class SearchExist79 { + + // 成员变量用于存储网格、单词和网格尺寸 + private char[][] board; + private String word; + private int rows, cols; + + /** + * 程序入口点,处理用户输入并执行单词搜索 + * + * @param args 命令行参数 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于输入 + Scanner scanner = new Scanner(System.in); + + // 输入二维字符网格的行数和列数 + System.out.print("请输入网格的行数 m 和列数 n(用空格分隔):"); + int m = scanner.nextInt(); + int n = scanner.nextInt(); + scanner.nextLine(); // 处理换行符 + + // 输入二维字符网格 + char[][] board = new char[m][n]; + System.out.println("请输入网格的字符(每行输入一个字符串):"); + for (int i = 0; i < m; i++) { + String line = scanner.nextLine(); + board[i] = line.toCharArray(); + } + + // 输入要查找的单词 + System.out.print("请输入要查找的单词:"); + String word = scanner.nextLine(); + + // 创建 SearchExist79 对象并检查单词是否存在 + SearchExist79 solution = new SearchExist79(); + boolean exists = solution.exist(board, word); + + // 输出结果 + System.out.println("单词 " + word + (exists ? " 存在于网格中。" : " 不存在于网格中。")); + } + + /** + * 主方法,检查单词是否存在于网格中 + * + * 算法思路: + * 使用回溯算法(深度优先搜索)在网格中搜索单词 + * 1. 遍历网格中的每个单元格作为起始点 + * 2. 从每个起始点开始进行深度优先搜索 + * 3. 搜索过程中标记已访问的单元格避免重复使用 + * 4. 如果找到完整单词返回true,否则继续搜索 + * + * @param board 二维字符网格 + * @param word 要查找的单词 + * @return 如果单词存在于网格中返回true,否则返回false + */ + public boolean exist(char[][] board, String word) { + // 初始化成员变量 + this.board = board; + this.word = word; + this.rows = board.length; + this.cols = board[0].length; + + // 遍历每个单元格作为起始点 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (dfs(i, j, 0)) { // 从当前位置开始 DFS 搜索 + return true; // 找到单词 + } + } + } + return false; // 未找到单词 + } + + /** + * 深度优先搜索方法 + * + * 算法思路: + * 从指定位置开始深度优先搜索,匹配单词的剩余部分 + * + * @param i 当前行索引 + * @param j 当前列索引 + * @param index 当前匹配的单词字符索引 + * @return 如果从当前位置能匹配剩余单词返回true,否则返回false + */ + private boolean dfs(int i, int j, int index) { + // 递归终止条件:已匹配完整个单词 + if (index == word.length()) { + return true; + } + + // 边界检查和字符匹配检查 + if (i < 0 || i >= rows || j < 0 || j >= cols || board[i][j] != word.charAt(index)) { + return false; + } + + // 标记当前单元格为已访问 + char temp = board[i][j]; + board[i][j] = '#'; // 使用特殊字符标记已访问单元格 + + // 向四个方向递归搜索 + boolean found = dfs(i + 1, j, index + 1) || // 向下搜索 + dfs(i - 1, j, index + 1) || // 向上搜索 + dfs(i, j + 1, index + 1) || // 向右搜索 + dfs(i, j - 1, index + 1); // 向左搜索 + + // 回溯:恢复当前单元格的字符 + board[i][j] = temp; + return found; + } + + /** + * 方法2:使用额外的visited数组记录访问状态 + * + * 算法思路: + * 使用额外的布尔数组记录访问状态,避免修改原数组 + * + * @param board 二维字符网格 + * @param word 要查找的单词 + * @return 如果单词存在于网格中返回true,否则返回false + */ + public boolean existWithVisited(char[][] board, String word) { + // 边界条件检查 + if (board == null || board.length == 0 || word == null) { + return false; + } + + // 初始化网格尺寸和访问状态数组 + int rows = board.length; + int cols = board[0].length; + boolean[][] visited = new boolean[rows][cols]; + + // 遍历每个单元格作为起始点 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + if (dfsWithVisited(board, word, 0, i, j, visited)) { + return true; + } + } + } + + return false; + } + + /** + * 使用visited数组的DFS方法 + * + * 算法思路: + * 使用visited数组记录访问状态的深度优先搜索实现 + * + * @param board 二维字符网格 + * @param word 要查找的单词 + * @param index 当前匹配的单词字符索引 + * @param i 当前行索引 + * @param j 当前列索引 + * @param visited 访问状态数组 + * @return 如果从当前位置能匹配剩余单词返回true,否则返回false + */ + private boolean dfsWithVisited(char[][] board, String word, int index, int i, int j, 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 found = dfsWithVisited(board, word, index + 1, i + 1, j, visited) || + dfsWithVisited(board, word, index + 1, i - 1, j, visited) || + dfsWithVisited(board, word, index + 1, i, j + 1, visited) || + dfsWithVisited(board, word, index + 1, i, j - 1, visited); + + // 回溯:标记当前位置为未访问 + visited[i][j] = false; + + return found; + } +} diff --git a/algorithm/SearchInsert35.java b/algorithm/SearchInsert35.java new file mode 100644 index 0000000000000..c02f3d9534ed9 --- /dev/null +++ b/algorithm/SearchInsert35.java @@ -0,0 +1,234 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 搜索插入位置(LeetCode 35) + * + * 时间复杂度:O(log n) + * - 使用二分查找,每次将搜索范围减半 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class SearchInsert35 { + + /** + * 主函数:处理用户输入并计算搜索插入位置 + * + * 算法流程: + * 1. 读取用户输入的排序数组和目标值 + * 2. 调用[searchInsert](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/SearchInsert35.java#L107-L137)方法查找目标值或插入位置 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + // Scanner scanner = new Scanner(System.in) 创建Scanner对象用于读取输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入排序数组 + // System.out.print("请输入排序数组(用空格分隔):") 打印输入提示 + System.out.print("请输入排序数组(用空格分隔):"); + // String line = scanner.nextLine() 读取用户输入的一行 + String line = scanner.nextLine(); + // String[] strArray = line.split(" ") 按空格分割字符串 + String[] strArray = line.split(" "); + + // 将字符串数组转换为整数数组 + // int[] nums = new int[strArray.length] 创建整数数组 + int[] nums = new int[strArray.length]; + // for (int i = 0; i < strArray.length; i++) 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // nums[i] = Integer.parseInt(strArray[i]) 转换字符串为整数 + nums[i] = Integer.parseInt(strArray[i]); // 转换字符串为整数 + } + + // 提示用户输入目标值 + // System.out.print("请输入目标值:") 打印目标值输入提示 + System.out.print("请输入目标值:"); + // int target = scanner.nextInt() 读取目标值 + int target = scanner.nextInt(); + + // 调用 searchInsert 方法查找目标值或插入位置 + // int result = searchInsert(nums, target) 调用searchInsert方法计算结果 + int result = searchInsert(nums, target); + + // 输出结果 + // System.out.println("目标值 " + target + " 的索引为:" + result) 打印结果 + System.out.println("目标值 " + target + " 的索引为:" + result); + } + + /** + * 在排序数组中查找目标值或插入位置 + * + * 算法思路: + * 使用二分查找,在有序数组中查找目标值 + * 如果找到目标值,返回其索引 + * 如果未找到,返回其应该插入的位置以保持数组有序 + * + * 执行过程分析(以`nums=[1,3,5,6]`, `target=5`为例): + * + * 初始状态: + * left = 0, right = 3 + * + * 第一次查找: + * mid = 0 + (3-0)/2 = 1 + * nums[1] = 3 < target=5 + * left = mid + 1 = 2 + * + * 第二次查找: + * mid = 2 + (3-2)/2 = 2 + * nums[2] = 5 == target=5 + * 返回索引2 + * + * 执行过程分析(以`nums=[1,3,5,6]`, `target=2`为例): + * + * 初始状态: + * left = 0, right = 3 + * + * 第一次查找: + * mid = 0 + (3-0)/2 = 1 + * nums[1] = 3 > target=2 + * right = mid - 1 = 0 + * + * 第二次查找: + * mid = 0 + (0-0)/2 = 0 + * nums[0] = 1 < target=2 + * left = mid + 1 = 1 + * + * 循环结束:left=1 > right=0 + * 返回left=1(插入位置) + * + * 执行过程分析(以`nums=[1,3,5,6]`, `target=7`为例): + * + * 初始状态: + * left = 0, right = 3 + * + * 第一次查找: + * mid = 0 + (3-0)/2 = 1 + * nums[1] = 3 < target=7 + * left = mid + 1 = 2 + * + * 第二次查找: + * mid = 2 + (3-2)/2 = 2 + * nums[2] = 5 < target=7 + * left = mid + 1 = 3 + * + * 第三次查找: + * mid = 3 + (3-3)/2 = 3 + * nums[3] = 6 < target=7 + * left = mid + 1 = 4 + * + * 循环结束:left=4 > right=3 + * 返回left=4(插入位置,数组末尾) + * + * 时间复杂度分析: + * - 二分查找:O(log n) + * - 每次迭代操作:O(1) + * - 总时间复杂度:O(log n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 升序排序的无重复元素数组 + * @param target 目标值 + * @return 目标值的索引或应该插入的位置 + */ + public static int searchInsert(int[] nums, int target) { + // 左指针,指向搜索范围的左边界 + // int left = 0 初始化左指针 + int left = 0; + // 右指针,指向搜索范围的右边界 + // int right = nums.length - 1 初始化右指针 + int right = nums.length - 1; + + // 二分查找 + // 当left <= right时继续查找 + // while (left <= right) 当左指针小于等于右指针时循环 + while (left <= right) { + // 计算中间索引,避免整数溢出 + // int mid = left + (right - left) / 2 计算中间索引 + int mid = left + (right - left) / 2; + + // 找到目标值,直接返回索引 + // if (nums[mid] == target) 检查中间元素是否等于目标值 + if (nums[mid] == target) { + // return mid 返回目标值索引 + return mid; + } + // 中间值小于目标值,向右半部分查找 + // else if (nums[mid] < target) 检查中间元素是否小于目标值 + else if (nums[mid] < target) { + // left = mid + 1 更新左指针 + left = mid + 1; + } + // 中间值大于目标值,向左半部分查找 + else { + // right = mid - 1 更新右指针 + right = mid - 1; + } + } + + // 如果未找到目标值,返回插入位置 + // 此时left即为插入位置,可以保持数组有序 + // return left 返回插入位置 + return left; + } + + + + /** + * 方法2:线性搜索解法(仅供对比) + * + * 算法思路: + * 从左到右遍历数组,找到第一个大于等于目标值的元素位置 + * + * 时间复杂度分析: + * - 遍历数组:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 升序排序的无重复元素数组 + * @param target 目标值 + * @return 目标值的索引或应该插入的位置 + */ + public int searchInsertLinear(int[] nums, int target) { + // for (int i = 0; i < nums.length; i++) 遍历数组 + for (int i = 0; i < nums.length; i++) { + // if (nums[i] >= target) 检查当前元素是否大于等于目标值 + if (nums[i] >= target) { + // return i 返回索引 + return i; + } + } + // return nums.length 返回数组长度作为插入位置 + return nums.length; + } + + /** + * 方法3:使用Java内置二分查找 + * + * 算法思路: + * 使用Java标准库的二分查找方法 + * + * 时间复杂度分析: + * - 二分查找:O(log n) + * - 总时间复杂度:O(log n) + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param nums 升序排序的无重复元素数组 + * @param target 目标值 + * @return 目标值的索引或应该插入的位置 + */ + public int searchInsertBuiltIn(int[] nums, int target) { + // int index = Arrays.binarySearch(nums, target) 调用Java内置二分查找 + int index = Arrays.binarySearch(nums, target); + // return index >= 0 ? index : -index - 1 根据返回值确定索引或插入位置 + return index >= 0 ? index : -index - 1; + } +} diff --git a/algorithm/SearchMatrix74.java b/algorithm/SearchMatrix74.java new file mode 100644 index 0000000000000..d923b92685b72 --- /dev/null +++ b/algorithm/SearchMatrix74.java @@ -0,0 +1,201 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 搜索二维矩阵(LeetCode 74) + * + * 时间复杂度:O(log(m*n)) + * - 将二维矩阵视为一维数组进行二分查找 + * - m是行数,n是列数 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class SearchMatrix74 { + + /** + * 主函数:处理用户输入并判断目标值是否在矩阵中 + * + * 算法流程: + * 1. 读取用户输入的矩阵和目标值 + * 2. 调用searchMatrix方法判断目标值是否在矩阵中 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入矩阵的行数和列数 + System.out.print("请输入矩阵的行数 m 和列数 n(用空格分隔):"); + int m = scanner.nextInt(); + int n = scanner.nextInt(); + + // 创建矩阵 + int[][] matrix = new int[m][n]; + System.out.println("请输入矩阵元素(每行元素用空格分隔):"); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + matrix[i][j] = scanner.nextInt(); // 读取矩阵元素 + } + } + + // 提示用户输入目标值 + System.out.print("请输入目标值:"); + int target = scanner.nextInt(); + + // 调用 searchMatrix 方法判断目标值是否在矩阵中 + boolean result = searchMatrix(matrix, target); + + // 输出结果 + System.out.println("目标值 " + target + " 在矩阵中:" + result); + } + + /** + * 在二维矩阵中搜索目标值 + * + * 算法思路: + * 将二维矩阵视为一维有序数组,使用二分查找 + * 关键在于坐标转换: + * - 一维索引index对应的二维坐标为:(index / cols, index % cols) + * - 二维坐标(row, col)对应的一维索引为:row * cols + col + * + * @param matrix m×n的二维矩阵,每行从左到右升序排列,下一行第一个元素大于上一行最后一个元素 + * @param target 目标值 + * @return 如果目标值在矩阵中返回true,否则返回false + */ + public static boolean searchMatrix(int[][] matrix, int target) { + // 确保矩阵非空 + // 这是重要的边界条件检查 + if (matrix.length == 0 || matrix[0].length == 0) { + return false; // 如果矩阵为空,返回 false + } + + // 获取矩阵的行数和列数 + int rows = matrix.length; // 行数 + int cols = matrix[0].length; // 列数 + + // 设置二分查找的边界 + int left = 0; // 左边界(一维索引) + int right = rows * cols - 1; // 右边界(一维索引) + + // 使用二分查找 + while (left <= right) { + // 计算中间索引,避免整数溢出 + int mid = left + (right - left) / 2; + + // 将一维索引转换为二维坐标 + // mid / cols:行索引 + // mid % cols:列索引 + int midValue = matrix[mid / cols][mid % cols]; + + // 找到目标值,返回 true + if (midValue == target) { + return true; + } + // 中间值小于目标值,向右半部分查找 + else if (midValue < target) { + left = mid + 1; + } + // 中间值大于目标值,向左半部分查找 + else { + right = mid - 1; + } + } + + // 如果未找到目标值,返回 false + return false; + } + + /** + * 方法2:两次二分查找解法 + * + * 算法思路: + * 1. 先在第一列中二分查找确定目标值可能在哪一行 + * 2. 再在该行中二分查找目标值 + * + * @param matrix m×n的二维矩阵 + * @param target 目标值 + * @return 如果目标值在矩阵中返回true,否则返回false + */ + public boolean searchMatrixTwoBinarySearch(int[][] matrix, int target) { + if (matrix.length == 0 || matrix[0].length == 0) { + return false; + } + + int rows = matrix.length; + int cols = matrix[0].length; + + // 第一次二分查找:确定行 + int top = 0; + int bottom = rows - 1; + + while (top <= bottom) { + int mid = top + (bottom - top) / 2; + if (matrix[mid][0] == target) { + return true; + } else if (matrix[mid][0] < target) { + top = mid + 1; + } else { + bottom = mid - 1; + } + } + + // 如果目标值小于第一行第一个元素 + if (bottom < 0) { + return false; + } + + // 第二次二分查找:在确定的行中查找 + int row = bottom; + int left = 0; + int right = cols - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + if (matrix[row][mid] == target) { + return true; + } else if (matrix[row][mid] < target) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return false; + } + + /** + * 方法3:从右上角开始搜索 + * + * 算法思路: + * 从矩阵右上角开始,利用矩阵的有序性质: + * - 如果当前值小于目标值,向下移动 + * - 如果当前值大于目标值,向左移动 + * + * @param matrix m×n的二维矩阵 + * @param target 目标值 + * @return 如果目标值在矩阵中返回true,否则返回false + */ + public boolean searchMatrixFromCorner(int[][] matrix, int target) { + if (matrix.length == 0 || matrix[0].length == 0) { + return false; + } + + int row = 0; + int col = matrix[0].length - 1; + + while (row < matrix.length && col >= 0) { + if (matrix[row][col] == target) { + return true; + } else if (matrix[row][col] < target) { + row++; // 向下移动 + } else { + col--; // 向左移动 + } + } + + return false; + } +} diff --git a/algorithm/SearchRange34.java b/algorithm/SearchRange34.java new file mode 100644 index 0000000000000..8aec499db5427 --- /dev/null +++ b/algorithm/SearchRange34.java @@ -0,0 +1,282 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 在排序数组中查找元素的第一个和最后一个位置(LeetCode 34) + * + * 时间复杂度:O(log n) + * - 需要进行两次二分查找 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + */ +public class SearchRange34 { + + /** + * 查找目标值在排序数组中的起始和结束位置 + * + * 算法思路: + * 使用两次二分查找分别找到目标值的左边界和右边界 + * 1. 第一次二分查找找到最左边的目标值位置(左边界) + * 2. 第二次二分查找找到最右边的目标值位置(右边界) + * + * @param nums 升序排序的整数数组 + * @param target 目标值 + * @return 目标值的起始和结束位置,如果不存在返回[-1,-1] + */ + public int[] searchRange(int[] nums, int target) { + // 边界情况:空数组 + if (nums == null || nums.length == 0) { + return new int[]{-1, -1}; + } + + // 查找左边界 + int leftBound = findLeftBound(nums, target); + // 如果左边界不存在,说明目标值不存在 + if (leftBound == -1) { + return new int[]{-1, -1}; + } + + // 查找右边界 + int rightBound = findRightBound(nums, target); + + // 返回目标值范围 + return new int[]{leftBound, rightBound}; + } + + /** + * 查找目标值的左边界(第一次出现的位置) + * + * 算法思路: + * 使用二分查找找到目标值第一次出现的位置 + * + * @param nums 升序排序的整数数组 + * @param target 目标值 + * @return 左边界索引,如果不存在返回-1 + */ + private int findLeftBound(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) { + // 找到目标值,但继续向左查找是否有更左边的 + right = mid - 1; + } else if (nums[mid] < target) { + // 更新左指针 + left = mid + 1; + } else { + // 更新右指针 + right = mid - 1; + } + } + + // 检查是否找到目标值 + if (left < nums.length && nums[left] == target) { + // 返回左边界索引 + return left; + } + // 返回-1 + return -1; + } + + /** + * 查找目标值的右边界(最后一次出现的位置) + * + * 算法思路: + * 使用二分查找找到目标值最后一次出现的位置 + * + * @param nums 升序排序的整数数组 + * @param target 目标值 + * @return 右边界索引 + */ + private int findRightBound(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) { + // 找到目标值,但继续向右查找是否有更右边的 + left = mid + 1; + } else if (nums[mid] < target) { + // 更新左指针 + left = mid + 1; + } else { + // 更新右指针 + right = mid - 1; + } + } + + // 返回右边界(此时right指向最后一次出现的位置) + return right; + } + + /** + * 方法2:使用Java内置二分查找 + * + * 算法思路: + * 使用Java标准库的二分查找方法,然后向两边扩展找到边界 + * + * @param nums 升序排序的整数数组 + * @param target 目标值 + * @return 目标值的起始和结束位置,如果不存在返回[-1,-1] + */ + public int[] searchRangeBuiltIn(int[] nums, int target) { + // 检查数组是否为空 + if (nums == null || nums.length == 0) { + // 返回[-1,-1] + return new int[]{-1, -1}; + } + + // 调用Java内置二分查找 + int leftIndex = Arrays.binarySearch(nums, target); + // 检查是否找到目标值 + if (leftIndex < 0) { + // 返回[-1,-1] + return new int[]{-1, -1}; + } + + // 向左扩展找到起始位置 + while (leftIndex > 0 && nums[leftIndex - 1] == target) { + // 更新起始位置 + leftIndex--; + } + + // 初始化结束位置 + int rightIndex = leftIndex; + // 向右扩展找到结束位置 + while (rightIndex < nums.length - 1 && nums[rightIndex + 1] == target) { + // 更新结束位置 + rightIndex++; + } + + // 返回目标值范围 + return new int[]{leftIndex, rightIndex}; + } + + /** + * 方法3:一次二分查找后向两边扩展 + * + * 算法思路: + * 先用二分查找找到目标值,然后向两边扩展找到边界 + * + * @param nums 升序排序的整数数组 + * @param target 目标值 + * @return 目标值的起始和结束位置,如果不存在返回[-1,-1] + */ + public int[] searchRangeExpand(int[] nums, int target) { + // 检查数组是否为空 + if (nums == null || nums.length == 0) { + // 返回[-1,-1] + return new int[]{-1, -1}; + } + + // 调用Java内置二分查找 + int index = Arrays.binarySearch(nums, target); + // 检查是否找到目标值 + if (index < 0) { + // 返回[-1,-1] + return new int[]{-1, -1}; + } + + // 初始化起始位置 + int left = index; + // 向左扩展找到起始位置 + while (left > 0 && nums[left - 1] == target) { + // 更新起始位置 + left--; + } + + // 初始化结束位置 + int right = index; + // 向右扩展找到结束位置 + while (right < nums.length - 1 && nums[right + 1] == target) { + // 更新结束位置 + right++; + } + + // 返回目标值范围 + return new int[]{left, right}; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + // 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // 打印提示信息 + System.out.println("请输入排序数组(用空格分隔):"); + // 读取输入 + String input = scanner.nextLine(); + // 分割字符串 + String[] strArray = input.split(" "); + + // 创建整数数组 + int[] nums = new int[strArray.length]; + // 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // 转换为整数 + nums[i] = Integer.parseInt(strArray[i]); + } + + // 返回整数数组 + return nums; + } + + /** + * 主函数:处理用户输入并查找目标值的范围 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("在排序数组中查找元素的第一个和最后一个位置"); + + // 调用readArray方法读取数组 + int[] nums = readArray(); + // 打印输入数组 + System.out.println("输入数组: " + Arrays.toString(nums)); + + // 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // 打印提示信息 + System.out.print("请输入目标值: "); + // 读取目标值 + int target = scanner.nextInt(); + + // 查找目标值范围 + // 创建SearchRange34对象 + SearchRange34 solution = new SearchRange34(); + // 调用searchRange方法 + int[] result1 = solution.searchRange(nums, target); + // 调用searchRangeBuiltIn方法 + int[] result2 = solution.searchRangeBuiltIn(nums, target); + // 调用searchRangeExpand方法 + int[] result3 = solution.searchRangeExpand(nums, target); + + // 输出结果 + // 打印两次二分查找方法结果 + System.out.println("两次二分查找方法结果: " + Arrays.toString(result1)); + // 打印内置二分查找方法结果 + System.out.println("内置二分查找方法结果: " + Arrays.toString(result2)); + // 打印扩展查找方法结果 + System.out.println("扩展查找方法结果: " + Arrays.toString(result3)); + } +} diff --git a/algorithm/SearchTargetMatrix240.java b/algorithm/SearchTargetMatrix240.java new file mode 100644 index 0000000000000..ae5d920990793 --- /dev/null +++ b/algorithm/SearchTargetMatrix240.java @@ -0,0 +1,261 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 搜索二维矩阵 II(LeetCode 240) + * + * 时间复杂度:O(m + n) + * - 最多遍历 m 行和 n 列 + * - 每次比较后要么行增加,要么列减少 + * - 总的移动步数最多为 m + n + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用与输入矩阵大小相关的额外存储空间 + */ +public class SearchTargetMatrix240 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 输入矩阵的大小 + System.out.print("请输入矩阵的行数 m:"); + + // 读取行数 + int m = scanner.nextInt(); + + System.out.print("请输入矩阵的列数 n:"); + + // 读取列数 + int n = scanner.nextInt(); + + // 初始化矩阵 + int[][] matrix = new int[m][n]; + + // 提示用户输入矩阵元素 + System.out.println("请输入矩阵的元素:"); + + // 读取矩阵元素 + // 外层循环遍历行 + for (int i = 0; i < m; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + matrix[i][j] = scanner.nextInt(); + } + } + + // 输入目标值 + System.out.print("请输入目标值 target:"); + + // 读取目标值 + int target = scanner.nextInt(); + + // 调用 searchMatrix 方法查找目标值 + boolean result = searchMatrix(matrix, target); + + // 输出结果 + if (result) { + System.out.println("目标值存在于矩阵中。"); + } else { + System.out.println("目标值不存在于矩阵中。"); + } + } + + /** + * 在有序二维矩阵中搜索目标值 + * 矩阵满足以下条件: + * 1. 每行的元素从左到右升序排列 + * 2. 每列的元素从上到下升序排列 + * + * 算法思路: + * 从矩阵的右上角开始搜索,利用矩阵的有序性质进行剪枝 + * 1. 从右上角开始,该位置是当前行的最大值,当前列的最小值 + * 2. 如果当前元素等于目标值,找到目标 + * 3. 如果当前元素大于目标值,说明目标值不可能在当前列,向左移动 + * 4. 如果当前元素小于目标值,说明目标值不可能在当前行,向下移动 + * + * 示例过程(以矩阵 [[1,4,7,11],[2,5,8,12],[3,6,9,16],[10,13,14,17]],target=5 为例): + * + * 矩阵: + * [ 1 4 7 11] + * [ 2 5 8 12] + * [ 3 6 9 16] + * [10 13 14 17] + * + * 初始位置: row=0, col=3, matrix[0][3]=11 + * + * 步骤1: matrix[0][3]=11 > target=5,向左移动 + * row=0, col=2, matrix[0][2]=7 + * + * 步骤2: matrix[0][2]=7 > target=5,向左移动 + * row=0, col=1, matrix[0][1]=4 + * + * 步骤3: matrix[0][1]=4 < target=5,向下移动 + * row=1, col=1, matrix[1][1]=5 + * + * 步骤4: matrix[1][1]=5 = target=5,找到目标 + * + * 最终结果: true + * + * 时间复杂度分析: + * - 最多遍历 m 行和 n 列:O(m + n),其中m为行数,n为列数 + * - 每次比较后要么行增加,要么列减少 + * - 总的移动步数最多为 m + n + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * - 没有使用与输入矩阵大小相关的额外存储空间 + * + * @param matrix 输入的二维整数矩阵 + * @param target 要搜索的目标值 + * @return 如果目标值存在于矩阵中返回true,否则返回false + */ + public static 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++; + } + } + + // 如果遍历完整个矩阵仍未找到目标值,返回 false + return false; + } + /** + * 方法2:从左下角开始搜索 + * + * 算法思路: + * 从矩阵的左下角开始搜索,利用矩阵的有序性质进行剪枝 + * 1. 从左下角开始,该位置是当前列的最大值,当前行的最小值 + * 2. 如果当前元素等于目标值,找到目标 + * 3. 如果当前元素大于目标值,说明目标值不可能在当前行,向上移动 + * 4. 如果当前元素小于目标值,说明目标值不可能在当前列,向右移动 + * + * 示例过程(以矩阵 [[1,4,7,11],[2,5,8,12],[3,6,9,16],[10,13,14,17]],target=5 为例): + * + * 1. 初始化: row=3, col=0, matrix[3][0]=10 + * 2. matrix[3][0]=10 > target=5, 向上移动, row=2 + * 3. matrix[2][0]=3 < target=5, 向右移动, col=1 + * 4. matrix[2][1]=6 > target=5, 向上移动, row=1 + * 5. matrix[1][1]=5 = target=5, 找到目标, 返回true + * + * 时间复杂度分析: + * - 最多遍历 m 行和 n 列:O(m + n),其中m为行数,n为列数 + * - 每次比较后要么行减少,要么列增加 + * - 总的移动步数最多为 m + n + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * - 没有使用与输入矩阵大小相关的额外存储空间 + * + * @param matrix 输入的二维整数矩阵 + * @param target 要搜索的目标值 + * @return 如果目标值存在于矩阵中返回true,否则返回false + */ + public static boolean searchMatrixFromBottomLeft(int[][] matrix, int target) { + int m = matrix.length; + int n = matrix[0].length; + + // 从矩阵的左下角开始搜索 + int row = m - 1; // 初始行索引为最下行 + int col = 0; // 初始列索引为0 + + // 当行和列都在合理范围内时,进行搜索 + while (row >= 0 && col < n) { + if (matrix[row][col] == target) { + // 找到目标值 + return true; + } else if (matrix[row][col] > target) { + // 当前元素大于目标值,向上移动一行 + row--; + } else { + // 当前元素小于目标值,向右移动一列 + col++; + } + } + + // 如果遍历完整个矩阵仍未找到目标值,返回 false + return false; + } + + /** + * 方法3:二分搜索(对每行进行二分搜索) + * + * 算法思路: + * 对矩阵的每一行进行二分搜索 + * + * 示例过程(以矩阵 [[1,4,7,11],[2,5,8,12],[3,6,9,16],[10,13,14,17]],target=5 为例): + * + * 1. 处理第0行[1,4,7,11]: binarySearch([1,4,7,11], 5) = false + * 2. 处理第1行[2,5,8,12]: binarySearch([2,5,8,12], 5) = true, 返回true + * + * 时间复杂度分析: + * - 遍历m行:O(m),其中m为行数 + * - 每行二分搜索:O(log n),其中n为列数 + * - 总时间复杂度:O(m * log n) + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * + * @param matrix 输入的二维整数矩阵 + * @param target 要搜索的目标值 + * @return 如果目标值存在于矩阵中返回true,否则返回false + */ + public static boolean searchMatrixBinarySearch(int[][] matrix, int target) { + // 遍历每一行 + for (int[] row : matrix) { + // 对当前行进行二分搜索 + if (binarySearch(row, target)) { + return true; + } + } + return false; + } + + /** + * 二分搜索辅助方法 + * + * 时间复杂度分析: + * - 二分搜索:O(log n),其中n为数组长度 + * + * 空间复杂度分析: + * - 只使用了常数级别的额外空间:O(1) + * + * @param arr 有序数组 + * @param target 目标值 + * @return 如果目标值存在于数组中返回true,否则返回false + */ + private static boolean binarySearch(int[] arr, int target) { + int left = 0; + int right = arr.length - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + if (arr[mid] == target) { + return true; + } else if (arr[mid] < target) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return false; + } +} diff --git a/algorithm/SetZeroMatrix73.java b/algorithm/SetZeroMatrix73.java new file mode 100644 index 0000000000000..813225bf93bf6 --- /dev/null +++ b/algorithm/SetZeroMatrix73.java @@ -0,0 +1,329 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 矩阵置零(LeetCode 73) + * + * 时间复杂度:O(m × n) + * - 需要遍历矩阵多次,每次遍历O(m × n) + * - 总时间复杂度为O(m × n) + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 利用矩阵的第一行和第一列作为标记空间 + */ +public class SetZeroMatrix73 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 读取矩阵的大小 + System.out.print("请输入矩阵的行数 m 和列数 n:"); + + // 读取行数 + int m = scanner.nextInt(); + + // 读取列数 + int n = scanner.nextInt(); + + // 初始化矩阵 + int[][] matrix = new int[m][n]; + + // 提示用户输入矩阵元素 + System.out.println("请输入矩阵元素,每行用空格分隔:"); + + // 读取矩阵元素 + // 外层循环遍历行 + for (int i = 0; i < m; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + matrix[i][j] = scanner.nextInt(); + } + } + + // 调用 setZeroes 方法将矩阵中0元素所在的行和列置零 + setZeroes(matrix); + + // 输出结果矩阵 + System.out.println("处理后的矩阵:"); + + // 遍历并打印处理后的矩阵 + // 外层循环遍历行 + for (int i = 0; i < m; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + System.out.print(matrix[i][j] + " "); + } + System.out.println(); + } + } + + /** + * 矩阵置零 + * 如果矩阵中某个元素为0,则将其所在的行和列都置零 + * + * 算法思路: + * 使用矩阵的第一行和第一列作为标记空间,记录哪些行和列需要置零 + * 由于第一行和第一列本身可能包含0,需要额外变量记录它们是否需要置零 + * + * 示例过程(以矩阵 [[1,1,1],[1,0,1],[1,1,1]] 为例): + * + * 原矩阵: + * [1 1 1] + * [1 0 1] + * [1 1 1] + * + * 步骤1 - 检查第一行第一列是否包含0: + * firstRowZero = false, firstColZero = false + * + * 步骤2 - 利用第一行第一列标记需要置零的行列: + * 发现matrix[1][1]=0,标记matrix[1][0]=0, matrix[0][1]=0 + * [1 0 1] + * [0 0 1] + * [1 1 1] + * + * 步骤3 - 根据标记置零: + * matrix[1][2]=0 (因为matrix[1][0]=0) + * matrix[2][1]=0 (因为matrix[0][1]=0) + * [1 0 1] + * [0 0 0] + * [1 0 1] + * + * 步骤4-5 - 处理第一行第一列: + * firstRowZero=false, firstColZero=false,不处理 + * + * 最终结果: + * [1 0 1] + * [0 0 0] + * [1 0 1] + * + * 时间复杂度分析: + * - 检查第一行和第一列:O(m + n),其中m为行数,n为列数 + * - 标记需要置零的行列:O(m × n) + * - 根据标记置零:O(m × n) + * - 处理第一行和第一列:O(m + n) + * - 总时间复杂度:O(m × n) + * + * 空间复杂度分析: + * - 只使用了两个布尔变量:O(1) + * - 利用原矩阵的第一行和第一列作为标记空间:O(1) + * - 总空间复杂度:O(1) + * + * @param matrix 输入的二维整数矩阵 + */ + public static void setZeroes(int[][] matrix) { + // 获取矩阵的行数和列数 + int m = matrix.length; + int n = matrix[0].length; + + // 记录第一行是否需要置零 + boolean firstRowZero = false; + + // 记录第一列是否需要置零 + boolean firstColZero = false; + + // 1. 判断第一行是否有 0 + // 遍历第一行的所有列 + for (int j = 0; j < n; j++) { + if (matrix[0][j] == 0) { + firstRowZero = true; + break; + } + } + + // 1. 判断第一列是否有 0 + // 遍历第一列的所有行 + for (int i = 0; i < m; i++) { + if (matrix[i][0] == 0) { + firstColZero = true; + break; + } + } + + // 2. 利用第一行和第一列标记需要置零的行和列 + // 从第二行第二列开始遍历(索引从1开始) + // 外层循环遍历行(从索引1开始) + for (int i = 1; i < m; i++) { + // 内层循环遍历列(从索引1开始) + for (int j = 1; j < n; j++) { + // 如果当前元素为0 + if (matrix[i][j] == 0) { + // 在第一行标记该列需要置零 + matrix[i][0] = 0; + // 在第一列标记该行需要置零 + matrix[0][j] = 0; + } + } + } + + // 3. 根据标记置零(从第二行、第二列开始) + // 外层循环遍历行(从索引1开始) + for (int i = 1; i < m; i++) { + // 内层循环遍历列(从索引1开始) + for (int j = 1; j < n; j++) { + // 如果该行或该列需要置零 + if (matrix[i][0] == 0 || matrix[0][j] == 0) { + // 将当前元素置零 + matrix[i][j] = 0; + } + } + } + + // 4. 处理第一列 + if (firstColZero) { + // 遍历所有行,将第一列元素置零 + for (int i = 0; i < m; i++) { + matrix[i][0] = 0; + } + } + + // 5. 处理第一行 + if (firstRowZero) { + // 遍历所有列,将第一行元素置零 + for (int j = 0; j < n; j++) { + matrix[0][j] = 0; + } + } + } + + /** + * 方法2:使用额外空间的解法 + * + * 算法思路: + * 使用额外的布尔数组记录哪些行和列需要置零 + * + * 示例过程(以矩阵 [[1,1,1],[1,0,1],[1,1,1]] 为例): + * + * 1. 初始化: m=3, n=3 + * rowZero = [false, false, false] + * colZero = [false, false, false] + * + * 2. 标记包含0的行列: + * matrix[1][1]=0: rowZero[1]=true, colZero[1]=true + * rowZero = [false, true, false] + * colZero = [false, true, false] + * + * 3. 根据标记置零: + * i=1: rowZero[1]=true, 第1行全部置零 + * j=1: colZero[1]=true, 第1列全部置零 + * + * 4. 最终结果: + * [1 0 1] + * [0 0 0] + * [1 0 1] + * + * 时间复杂度分析: + * - 标记包含0的行和列:O(m × n),其中m为行数,n为列数 + * - 根据标记置零:O(m × n) + * - 总时间复杂度:O(m × n) + * + * 空间复杂度分析: + * - 行标记数组:O(m) + * - 列标记数组:O(n) + * - 总空间复杂度:O(m + n) + * + * @param matrix 输入的二维整数矩阵 + */ + public static void setZeroesExtraSpace(int[][] matrix) { + int m = matrix.length; + int n = matrix[0].length; + + // 创建行和列的标记数组 + boolean[] rowZero = new boolean[m]; + boolean[] colZero = new boolean[n]; + + // 标记包含0的行和列 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (matrix[i][j] == 0) { + rowZero[i] = true; + colZero[j] = true; + } + } + } + + // 根据标记置零 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (rowZero[i] || colZero[j]) { + matrix[i][j] = 0; + } + } + } + } + + /** + * 方法3:暴力解法(仅供对比) + * + * 算法思路: + * 创建矩阵副本,遍历原矩阵找到0元素,然后在副本中置零对应行列 + * + * 示例过程(以矩阵 [[1,1,1],[1,0,1],[1,1,1]] 为例): + * + * 1. 创建副本: copy = [[1,1,1],[1,0,1],[1,1,1]] + * + * 2. 遍历原矩阵处理0元素: + * i=0,j=0: matrix[0][0]=1, 跳过 + * i=0,j=1: matrix[0][1]=1, 跳过 + * i=0,j=2: matrix[0][2]=1, 跳过 + * i=1,j=0: matrix[1][0]=1, 跳过 + * i=1,j=1: matrix[1][1]=0, 置零第1行和第1列 + * copy[1][0]=0, copy[1][1]=0, copy[1][2]=0 + * copy[0][1]=0, copy[1][1]=0, copy[2][1]=0 + * copy = [[1,0,1],[0,0,0],[1,0,1]] + * i=1,j=2: matrix[1][2]=1, 跳过 + * i=2,j=0: matrix[2][0]=1, 跳过 + * i=2,j=1: matrix[2][1]=1, 跳过 + * i=2,j=2: matrix[2][2]=1, 跳过 + * + * 3. 复制回原矩阵: matrix = [[1,0,1],[0,0,0],[1,0,1]] + * + * 时间复杂度分析: + * - 创建副本:O(m × n),其中m为行数,n为列数 + * - 遍历原矩阵并置零:O(m × n × (m + n)) + * - 复制回原矩阵:O(m × n) + * - 总时间复杂度:O(m × n × (m + n)) + * + * 空间复杂度分析: + * - 矩阵副本:O(m × n) + * - 总空间复杂度:O(m × n) + * + * @param matrix 输入的二维整数矩阵 + */ + public static void setZeroesBruteForce(int[][] matrix) { + int m = matrix.length; + int n = matrix[0].length; + + // 创建矩阵副本 + int[][] copy = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + copy[i][j] = matrix[i][j]; + } + } + + // 遍历原矩阵 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (matrix[i][j] == 0) { + // 置零第i行 + for (int k = 0; k < n; k++) { + copy[i][k] = 0; + } + // 置零第j列 + for (int k = 0; k < m; k++) { + copy[k][j] = 0; + } + } + } + } + + // 将结果复制回原矩阵 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + matrix[i][j] = copy[i][j]; + } + } + } +} diff --git a/algorithm/SingleNumber136.java b/algorithm/SingleNumber136.java new file mode 100644 index 0000000000000..4fc8acecd190e --- /dev/null +++ b/algorithm/SingleNumber136.java @@ -0,0 +1,202 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.HashSet; +import java.util.Set; + +/** + * 只出现一次的数字(LeetCode 136) + * + * 时间复杂度: + * - 方法1(异或运算):O(n) + * 需要遍历数组一次 + * - 方法2(哈希表):O(n) + * 需要遍历数组一次,哈希表操作平均O(1) + * - 方法3(排序):O(n log n) + * 排序需要O(n log n)时间 + * + * 空间复杂度: + * - 方法1(异或运算):O(1) + * 只使用常数个额外变量 + * - 方法2(哈希表):O(n) + * 需要哈希表存储元素计数 + * - 方法3(排序):O(1) + * 如果允许修改原数组,则为O(1);否则需要O(n)空间复制数组 + */ +public class SingleNumber136 { + + /** + * 主函数:处理用户输入并找出只出现一次的元素 + * + * 算法流程: + * 1. 读取用户输入的整数数组 + * 2. 调用[singleNumber](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/SingleNumber136.java#L139-L152)方法找出只出现一次的元素 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入整数数组 + System.out.print("请输入整数数组(用空格分隔):"); + String line = scanner.nextLine(); + String[] strArray = line.split(" "); + + // 将字符串数组转换为整数数组 + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); // 转换字符串为整数 + } + + // 保存原始数组用于输出 + int[] original = Arrays.copyOf(nums, nums.length); + System.out.println("输入的数组: " + Arrays.toString(original)); + + // 调用不同方法找出只出现一次的元素 + SingleNumber136 solution = new SingleNumber136(); + int result1 = solution.singleNumber(nums); // 异或方法 + int result2 = solution.singleNumberHashMap(nums); // 哈希表方法 + int result3 = solution.singleNumberSort(nums); // 排序方法 + + // 输出结果 + System.out.println("异或方法结果: " + result1); + System.out.println("哈希表方法结果: " + result2); + System.out.println("排序方法结果: " + result3); + } + + /** + * 方法1:异或运算解法(最优解) + * + * 算法思路: + * 利用异或运算的性质: + * 1. a ^ a = 0(相同数字异或为0) + * 2. a ^ 0 = a(任何数字与0异或等于自身) + * 3. 异或运算满足交换律和结合律 + * + * 因为除了一个元素外,其他元素都出现两次, + * 所以将所有元素进行异或运算,成对出现的元素会相互抵消为0, + * 最终只剩下只出现一次的元素 + * + * 执行过程分析(以数组 [4,1,2,1,2] 为例): + * + * 初始状态:unique = 0 + * + * 遍历过程: + * 1. unique = 0 ^ 4 = 4 + * 2. unique = 4 ^ 1 = 5 + * 3. unique = 5 ^ 2 = 7 + * 4. unique = 7 ^ 1 = 6 + * 5. unique = 6 ^ 2 = 4 + * + * 最终结果:unique = 4 + * + * 二进制分析: + * 4 = 100, 1 = 001, 2 = 010 + * 000 ^ 100 = 100 (4) + * 100 ^ 001 = 101 (5) + * 101 ^ 010 = 111 (7) + * 111 ^ 001 = 110 (6) + * 110 ^ 010 = 100 (4) + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * - 异或运算:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用一个额外变量:O(1) + * + * @param nums 整数数组,除了一个元素只出现一次外,其他元素都出现两次 + * @return 只出现一次的元素 + */ + public static int singleNumber(int[] nums) { + int unique = 0; // 初始化唯一元素的变量 + + // 遍历数组,进行异或运算 + for (int num : nums) { + unique ^= num; // 异或运算 + } + + // 返回只出现一次的元素 + return unique; + } + + /** + * 方法2:哈希表解法 + * + * 算法思路: + * 使用哈希表统计每个元素出现的次数,然后找出出现次数为1的元素 + * + * 时间复杂度分析: + * - 遍历数组统计次数:O(n) + * - 遍历哈希表查找结果:O(n) + * - 哈希表操作平均时间复杂度:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 哈希表存储元素计数:O(n) + * - 最坏情况下需要存储所有不同元素:O(n) + * + * @param nums 整数数组 + * @return 只出现一次的元素 + */ + public int singleNumberHashMap(int[] nums) { + Map countMap = new HashMap<>(); + + // 统计每个元素的出现次数 + for (int num : nums) { + countMap.put(num, countMap.getOrDefault(num, 0) + 1); + } + + // 找出出现次数为1的元素 + for (Map.Entry entry : countMap.entrySet()) { + if (entry.getValue() == 1) { + return entry.getKey(); + } + } + + // 根据题目保证,一定存在只出现一次的元素 + return -1; + } + + /** + * 方法3:排序解法 + * + * 算法思路: + * 对数组进行排序,相同的元素会相邻,只出现一次的元素必然与相邻元素不同 + * + * 时间复杂度分析: + * - 排序操作:O(n log n) + * - 遍历查找:O(n) + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 如果允许修改原数组,则为O(1) + * - 否则需要O(n)空间复制数组 + * - 排序算法本身可能需要额外空间,取决于具体实现 + * + * @param nums 整数数组 + * @return 只出现一次的元素 + */ + public int singleNumberSort(int[] nums) { + Arrays.sort(nums); + + // 检查第一个元素 + if (nums.length == 1 || nums[0] != nums[1]) { + return nums[0]; + } + + // 检查中间元素 + for (int i = 1; i < nums.length - 1; i++) { + if (nums[i] != nums[i-1] && nums[i] != nums[i+1]) { + return nums[i]; + } + } + + // 检查最后一个元素 + return nums[nums.length - 1]; + } +} diff --git a/algorithm/Solution.java b/algorithm/Solution.java new file mode 100644 index 0000000000000..d05bf6b9df177 --- /dev/null +++ b/algorithm/Solution.java @@ -0,0 +1,12 @@ +package com.funian.algorithm.algorithm; + +import java.util.*; + +public class Solution { + public static void main(String[] args) { + System.out.println("Hello World!"); + } + + + +} \ No newline at end of file diff --git a/algorithm/SolveNQueues51.java b/algorithm/SolveNQueues51.java new file mode 100644 index 0000000000000..8f802cfd0e933 --- /dev/null +++ b/algorithm/SolveNQueues51.java @@ -0,0 +1,314 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * N皇后(LeetCode 51) + * + * 时间复杂度:O(N!) + * - 第一行有N种选择,第二行最多N-1种选择,以此类推 + * + * 空间复杂度:O(N) + * - 递归调用栈深度为N + * - 需要额外的数组存储皇后位置和棋盘状态 + */ +public class SolveNQueues51 { + + /** + * 解决N皇后问题 + * + * 算法思路: + * 使用回溯算法,逐行放置皇后 + * 1. 使用三个布尔数组记录列、主对角线、副对角线的占用情况 + * 2. 递归在每一行尝试放置皇后 + * 3. 检查当前位置是否与已放置的皇后冲突 + * 4. 找到解后构造棋盘并添加到结果中 + * + * @param n 皇后数量和棋盘大小 + * @return 所有解决方案的棋盘表示 + */ + public List> solveNQueens(int n) { + // 创建结果列表 + List> result = new ArrayList<>(); + // 记录每行皇后的列位置 + int[] queens = new int[n]; + // 记录列的占用情况 + boolean[] columns = new boolean[n]; + // 记录主对角线的占用情况(row - col + n - 1) + boolean[] diagonals1 = new boolean[2 * n - 1]; + // 记录副对角线的占用情况(row + col) + boolean[] diagonals2 = new boolean[2 * n - 1]; + + // 开始回溯 + backtrack(0, n, queens, columns, diagonals1, diagonals2, result); + + // 返回结果 + return result; + } + + /** + * 回溯辅助方法 + * + * 算法思路: + * 递归地在每一行尝试放置皇后,并检查冲突 + * + * @param row 当前处理的行 + * @param n 棋盘大小 + * @param queens 记录每行皇后的列位置 + * @param columns 列占用情况 + * @param diagonals1 主对角线占用情况 + * @param diagonals2 副对角线占用情况 + * @param result 存储所有解决方案的结果列表 + */ + private void backtrack(int row, int n, int[] queens, boolean[] columns, + boolean[] diagonals1, boolean[] diagonals2, + List> result) { + // 递归终止条件:所有行都已放置皇后 + if (row == n) { + // 构造棋盘并添加到结果中 + result.add(generateBoard(queens, n)); + // 返回 + return; + } + + // 在当前行尝试每个列位置 + for (int col = 0; col < n; col++) { + // 检查当前位置是否与已放置的皇后冲突 + // 列冲突检查 + if (columns[col]) continue; + // 主对角线冲突检查(row - col为常数) + if (diagonals1[row - col + n - 1]) continue; + // 副对角线冲突检查(row + col为常数) + if (diagonals2[row + col]) continue; + + // 做选择:在当前位置放置皇后 + queens[row] = col; + columns[col] = true; + diagonals1[row - col + n - 1] = true; + diagonals2[row + col] = true; + + // 递归:处理下一行 + backtrack(row + 1, n, queens, columns, diagonals1, diagonals2, result); + + // 撤销选择:回溯,重置状态 + columns[col] = false; + diagonals1[row - col + n - 1] = false; + diagonals2[row + col] = false; + } + } + + /** + * 根据皇后位置构造棋盘 + * + * 算法思路: + * 根据记录的皇后位置生成棋盘的字符串表示 + * + * @param queens 每行皇后的列位置 + * @param n 棋盘大小 + * @return 棋盘的字符串表示 + */ + private List generateBoard(int[] queens, int n) { + // 创建棋盘列表 + List board = new ArrayList<>(); + + // 遍历每一行 + for (int i = 0; i < n; i++) { + // 创建行字符数组 + char[] row = new char[n]; + // 填充行数组为'.' + Arrays.fill(row, '.'); + // 在皇后位置放置'Q' + row[queens[i]] = 'Q'; + // 将行添加到棋盘列表 + board.add(new String(row)); + } + + // 返回棋盘 + return board; + } + + /** + * 方法2:简化版(不使用额外数组记录位置) + * + * 算法思路: + * 直接在棋盘上操作,每次放置皇后时检查冲突 + * + * @param n 皇后数量和棋盘大小 + * @return 所有解决方案的棋盘表示 + */ + public List> solveNQueensSimple(int n) { + // 创建结果列表 + List> result = new ArrayList<>(); + // 创建棋盘数组 + char[][] board = new char[n][n]; + + // 初始化棋盘 + for (int i = 0; i < n; i++) { + // 填充行数组为'.' + Arrays.fill(board[i], '.'); + } + + // 开始回溯 + backtrackSimple(0, n, board, result); + + // 返回结果 + return result; + } + + /** + * 简化版回溯辅助方法 + * + * 算法思路: + * 在棋盘上直接操作,每次放置皇后前检查冲突 + * + * @param row 当前处理的行 + * @param n 棋盘大小 + * @param board 棋盘状态 + * @param result 存储所有解决方案的结果列表 + */ + private void backtrackSimple(int row, int n, char[][] board, List> result) { + // 检查是否已处理完所有行 + if (row == n) { + // 构造解决方案 + List solution = new ArrayList<>(); + // 遍历每一行 + for (int i = 0; i < n; i++) { + // 将行添加到解决方案列表 + solution.add(new String(board[i])); + } + // 将解决方案添加到结果列表 + result.add(solution); + // 返回 + return; + } + + // 遍历当前行的所有列 + for (int col = 0; col < n; col++) { + // 检查当前位置是否可以放置皇后 + if (isValid(board, row, col, n)) { + // 放置皇后 + board[row][col] = 'Q'; + + // 递归处理下一行 + backtrackSimple(row + 1, n, board, result); + + // 回溯 + board[row][col] = '.'; + } + } + } + + /** + * 检查当前位置是否可以放置皇后 + * + * 算法思路: + * 检查当前位置是否与已放置的皇后冲突 + * + * @param board 棋盘状态 + * @param row 行位置 + * @param col 列位置 + * @param n 棋盘大小 + * @return 是否可以放置皇后 + */ + private boolean isValid(char[][] board, int row, int col, int n) { + // 检查列 + for (int i = 0; i < row; i++) { + // 检查是否有皇后 + if (board[i][col] == 'Q') { + // 返回false表示不能放置 + return false; + } + } + + // 检查主对角线(左上方向) + for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { + // 检查是否有皇后 + if (board[i][j] == 'Q') { + // 返回false表示不能放置 + return false; + } + } + + // 检查副对角线(右上方向) + for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + // 检查是否有皇后 + if (board[i][j] == 'Q') { + // 返回false表示不能放置 + return false; + } + } + + // 返回true表示可以放置 + return true; + } + + /** + * 辅助方法:读取用户输入的N值 + * + * @return N值 + */ + public static int readN() { + // 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // 打印提示信息 + System.out.print("请输入N值(N皇后问题): "); + // 读取并返回N值 + return scanner.nextInt(); + } + + /** + * 辅助方法:打印解决方案 + * + * @param result 解决方案列表 + */ + public static void printSolutions(List> result) { + // 打印解决方案数量 + System.out.println("共有 " + result.size() + " 个解决方案:"); + // 遍历所有解决方案 + for (int i = 0; i < result.size(); i++) { + // 打印解决方案编号 + System.out.println("解决方案 " + (i + 1) + ":"); + // 遍历解决方案的每一行 + for (String row : result.get(i)) { + // 打印行 + System.out.println(row); + } + // 打印空行 + System.out.println(); + } + } + + /** + * 主函数:处理用户输入并解决N皇后问题 + */ + public static void main(String[] args) { + // 打印程序标题 + System.out.println("N皇后问题"); + + // 读取用户输入的N值 + int n = readN(); + // 打印问题描述 + System.out.println("解决 " + n + " 皇后问题:"); + + // 解决N皇后问题 + SolveNQueues51 solution = new SolveNQueues51(); + // 调用solveNQueens方法 + List> result1 = solution.solveNQueens(n); + // 调用solveNQueensSimple方法 + List> result2 = solution.solveNQueensSimple(n); + + // 输出结果 + // 打印方法1结果标题 + System.out.println("方法1结果:"); + // 调用printSolutions方法打印方法1结果 + printSolutions(result1); + + // 打印方法2结果标题 + System.out.println("方法2结果:"); + // 调用printSolutions方法打印方法2结果 + printSolutions(result2); + } +} diff --git a/algorithm/SortColor75.java b/algorithm/SortColor75.java new file mode 100644 index 0000000000000..01eaf3457b212 --- /dev/null +++ b/algorithm/SortColor75.java @@ -0,0 +1,200 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 颜色分类(LeetCode 75)- 荷兰国旗问题 + * + * 时间复杂度:O(n) + * - 需要遍历数组一次 + * + * 空间复杂度:O(1) + * - 只使用了常数个额外变量 + * - 原地排序数组 + */ +public class SortColor75 { + + /** + * 对颜色数组进行排序(0表示红色,1表示白色,2表示蓝色) + * + * 算法思路: + * 使用三指针技术(Dutch National Flag Algorithm): + * 1. zero指针:指向下一个0应该放置的位置 + * 2. two指针:指向下一个2应该放置的位置 + * 3. current指针:当前正在处理的元素 + * + * 执行过程分析(以数组 [2,0,2,1,1,0] 为例): + * + * 初始状态: + * nums = [2,0,2,1,1,0] + * zero = 0, current = 0, two = 5 + * + * 第1步:current=0, nums[0]=2 + * 交换nums[0]和nums[5],two--,nums=[0,0,2,1,1,2] + * zero=0, current=0, two=4 + * + * 第2步:current=0, nums[0]=0 + * 交换nums[0]和nums[0],zero++,current++,nums=[0,0,2,1,1,2] + * zero=1, current=1, two=4 + * + * 第3步:current=1, nums[1]=0 + * 交换nums[1]和nums[1],zero++,current++,nums=[0,0,2,1,1,2] + * zero=2, current=2, two=4 + * + * 第4步:current=2, nums[2]=2 + * 交换nums[2]和nums[4],two--,nums=[0,0,1,1,2,2] + * zero=2, current=2, two=3 + * + * 第5步:current=2, nums[2]=1 + * current++,nums=[0,0,1,1,2,2] + * zero=2, current=3, two=3 + * + * 第6步:current=3, nums[3]=1 + * current++,nums=[0,0,1,1,2,2] + * zero=2, current=4, two=3 + * + * current > two,循环结束 + * 最终结果:[0,0,1,1,2,2] + * + * 时间复杂度分析: + * - 遍历数组一次:O(n) + * - 每个元素最多被访问常数次 + * + * 空间复杂度分析: + * - 只使用常数额外变量(zero, two, current):O(1) + * - 原地排序,不需要额外存储空间 + * + * @param nums 包含0、1、2的数组 + */ + public void sortColors(int[] nums) { + int zero = 0; // 指向下一个0应该放置的位置 + int two = nums.length - 1; // 指向下一个2应该放置的位置 + int current = 0; // 当前正在处理的元素索引 + + // 当current指针不超过two指针时继续循环 + while (current <= two) { + if (nums[current] == 0) { + // 如果当前元素是0,将其交换到数组前端 + swap(nums, current, zero); + zero++; // 前移zero指针 + current++; // 前移current指针 + } else if (nums[current] == 2) { + // 如果当前元素是2,将其交换到数组后端 + swap(nums, current, two); + two--; // 后移two指针 + // 注意:这里不前移current指针,因为从后面交换来的元素还未处理 + } else { + // 如果当前元素是1,保持在中间,直接前移current指针 + current++; + } + } + } + + + /** + * 方法2:两次遍历计数法 + * + * 算法思路: + * 1. 第一次遍历统计0、1、2的个数 + * 2. 第二次遍历根据统计结果重写数组 + * + * 时间复杂度分析: + * - 第一次遍历统计:O(n) + * - 第二次遍历重写:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用常数额外变量(count0, count1, count2, i):O(1) + * + * @param nums 包含0、1、2的数组 + */ + public void sortColorsTwoPass(int[] nums) { + // 统计0、1、2的个数 + int count0 = 0, count1 = 0, count2 = 0; + + for (int num : nums) { + if (num == 0) count0++; + else if (num == 1) count1++; + else count2++; + } + + // 重写数组 + int i = 0; + while (count0-- > 0) nums[i++] = 0; + while (count1-- > 0) nums[i++] = 1; + while (count2-- > 0) nums[i++] = 2; + } + + /** + * 交换数组中两个元素的辅助方法 + * + * 时间复杂度分析: + * - 交换操作:O(1) + * + * 空间复杂度分析: + * - 只使用常数额外变量(temp):O(1) + * + * @param nums 数组 + * @param i 第一个元素的索引 + * @param j 第二个元素的索引 + */ + private void swap(int[] nums, int i, int j) { + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 读取输入:O(m),m为输入字符数 + * - 解析为整数:O(n),n为数组长度 + * - 总时间复杂度:O(m+n) + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * - 总空间复杂度:O(m+n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入颜色数组(0表示红色,1表示白色,2表示蓝色,用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 主函数:处理用户输入并演示颜色分类 + */ + public static void main(String[] args) { + System.out.println("颜色分类(荷兰国旗问题)"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("输入的数组: " + Arrays.toString(nums)); + + // 保存原始数组用于对比 + int[] original = Arrays.copyOf(nums, nums.length); + + // 方法1:三指针法排序 + SortColor75 solution = new SortColor75(); + solution.sortColors(nums); + System.out.println("三指针法排序结果: " + Arrays.toString(nums)); + + // 方法2:两次遍历法排序 + int[] nums2 = Arrays.copyOf(original, original.length); + solution.sortColorsTwoPass(nums2); + System.out.println("两次遍历法排序结果: " + Arrays.toString(nums2)); + } +} diff --git a/algorithm/SortTwoList148.java b/algorithm/SortTwoList148.java new file mode 100644 index 0000000000000..339ed168c19c0 --- /dev/null +++ b/algorithm/SortTwoList148.java @@ -0,0 +1,238 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 排序链表(LeetCode 148) + * + * 时间复杂度:O(n log n) + * - 采用归并排序的思想 + * - 分解过程需要 log n 层 + * - 每层合并需要 O(n) 时间 + * - 总时间复杂度为 O(n log n) + * + * 空间复杂度:O(log n) + * - 递归调用栈的深度为 log n + */ +public class SortTwoList148 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode(int x) { val = x; } + } + + /** + * 对链表进行排序的主方法 + * + * 算法思路: + * 使用归并排序的思想对链表进行排序 + * 1. 分解:将链表从中间分成两部分 + * 2. 递归:分别对两部分进行排序 + * 3. 合并:将两个已排序的链表合并成一个有序链表 + * + * 执行过程分析(以链表 4->2->1->3 为例): + * + * 分解过程: + * 4->2->1->3 + * ↓ + * 4->2 1->3 + * ↓ ↓ + * 4 2 1 3 + * + * 合并过程: + * 4 2 1 3 + * ↓ ↓ + * 2->4 1->3 + * ↓ + * 1->2->3->4 + * + * 时间复杂度分析: + * - 分解过程:O(log n),其中n为链表长度,递归深度为log n + * - 每层合并:O(n),需要遍历当前层的所有节点 + * - 总时间复杂度:O(n log n) + * + * 空间复杂度分析: + * - 递归调用栈:O(log n) + * + * @param head 链表的头节点 + * @return 排序后的链表头节点 + */ + public ListNode sortList(ListNode head) { + // 基础情况:如果链表为空或只有一个节点,直接返回 + // 这是递归的终止条件 + if (head == null || head.next == null) { + return head; + } + + // 使用快慢指针找到中间节点,将链表分成两部分 + // slow指针每次移动一步,fast指针每次移动两步 + ListNode slow = head; + ListNode fast = head; + ListNode prev = null; + while (fast != null && fast.next != null) { + prev = slow; + slow = slow.next; + fast = fast.next.next; + } + + // 切断链表,分成两个独立的链表 + // prev是slow的前一个节点,将其next设为null来断开链表 + prev.next = null; + + // 递归排序左右两部分 + // 左部分从原头节点开始,右部分从中间节点开始 + ListNode left = sortList(head); + ListNode right = sortList(slow); + + // 合并两个已排序的链表 + return merge(left, right); + } + + /** + * 合并两个已排序的链表 + * + * 算法思路: + * 使用双指针技术,依次比较两个链表的节点值 + * 将较小的节点连接到结果链表中 + * 最后将剩余的节点直接连接到结果链表 + * + * 执行过程分析(以 l1=[1,2], l2=[3,4] 为例): + * + * 初始状态: + * l1: 1 -> 2 -> null + * l2: 3 -> 4 -> null + * dummy -> null + * current -> dummy + * + * 第1次比较(1 vs 3): + * 选择l1的节点1 + * dummy -> 1 -> null + * current -> 1 + * l1: 2 -> null + * l2: 3 -> 4 -> null + * + * 第2次比较(2 vs 3): + * 选择l1的节点2 + * dummy -> 1 -> 2 -> null + * current -> 2 + * l1: null + * l2: 3 -> 4 -> null + * + * l1为空,将l2剩余部分连接到结果链表: + * dummy -> 1 -> 2 -> 3 -> 4 -> null + * + * 时间复杂度分析: + * - 遍历两个链表:O(m + n),其中m、n分别为两个链表的长度 + * + * 空间复杂度分析: + * - 只使用常数额外变量:O(1) + * + * @param l1 第一个已排序链表的头节点 + * @param l2 第二个已排序链表的头节点 + * @return 合并后的有序链表的头节点 + */ + private ListNode merge(ListNode l1, ListNode l2) { + // 创建哑节点,简化边界条件的处理 + ListNode dummy = new ListNode(0); + 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; + } + + /** + * 创建链表的辅助方法 + * + * 算法思路: + * 读取用户输入的节点值,创建对应的链表 + * + * 时间复杂度分析: + * - 创建链表:O(n),其中n为输入节点数 + * + * 空间复杂度分析: + * - 创建链表节点:O(n) + * + * @return 创建的链表头节点 + */ + public static ListNode createList() { + // 创建Scanner对象读取用户输入 + Scanner scanner = new Scanner(System.in); + // 提示用户输入 + System.out.println("请输入链表的节点值,以空格分隔(输入结束后按回车):"); + // 读取一行输入 + String input = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] values = input.split(" "); + + // 如果没有输入节点值,返回空链表 + if (values.length == 0 || values[0].isEmpty()) { + return null; + } + + // 创建链表头节点 + ListNode head = new ListNode(Integer.parseInt(values[0])); + // current 当前节点指针 + ListNode current = head; + + // 依次创建并连接后续节点 + for (int i = 1; i < values.length; i++) { + current.next = new ListNode(Integer.parseInt(values[i])); + current = current.next; + } + + // 返回链表头节点 + return head; + } + + /** + * 主函数:处理用户输入并输出排序结果 + * + * 算法流程: + * 1. 调用createList方法读取用户输入并创建链表 + * 2. 调用sortList方法对链表进行排序 + * 3. 输出排序后的链表 + */ + public static void main(String[] args) { + // 注意:原代码中这里有个错误,应该是SortTwoList148而不是Solution + // 创建解决方案实例 + SortTwoList148 solution = new SortTwoList148(); + + // 创建链表 + ListNode head = createList(); + + // 对链表进行排序 + ListNode sortedHead = solution.sortList(head); + + // 打印排序后的链表 + System.out.print("排序后的链表为:"); + while (sortedHead != null) { + System.out.print(sortedHead.val + " "); + sortedHead = sortedHead.next; + } + System.out.println(); + } +} diff --git a/algorithm/SortedArrayToBST108.java b/algorithm/SortedArrayToBST108.java new file mode 100644 index 0000000000000..9e102320fb111 --- /dev/null +++ b/algorithm/SortedArrayToBST108.java @@ -0,0 +1,239 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.Arrays; + +/** + * 将有序数组转换为二叉搜索树(LeetCode 108) + * + * 时间复杂度:O(n) + * - n是数组中的元素个数 + * - 每个元素都需要被访问一次来创建对应的树节点 + * + * 空间复杂度:O(log n) + * - 递归调用栈的深度为log n(平衡二叉树的高度) + * - 如果算上返回的树结构,则空间复杂度为O(n) + */ +public class SortedArrayToBST108 { + + /** + * 二叉树节点定义 + */ + static class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode(int x) { + val = x; + } + } + + /** + * 将升序数组转换为高度平衡的二叉搜索树 + * + * 算法思路: + * 由于数组已经排序,我们可以采用分治的思想: + * 1. 选择数组中间的元素作为根节点 + * 2. 递归地将左半部分数组构建成左子树 + * 3. 递归地将右半部分数组构建成右子树 + * + * 执行过程分析(以数组 [-10,-3,0,5,9] 为例): + * + * 初始数组: [-10, -3, 0, 5, 9] + * + * 递归调用过程: + * buildBST(nums, 0, 4) + * ├─ mid = 2, nums[2] = 0 -> 创建根节点0 + * ├─ buildBST(nums, 0, 1) // 构建左子树 + * │ ├─ mid = 0, nums[0] = -10 -> 创建节点-10 + * │ ├─ buildBST(nums, 0, -1) -> 返回null + * │ └─ buildBST(nums, 1, 1) + * │ ├─ mid = 1, nums[1] = -3 -> 创建节点-3 + * │ ├─ buildBST(nums, 1, 0) -> 返回null + * │ └─ buildBST(nums, 2, 1) -> 返回null + * └─ buildBST(nums, 3, 4) // 构建右子树 + * ├─ mid = 3, nums[3] = 5 -> 创建节点5 + * ├─ buildBST(nums, 3, 2) -> 返回null + * └─ buildBST(nums, 4, 4) + * ├─ mid = 4, nums[4] = 9 -> 创建节点9 + * ├─ buildBST(nums, 4, 3) -> 返回null + * └─ buildBST(nums, 5, 4) -> 返回null + * + * 构建的二叉搜索树: + * 0 + * / \ + * -10 5 + * \ \ + * -3 9 + * + * 时间复杂度分析: + * - 每个数组元素访问一次:O(n),其中n为数组长度 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(log n) + * - 树节点存储空间:O(n) + * - 总空间复杂度:O(n) + * + * @param nums 有序数组 + * @return 构建的平衡二叉搜索树的根节点 + */ + public TreeNode sortedArrayToBST(int[] nums) { + return buildBST(nums, 0, nums.length - 1); + } + + /** + * 递归构建平衡二叉搜索树 + * + * 算法思路: + * 1. 选取中间元素作为根节点确保左右子树节点数尽可能平衡 + * 2. 递归处理左右子数组构建左右子树 + * 3. 基础情况:当left > right时返回null + * + * 时间复杂度分析: + * - 每个数组元素处理一次:O(n) + * - 递归深度:O(log n) + * + * 空间复杂度分析: + * - 递归调用栈:O(log n) + * + * @param nums 有序数组 + * @param left 左边界索引 + * @param right 右边界索引 + * @return 构建的子树根节点 + */ + private TreeNode buildBST(int[] nums, int left, int right) { + // 基本情况:如果左指针大于右指针,返回 null + // 这表示当前子数组为空 + if (left > right) { + return null; + } + + // 找到中间索引,作为当前子树的根节点 + // 使用 left + (right - left) / 2 避免整数溢出 + int mid = left + (right - left) / 2; + + // 创建当前根节点,值为中间元素 + TreeNode root = new TreeNode(nums[mid]); + + // 递归构建左子树和右子树 + root.left = buildBST(nums, left, mid - 1); // 左半部分数组构建成左子树 + root.right = buildBST(nums, mid + 1, right); // 右半部分数组构建成右子树 + + return root; // 返回当前根节点 + } + + /** + * 辅助方法:中序遍历打印树结构 + * + * 算法思路: + * 按照左-根-右的顺序遍历二叉树,对于BST来说会输出有序序列 + * + * 时间复杂度分析: + * - 访问每个节点一次:O(n) + * + * 空间复杂度分析: + * - 递归调用栈:O(h),其中h为树高度 + * + * @param root 树的根节点 + */ + private static void printTreeInOrder(TreeNode root) { + if (root != null) { + printTreeInOrder(root.left); + System.out.print(root.val + " "); + printTreeInOrder(root.right); + } + } + + /** + * 辅助方法:层序遍历打印树结构 + * + * 算法思路: + * 使用队列按层级顺序遍历二叉树 + * + * 时间复杂度分析: + * - 访问每个节点一次:O(n) + * + * 空间复杂度分析: + * - 队列存储节点:O(w),其中w为树的最大宽度 + * + * @param root 树的根节点 + */ + private static void printTreeLevelOrder(TreeNode root) { + if (root == null) { + System.out.println("空树"); + return; + } + + java.util.Queue queue = new java.util.LinkedList<>(); + queue.offer(root); + + System.out.print("层序遍历: "); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); + System.out.print(node.val + " "); + + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } + System.out.println(); + } + + /** + * 辅助方法:读取用户输入的有序数组 + * + * 算法思路: + * 从标准输入读取一行,按空格分割并转换为整数数组 + * + * 时间复杂度分析: + * - 处理输入字符串:O(m),其中m为输入字符数 + * - 转换为整数:O(n),其中n为数组长度 + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * + * @return 用户输入的有序数组 + */ + private static int[] readSortedArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入有序数组元素,以空格分隔:"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 主函数:处理用户输入并演示将有序数组转换为二叉搜索树 + */ + public static void main(String[] args) { + System.out.println("将有序数组转换为高度平衡的二叉搜索树"); + + // 读取用户输入的有序数组 + int[] nums = readSortedArray(); + + System.out.println("输入的有序数组: " + Arrays.toString(nums)); + + // 创建解决方案对象并构建平衡二叉搜索树 + SortedArrayToBST108 solution = new SortedArrayToBST108(); + TreeNode root = solution.sortedArrayToBST(nums); + + // 输出结果 + System.out.println("\n构建的二叉搜索树:"); + System.out.print("中序遍历: "); + printTreeInOrder(root); + System.out.println(); + + printTreeLevelOrder(root); + } +} diff --git a/algorithm/SpiralOrderMatrix54.java b/algorithm/SpiralOrderMatrix54.java new file mode 100644 index 0000000000000..8bd11f70572e1 --- /dev/null +++ b/algorithm/SpiralOrderMatrix54.java @@ -0,0 +1,337 @@ +package com.funian.algorithm.algorithm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * 螺旋矩阵(LeetCode 54) + * + * 时间复杂度:O(m × n) + * - 需要遍历矩阵中的每个元素一次 + * - 总共m×n个元素,时间复杂度为O(m × n) + * + * 空间复杂度:O(1) + * - 不考虑返回结果列表,只使用了常数级别的额外空间 + * - 使用四个边界变量控制遍历过程 + */ +public class SpiralOrderMatrix54 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 输入矩阵的行和列 + System.out.print("请输入矩阵的行数 m 和列数 n:"); + + // 读取行数 + int m = scanner.nextInt(); + + // 读取列数 + int n = scanner.nextInt(); + + // 初始化矩阵 + int[][] matrix = new int[m][n]; + + // 提示用户输入矩阵元素 + System.out.println("请输入矩阵的元素,每行用空格分隔:"); + + // 读取矩阵元素 + // 外层循环遍历行 + for (int i = 0; i < m; i++) { + // 内层循环遍历列 + for (int j = 0; j < n; j++) { + matrix[i][j] = scanner.nextInt(); + } + } + + // 调用 spiralOrder 方法按螺旋顺序遍历矩阵 + List result = spiralOrder(matrix); + + // 输出结果 + System.out.println("螺旋顺序遍历的结果是:"); + + // 遍历并打印结果 + for (int num : result) { + System.out.print(num + " "); + } + System.out.println(); + } + + /** + * 螺旋顺序遍历矩阵 + * 按照顺时针螺旋顺序返回矩阵中的所有元素 + * + * 算法思路: + * 使用四个边界变量(top, bottom, left, right)控制遍历过程 + * 1. 从左到右遍历上边界 + * 2. 从上到下遍历右边界 + * 3. 从右到左遍历下边界 + * 4. 从下到上遍历左边界 + * 5. 每遍历完一边,相应的边界向内收缩 + * 6. 重复直到所有元素都被遍历 + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 原矩阵: + * [1 2 3] + * [4 5 6] + * [7 8 9] + * + * 初始边界: top=0, bottom=2, left=0, right=2 + * + * 第1轮: + * 1. 从左到右遍历上边界(top=0): 1 2 3 + * top++ → top=1 + * 2. 从上到下遍历右边界(right=2): 6 9 + * right-- → right=1 + * 3. 从右到左遍历下边界(bottom=2): 8 7 + * bottom-- → bottom=1 + * 4. 从下到上遍历左边界(left=0): 4 + * left++ → left=1 + * + * 第2轮: + * 1. 从左到右遍历上边界(top=1): 5 + * top++ → top=2 + * + * 边界条件: top(2) > bottom(1),结束遍历 + * + * 最终结果: [1, 2, 3, 6, 9, 8, 7, 4, 5] + * + * 时间复杂度分析: + * - 遍历矩阵中的每个元素一次:O(m × n),其中m为行数,n为列数 + * + * 空间复杂度分析: + * - 结果列表:O(m × n) + * - 边界变量:O(1) + * - 总空间复杂度:O(1)(不考虑输出空间) + * + * @param matrix 输入的二维整数矩阵 + * @return 按螺旋顺序排列的元素列表 + */ + public static List spiralOrder(int[][] matrix) { + // 存储螺旋遍历结果的列表 + List result = new ArrayList<>(); + + // 边界条件检查 + if (matrix == null || matrix.length == 0) { + return result; + } + + // 获取矩阵的行数和列数 + int m = matrix.length; + int n = matrix[0].length; + + // 定义四个边界:上、下、左、右 + int top = 0; + int bottom = m - 1; + int left = 0; + int right = n - 1; + + // 模拟螺旋顺序遍历 + while (top <= bottom && left <= right) { + // 从左到右遍历上边界 + for (int i = left; i <= right; i++) { + result.add(matrix[top][i]); + } + // 上边界下移 + top++; + + // 从上到下遍历右边界 + for (int i = top; i <= bottom; i++) { + result.add(matrix[i][right]); + } + // 右边界左移 + right--; + + // 从右到左遍历下边界(检查是否还有行需要遍历) + if (top <= bottom) { + for (int i = right; i >= left; i--) { + result.add(matrix[bottom][i]); + } + // 下边界上移 + bottom--; + } + + // 从下到上遍历左边界(检查是否还有列需要遍历) + if (left <= right) { + for (int i = bottom; i >= top; i--) { + result.add(matrix[i][left]); + } + // 左边界右移 + left++; + } + } + + // 返回结果列表 + return result; + } + + /** + * 方法2:递归解法 + * + * 算法思路: + * 递归地处理每一圈的螺旋遍历 + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 1. 初始调用: spiral(matrix, 0, 0, 2, 2, result) + * 遍历外圈: [1,2,3,6,9,8,7,4],更新边界为top=1, right=1, bottom=1, left=1 + * + * 2. 递归调用: spiral(matrix, 1, 1, 1, 1, result) + * 遍历内圈: [5],更新边界为top=2, right=0, bottom=0, left=2 + * + * 3. 递归调用: spiral(matrix, 2, 2, 0, 0, result) + * 边界条件不满足(top>bottom),返回 + * + * 最终结果: [1, 2, 3, 6, 9, 8, 7, 4, 5] + * + * 时间复杂度分析: + * - 遍历矩阵中的每个元素一次:O(m × n),其中m为行数,n为列数 + * + * 空间复杂度分析: + * - 递归调用栈:O(min(m, n)) + * - 结果列表:O(m × n) + * - 总空间复杂度:O(min(m, n)) + * + * @param matrix 输入的二维整数矩阵 + * @return 按螺旋顺序排列的元素列表 + */ + public static List spiralOrderRecursive(int[][] matrix) { + List result = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return result; + } + + spiral(matrix, 0, 0, matrix.length - 1, matrix[0].length - 1, result); + return result; + } + + /** + * 递归辅助方法 + * + * 算法思路: + * 递归处理当前圈的螺旋遍历,然后递归处理内圈 + * + * @param matrix 矩阵 + * @param top 上边界 + * @param left 左边界 + * @param bottom 下边界 + * @param right 右边界 + * @param result 结果列表 + */ + private static void spiral(int[][] matrix, int top, int left, int bottom, int right, List result) { + // 边界条件 + if (top > bottom || left > right) { + return; + } + + // 从左到右遍历上边界 + for (int i = left; i <= right; i++) { + result.add(matrix[top][i]); + } + top++; + + // 从上到下遍历右边界 + for (int i = top; i <= bottom; i++) { + result.add(matrix[i][right]); + } + right--; + + // 从右到左遍历下边界 + if (top <= bottom) { + for (int i = right; i >= left; i--) { + result.add(matrix[bottom][i]); + } + bottom--; + } + + // 从下到上遍历左边界 + if (left <= right) { + for (int i = bottom; i >= top; i--) { + result.add(matrix[i][left]); + } + left++; + } + + // 递归处理内层 + spiral(matrix, top, left, bottom, right, result); + } + + /** + * 方法3:方向数组解法 + * + * 算法思路: + * 使用方向数组控制移动方向,遇到边界或已访问元素时转向 + * + * 示例过程(以矩阵 [[1,2,3],[4,5,6],[7,8,9]] 为例): + * + * 1. 初始化: + * dx=[0,1,0,-1], dy=[1,0,-1,0] (右,下,左,上) + * direction=0 (向右), x=0, y=0 + * visited=[[false,false,false],[false,false,false],[false,false,false]] + * + * 2. 遍历过程: + * i=0: 添加matrix[0][0]=1, visited[0][0]=true + * i=1: 添加matrix[0][1]=2, visited[0][1]=true + * i=2: 添加matrix[0][2]=3, visited[0][2]=true + * i=3: 下一位置(0,3)越界,转向(direction=1),添加matrix[1][2]=6 + * i=4: 添加matrix[2][2]=9, visited[2][2]=true + * i=5: 下一位置(3,2)越界,转向(direction=2),添加matrix[2][1]=8 + * ...继续直到遍历完所有元素 + * + * 时间复杂度分析: + * - 遍历矩阵中的每个元素一次:O(m × n),其中m为行数,n为列数 + * + * 空间复杂度分析: + * - 访问标记数组:O(m × n) + * - 结果列表:O(m × n) + * - 方向数组:O(1) + * - 其他变量:O(1) + * - 总空间复杂度:O(m × n) + * + * @param matrix 输入的二维整数矩阵 + * @return 按螺旋顺序排列的元素列表 + */ + public static List spiralOrderDirection(int[][] matrix) { + List result = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return result; + } + + int m = matrix.length; + int n = matrix[0].length; + + // 方向数组:右、下、左、上 + int[] dx = {0, 1, 0, -1}; + int[] dy = {1, 0, -1, 0}; + int direction = 0; // 当前方向索引 + + int x = 0, y = 0; // 当前位置 + + // 访问标记数组 + boolean[][] visited = new boolean[m][n]; + + for (int i = 0; i < m * n; i++) { + result.add(matrix[x][y]); + visited[x][y] = true; + + // 计算下一个位置 + int nx = x + dx[direction]; + int ny = y + dy[direction]; + + // 检查是否需要转向 + if (nx < 0 || nx >= m || ny < 0 || ny >= n || visited[nx][ny]) { + // 转向 + direction = (direction + 1) % 4; + nx = x + dx[direction]; + ny = y + dy[direction]; + } + + // 更新当前位置 + x = nx; + y = ny; + } + + return result; + } +} diff --git a/algorithm/SubSet78.java b/algorithm/SubSet78.java new file mode 100644 index 0000000000000..141df621a1eee --- /dev/null +++ b/algorithm/SubSet78.java @@ -0,0 +1,305 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * 子集(LeetCode 78) + * + * 时间复杂度:O(2^n × n) + * - 共有2^n个子集,每个子集平均需要O(n)时间构造 + * + * 空间复杂度:O(n) + * - 递归调用栈深度为n + * - 需要额外的列表存储当前路径 + */ +public class SubSet78 { + + /** + * 生成数组的所有子集 + * + * 算法思路: + * 使用回溯算法,通过递归和状态重置生成所有可能的子集 + * 1. 对于每个元素,有两种选择:包含或不包含 + * 2. 递归构建子集,在每个节点都将当前路径添加到结果中 + * 3. 回溯时重置状态,尝试其他可能 + * + * 执行过程分析(以nums=[1,2,3]为例): + * + * 递归树: + * [] + * / \ + * [1] [] + * / \ / \ + * [1,2] [1] [2] [] + * / | / | / | / \ + * [1,2,3][1,2][1,3][1] [2,3][2] [3] [] + * + * 详细执行过程: + * + * 1. start=0, path=[] + * - 添加[]到结果 + * - choose(1): path=[1], start=1 + * - 添加[1]到结果 + * - choose(2): path=[1,2], start=2 + * - 添加[1,2]到结果 + * - choose(3): path=[1,2,3], start=3 + * - 添加[1,2,3]到结果 + * - backtrack: remove(3), path=[1,2] + * - backtrack: remove(2), path=[1] + * - choose(3): path=[1,3], start=3 + * - 添加[1,3]到结果 + * - backtrack: remove(3), path=[1] + * - backtrack: remove(1), path=[] + * - choose(2): path=[2], start=2 + * - 添加[2]到结果 + * - choose(3): path=[2,3], start=3 + * - 添加[2,3]到结果 + * - backtrack: remove(3), path=[2] + * - backtrack: remove(2), path=[] + * - choose(3): path=[3], start=3 + * - 添加[3]到结果 + * - backtrack: remove(3), path=[] + * + * 最终结果:[[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]] + * + * 时间复杂度分析: + * - 总共有2^n个子集需要生成 + * - 每个子集平均需要O(n)时间构造和复制 + * - 总时间复杂度:O(2^n × n) + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * - path列表存储当前路径:O(n) + * - result列表存储所有子集:O(2^n × n) + * - 总空间复杂度:O(2^n × n) + * + * @param nums 整数数组(元素互不相同) + * @return 所有可能的子集 + */ + public List> subsets(int[] nums) { + List> result = new ArrayList<>(); + List path = new ArrayList<>(); + + // 开始回溯 + backtrack(nums, 0, path, result); + + return result; + } + + /** + * 回溯辅助方法 + * + * 算法思路: + * 1. 每次递归都将当前路径添加到结果中 + * 2. 从start位置开始遍历元素,避免重复 + * 3. 选择元素加入路径,递归处理后续元素 + * 4. 回溯时移除元素,尝试其他可能 + * + * 时间复杂度分析: + * - 递归深度最多为n + * - 每层需要O(n)时间遍历元素 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n) + * + * @param nums 原数组 + * @param start 当前处理的起始位置 + * @param path 当前构建的子集 + * @param result 存储所有子集的结果列表 + */ + private void backtrack(int[] nums, int start, List path, List> result) { + // 将当前路径添加到结果中(需要创建新列表避免引用问题) + result.add(new ArrayList<>(path)); + + // 从start开始尝试每个元素 + for (int i = start; i < nums.length; i++) { + // 做选择:将元素添加到当前路径 + path.add(nums[i]); + + // 递归:继续构建子集,从下一个位置开始 + backtrack(nums, i + 1, path, result); + + // 撤销选择:回溯,移除元素 + path.remove(path.size() - 1); + } + } + + + /** + * 方法2:位运算解法 + * + * 算法思路: + * 使用位运算生成所有子集 + * 1. 共有2^n个子集,可以用0到2^n-1的数字表示 + * 2. 每个数字的二进制表示中,1表示包含对应元素,0表示不包含 + * + * 执行过程分析(以nums=[1,2,3]为例): + * + * i=0 (000): [] + * i=1 (001): [3] + * i=2 (010): [2] + * i=3 (011): [2,3] + * i=4 (100): [1] + * i=5 (101): [1,3] + * i=6 (110): [1,2] + * i=7 (111): [1,2,3] + * + * 时间复杂度分析: + * - 外层循环执行2^n次 + * - 内层循环每次执行n次 + * - 总时间复杂度:O(2^n × n) + * + * 空间复杂度分析: + * - subset列表存储当前子集:O(n) + * - result列表存储所有子集:O(2^n × n) + * - 总空间复杂度:O(2^n × n) + * + * @param nums 整数数组 + * @return 所有可能的子集 + */ + public List> subsetsBitwise(int[] nums) { + List> result = new ArrayList<>(); + int n = nums.length; + + // 遍历0到2^n-1的所有数字 + for (int i = 0; i < (1 << n); i++) { + List subset = new ArrayList<>(); + + // 检查每一位是否为1 + for (int j = 0; j < n; j++) { + // 如果第j位为1,包含nums[j] + if ((i & (1 << j)) != 0) { + subset.add(nums[j]); + } + } + + result.add(subset); + } + + return result; + } + + /** + * 方法3:迭代解法 + * + * 算法思路: + * 逐个添加元素,每次添加新元素时,将该元素添加到已有的所有子集中 + * + * 执行过程分析(以nums=[1,2,3]为例): + * + * 初始:result = [[]] + * + * 添加1:result = [[], [1]] + * + * 添加2:对[]和[1]分别添加2 + * result = [[], [1], [2], [1,2]] + * + * 添加3:对[], [1], [2], [1,2]分别添加3 + * result = [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]] + * + * 时间复杂度分析: + * - 外层循环执行n次 + * - 内层循环在第i次执行时执行2^i次 + * - 总时间复杂度:O(2^n × n) + * + * 空间复杂度分析: + * - 存储所有子集:O(2^n × n) + * - 创建新子集时的临时空间:O(n) + * - 总空间复杂度:O(2^n × n) + * + * @param nums 整数数组 + * @return 所有可能的子集 + */ + public List> subsetsIterative(int[] nums) { + List> result = new ArrayList<>(); + result.add(new ArrayList<>()); // 添加空集 + + // 逐个处理每个元素 + for (int num : nums) { + int size = result.size(); + // 对已有的每个子集添加当前元素 + for (int i = 0; i < size; i++) { + List newSubset = new ArrayList<>(result.get(i)); + newSubset.add(num); + result.add(newSubset); + } + } + + return result; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - 处理输入字符串:O(m),m为输入字符数 + * - 转换为整数:O(n),n为元素个数 + * + * 空间复杂度分析: + * - 存储字符串数组:O(m) + * - 存储整数数组:O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + Scanner scanner = new Scanner(System.in); + System.out.println("请输入整数数组(用空格分隔):"); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + int[] nums = new int[strArray.length]; + for (int i = 0; i < strArray.length; i++) { + nums[i] = Integer.parseInt(strArray[i]); + } + + return nums; + } + + /** + * 辅助方法:打印子集结果 + * + * 时间复杂度分析: + * - 遍历所有子集:O(2^n) + * - 打印每个子集:O(n) + * - 总时间复杂度:O(2^n × n) + * + * @param result 子集结果 + */ + public static void printSubsets(List> result) { + System.out.println("所有子集:"); + for (int i = 0; i < result.size(); i++) { + System.out.println((i + 1) + ": " + result.get(i)); + } + System.out.println("总共 " + result.size() + " 个子集"); + } + + /** + * 主函数:处理用户输入并生成所有子集 + */ + public static void main(String[] args) { + System.out.println("子集"); + + // 读取用户输入的数组 + int[] nums = readArray(); + System.out.println("输入数组: " + Arrays.toString(nums)); + + // 生成所有子集 + SubSet78 solution = new SubSet78(); + List> result1 = solution.subsets(nums); + List> result2 = solution.subsetsBitwise(nums); + List> result3 = solution.subsetsIterative(nums); + + // 输出结果 + System.out.println("回溯方法结果:"); + printSubsets(result1); + + System.out.println("\n位运算方法结果:"); + printSubsets(result2); + + System.out.println("\n迭代方法结果:"); + printSubsets(result3); + } +} diff --git a/algorithm/SubarraySum560.java b/algorithm/SubarraySum560.java new file mode 100644 index 0000000000000..3971f8c3c9b32 --- /dev/null +++ b/algorithm/SubarraySum560.java @@ -0,0 +1,225 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * 和为K的子数组(LeetCode 560) + * + * 时间复杂度:O(n) + * - 只需要遍历数组一次 + * - HashMap的查找和插入操作平均时间复杂度为O(1) + * + * 空间复杂度:O(n) + * - 最坏情况下,HashMap需要存储n个不同的前缀和 + */ +public class SubarraySum560 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("输入数组:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] strs = line.split(" "); + + // 创建整型数组 + int[] nums = new int[strs.length]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < strs.length; i++) { + nums[i] = Integer.parseInt(strs[i]); + } + + // 提示用户输入目标和k + System.out.println("输入k:"); + + // 读取目标和k + int k = scanner.nextInt(); + + // 调用 subarraySum 方法计算和为k的子数组个数 + int result = subarraySum(nums, k); + + // 输出结果 + System.out.println("结果为:" + result); + } + + /** + * 给定一个整数数组和一个整数 k,找到该数组中和为 k 的连续子数组的个数 + * + * 算法思路: + * 使用前缀和 + HashMap 的方法 + * 1. 维护一个前缀和变量 currentSum,记录从数组开始到当前位置的元素和 + * 2. 使用 HashMap 记录每个前缀和出现的次数 + * 3. 对于当前位置的前缀和 sum,如果存在之前的前缀和为 (sum - k) + * 则说明存在子数组的和为 k + * 4. 这是因为:如果前缀和[j] - 前缀和[i] = k,则子数组[i+1, j]的和为k + * + * 示例过程(以数组 [1,1,1], k=2 为例): + * 索引: 0 1 2 + * 元素: 1 1 1 + * + * i=0: currentSum=1, 需要找前缀和=-1的次数, 不存在, map:{0:1, 1:1}, count=0 + * i=1: currentSum=2, 需要找前缀和=0的次数, 有1次, map:{0:1, 1:1, 2:1}, count=1 + * i=2: currentSum=3, 需要找前缀和=1的次数, 有1次, map:{0:1, 1:1, 2:1, 3:1}, count=2 + * + * 结果:2个子数组 [1,1] 和 [1,1] + * + * 时间复杂度分析: + * - 单次遍历数组:O(n),其中n为输入数组`nums`的长度 + * - HashMap操作:O(1),平均情况下的查找和插入操作 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - HashMap存储前缀和:O(n),最坏情况下存储n个不同的前缀和 + * - 其他变量:O(1) + * - 总空间复杂度:O(n) + * + * @param nums 整数数组 + * @param k 目标和 + * @return 和为k的连续子数组个数 + */ + public static int subarraySum(int[] nums, int k) { + // 使用 HashMap 存储前缀和及其出现次数 + Map map = new HashMap<>(); + + // 初始化:前缀和为0出现1次(表示空数组) + map.put(0, 1); + + // 记录当前前缀和 + int currentSum = 0; + + // 记录和为k的子数组个数 + int count = 0; + + // 遍历数组中的每个元素 + for (int num : nums) { + // 更新当前前缀和 + currentSum += num; + + // 检查是否存在前缀和为 (currentSum - k) 的情况 + // 如果存在,说明有子数组的和为k + if (map.containsKey(currentSum - k)) { + // 累加该前缀和出现的次数到结果中 + count += map.get(currentSum - k); + } + + // 将当前前缀和加入HashMap,更新其出现次数 + map.put(currentSum, map.getOrDefault(currentSum, 0) + 1); + } + + // 返回和为k的子数组个数 + return count; + } + + /** + * 方法2:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 双重循环遍历所有可能的子数组,计算每个子数组的和 + * + * 示例过程(以数组 [1,1,1], k=2 为例): + * + * i=0: 子数组[1],和=1≠2; 子数组[1,1],和=2=2, count=1; 子数组[1,1,1],和=3≠2 + * i=1: 子数组[1],和=1≠2; 子数组[1,1],和=2=2, count=2 + * i=2: 子数组[1],和=1≠2 + * + * 结果:count=2 + * + * 时间复杂度分析: + * - 双重循环:O(n²),其中n为输入数组`nums`的长度 + * - 计算子数组和:O(1) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 只使用了几个变量:O(1) + * - 总空间复杂度:O(1) + * + * @param nums 整数数组 + * @param k 目标和 + * @return 和为k的连续子数组个数 + */ + public static int subarraySumBruteForce(int[] nums, int k) { + int count = 0; + + // 双重循环遍历所有可能的子数组 + for (int i = 0; i < nums.length; i++) { + int sum = 0; + for (int j = i; j < nums.length; j++) { + sum += nums[j]; + if (sum == k) { + count++; + } + } + } + + return count; + } + + /** + * 方法3:前缀和数组解法 + * + * 算法思路: + * 预先计算前缀和数组,然后通过前缀和差值计算子数组和 + * + * 示例过程(以数组 [1,1,1], k=2 为例): + * + * 1. 构建前缀和数组: + * prefixSum[0] = 0 + * prefixSum[1] = 0+1 = 1 + * prefixSum[2] = 1+1 = 2 + * prefixSum[3] = 2+1 = 3 + * prefixSum = [0,1,2,3] + * + * 2. 遍历所有子数组: + * i=0,j=0: sum = prefixSum[1]-prefixSum[0] = 1-0 = 1 ≠ 2 + * i=0,j=1: sum = prefixSum[2]-prefixSum[0] = 2-0 = 2 = 2, count=1 + * i=0,j=2: sum = prefixSum[3]-prefixSum[0] = 3-0 = 3 ≠ 2 + * i=1,j=1: sum = prefixSum[2]-prefixSum[1] = 2-1 = 1 ≠ 2 + * i=1,j=2: sum = prefixSum[3]-prefixSum[1] = 3-1 = 2 = 2, count=2 + * i=2,j=2: sum = prefixSum[3]-prefixSum[2] = 3-2 = 1 ≠ 2 + * + * 结果:count=2 + * + * 时间复杂度分析: + * - 构建前缀和数组:O(n),其中n为输入数组`nums`的长度 + * - 双重循环计算子数组和:O(n²) + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 前缀和数组:O(n),需要额外存储n+1个前缀和值 + * - 其他变量:O(1) + * - 总空间复杂度:O(n) + * + * @param nums 整数数组 + * @param k 目标和 + * @return 和为k的连续子数组个数 + */ + public static int subarraySumPrefixArray(int[] nums, int k) { + int count = 0; + int n = nums.length; + + // 构建前缀和数组 + int[] prefixSum = new int[n + 1]; + for (int i = 0; i < n; i++) { + prefixSum[i + 1] = prefixSum[i] + nums[i]; + } + + // 遍历所有可能的子数组 + for (int i = 0; i < n; i++) { + for (int j = i; j < n; j++) { + // 子数组[i,j]的和 = prefixSum[j+1] - prefixSum[i] + if (prefixSum[j + 1] - prefixSum[i] == k) { + count++; + } + } + } + + return count; + } +} diff --git a/algorithm/SwapPairsList24.java b/algorithm/SwapPairsList24.java new file mode 100644 index 0000000000000..43e60233fe37b --- /dev/null +++ b/algorithm/SwapPairsList24.java @@ -0,0 +1,248 @@ +package com.funian.algorithm.algorithm; + +import java.util.List; + +/** + * 两两交换链表中的节点(LeetCode 24) + * + * 时间复杂度:O(n) + * - n 是链表的长度 + * - 需要遍历链表一次,每个节点最多被访问常数次 + * - 总时间复杂度为 O(n) + * + * 空间复杂度: + * - 迭代解法:O(1) + * 只使用了常数个额外变量 + * - 递归解法:O(n) + * 递归调用栈的深度最多为 n/2 + */ +public class SwapPairsList24 { + + /** + * 链表节点定义 + */ + static class ListNode { + int val; + ListNode next; + ListNode(int val) { this.val = val; } + } + + /** + * 方法1:迭代解法 + * + * 算法思路: + * 使用哑节点简化头节点交换的处理 + * 通过三个指针(prev, first, second)来完成节点交换 + * 每次处理两个节点,然后移动指针处理下一对 + * + * 执行过程分析(以链表 1->2->3->4 为例): + * + * 初始状态: + * dummy -> 1 -> 2 -> 3 -> 4 -> null + * prev -> dummy + * first -> 1 + * second -> 2 + * + * 第一次交换(节点1和节点2): + * 1. 保存节点3:nextPair = 3 -> 4 -> null + * 2. 交换节点1和节点2: + * second.next = first (2 -> 1) + * first.next = nextPair (1 -> 3 -> 4 -> null) + * prev.next = second (dummy -> 2) + * 3. 更新指针: + * prev -> 1 + * first -> 3 + * second -> 4 + * 4. 状态:dummy -> 2 -> 1 -> 3 -> 4 -> null + * + * 第二次交换(节点3和节点4): + * 1. 保存节点null:nextPair = null + * 2. 交换节点3和节点4: + * second.next = first (4 -> 3) + * first.next = nextPair (3 -> null) + * prev.next = second (1 -> 4) + * 3. 更新指针: + * first -> null (循环结束) + * 4. 最终状态:dummy -> 2 -> 1 -> 4 -> 3 -> null + * + * 时间复杂度分析: + * - 遍历链表:O(n),其中n为链表长度 + * - 每次循环执行常数时间操作 + * + * 空间复杂度分析: + * - 只使用常数个额外变量:O(1) + * + * @param head 链表的头节点 + * @return 交换后的链表头节点 + */ + public ListNode swapPairsIterative(ListNode head) { + // 创建哑节点,简化头节点交换的处理 + ListNode dummy = new ListNode(0); + dummy.next = head; + + // prev指向前一对节点交换后的后一个节点 + ListNode prev = dummy; + + // 当还有至少两个节点需要交换时继续循环 + while (head != null && head.next != null) { + // 定义要交换的两个节点 + ListNode first = head; + ListNode second = head.next; + + // 保存下一对节点的开始位置 + ListNode nextPair = second.next; + + // 执行交换操作 + second.next = first; + first.next = nextPair; + prev.next = second; + + // 更新指针,准备处理下一对节点 + prev = first; + head = nextPair; + } + + // 返回交换后的链表头节点 + return dummy.next; + } + + /** + * 方法2:递归解法 + * + * 算法思路: + * 将问题分解为子问题 + * 1. 交换前两个节点 + * 2. 递归处理剩余的节点 + * 3. 将两部分连接起来 + * + * 执行过程分析(以链表 1->2->3->4 为例): + * + * 递归调用过程: + * swapPairs(1->2->3->4) + * ├─ first = 1 + * ├─ second = 2 + * ├─ nextPair = swapPairs(3->4) + * │ ├─ first = 3 + * │ ├─ second = 4 + * │ ├─ nextPair = swapPairs(null) + * │ │ └─ 返回 null + * │ ├─ second.next = first (4->3) + * │ ├─ first.next = nextPair (3->null) + * │ └─ 返回 4->3->null + * ├─ second.next = first (2->1) + * ├─ first.next = nextPair (1->4->3->null) + * └─ 返回 2->1->4->3->null + * + * 时间复杂度分析: + * - 递归深度:O(n/2),其中n为链表长度 + * - 每层递归执行常数时间操作 + * + * 空间复杂度分析: + * - 递归调用栈深度:O(n/2) + * + * @param head 链表的头节点 + * @return 交换后的链表头节点 + */ + public ListNode swapPairsRecursive(ListNode head) { + // 基础情况:如果节点数少于2个,直接返回 + if (head == null || head.next == null) { + return head; + } + + // 定义要交换的两个节点 + ListNode first = head; + ListNode second = head.next; + + // 递归处理剩余的节点 + ListNode nextPair = swapPairsRecursive(second.next); + + // 执行交换操作 + second.next = first; + first.next = nextPair; + + // 返回新的头节点(第二个节点) + return second; + } + + /** + * 辅助方法:创建链表(用于测试) + */ + public ListNode createList(int[] values) { + if (values.length == 0) return null; + // 创建头节点 + ListNode head = new ListNode(values[0]); + // current 当前节点指针 + ListNode current = head; + // for (int i = 1; i < values.length; i++) 遍历数组剩余元素 + for (int i = 1; i < values.length; i++) { + current.next = new ListNode(values[i]); + current = current.next; + } + // 返回链表头节点 + return head; + } + + /** + * 辅助方法:打印链表(用于测试) + */ + public void printList(ListNode head) { + // current 当前节点指针,初始指向头节点 + ListNode current = head; + while (current != null) { + System.out.print(current.val); + if (current.next != null) { + System.out.print(" -> "); + } + current = current.next; + } + System.out.println(" -> null"); + } + + /** + * 测试方法和使用示例 + */ + public static void main(String[] args) { + // 创建解决方案实例 + SwapPairsList24 solution = new SwapPairsList24(); + + // 测试用例1:偶数个节点 + ListNode head1 = solution.createList(new int[]{1, 2, 3, 4}); + System.out.println("原始链表(偶数个节点):"); + solution.printList(head1); + + // solution.swapPairsIterative(head1) 使用迭代解法交换节点 + ListNode result1 = solution.swapPairsIterative(head1); + System.out.println("迭代解法交换后:"); + solution.printList(result1); + + // 测试用例2:奇数个节点 + ListNode head2 = solution.createList(new int[]{1, 2, 3, 4, 5}); + System.out.println("原始链表(奇数个节点):"); + solution.printList(head2); + + // solution.swapPairsRecursive(head2) 使用递归解法交换节点 + ListNode result2 = solution.swapPairsRecursive(head2); + System.out.println("递归解法交换后:"); + solution.printList(result2); + + // 测试用例3:单个节点 + ListNode head3 = solution.createList(new int[]{1}); + System.out.println("原始链表(单个节点):"); + solution.printList(head3); + + // solution.swapPairsIterative(head3) 使用迭代解法交换节点 + ListNode result3 = solution.swapPairsIterative(head3); + System.out.println("交换后:"); + solution.printList(result3); + + // 测试用例4:空链表 + ListNode head4 = null; + System.out.println("原始链表(空链表):"); + solution.printList(head4); + + // solution.swapPairsRecursive(head4) 使用递归解法交换节点 + ListNode result4 = solution.swapPairsRecursive(head4); + System.out.println("交换后:"); + solution.printList(result4); + } +} diff --git a/algorithm/Test.java b/algorithm/Test.java new file mode 100644 index 0000000000000..7a26a7ec0b2e1 --- /dev/null +++ b/algorithm/Test.java @@ -0,0 +1,1567 @@ +//package com.funian.algorithm.algorithm; +// +//import java.util.*; +// +//public class Test { +// public int[] two(int[] nums, int taregt) { +// // 哈希存储 +// Map map = new HashMap<>(); +// +// // 循环遍历 +// for (int i = 0; i < nums.length; i++) { +// // 补数 +// int complement = taregt - nums[i]; +// if (map.containsKey(complement)) { +// return new int[] {map.get(complement), i}; +// } +// +// // 插入哈希表 +// map.put(nums[i], i); +// } +// return null; +// } +// +// public int[] two1(int[] nums, int taregt) { +// // 哈希表 +// Map map = new HashMap<>(); +// +// // 循环遍历 +// for (int i = 0; i < nums.length; i++) { +// // 补数 +// int complement = taregt - nums[i]; +// // 补数判断 +// if (map.containsKey(complement)) { +// return new int[] {map.get(complement), i}; +// } +// +// // 插入新数据 +// map.put(nums[i], i); +// } +// return new int[]; +// } +// +// public static int[] two3(int[] nums, int target) { +// // 哈希表 +// Map map = new HashMap<>(); +// +// // 循环 +// for (int i = 0; i < nums.length; i++) { +// // 补数 +// int complement = target - nums[i]; +// // 判断 +// if (map.containsKey(complement)) { +// // 返回 +// return new int[] {map.get(complement), i}; +// } +// // 插入数据和索引 +// map.put(nums[i], i); +// } +// // 返回空 +// return null; +// } +// +// /** +// * 字母分组 +// * @param strs +// * @return +// */ +// public static List> group(String[] strs) { +// // 哈希表 +// Map> map = new HashMap<>(); +// +// //遍历 +// for (String str : strs) { +// // 字符数组 +// char[] chars = str.toCharArray(); +// // 排序 +// Arrays.sort(chars); +// +// // key设定 +// String key = new String(chars); +// +// // 判断 +// if (!map.containsKey(key)) { +// map.put(key, new ArrayList<>()); +// } +// // 获取 +// map.get(key).add(str); +// +// } +// return new ArrayList<>(map.values()); +// } +// +// +// /** +// * 字母分组 +// * @param strs +// * @return +// */ +// public static List> gruopp(String[] strs) { +// // 哈希表 +// Map> map = new HashMap<>(); +// // 遍历 +// for (String str : strs) { +// // 字符数组 +// char[] chars = str.toCharArray(); +// +// // 排序 +// Arrays.sort(chars); +// +// // key设置 +// String key = new String(chars); +// +// // 判断 +// if (!map.containsKey(key)) { +// map.put(key, new ArrayList<>()); +// } +// +// map.get(key).add(str); +// } +// return new ArrayList<>(map.values()); +// } +// +// public static int longggg(int[] nums) { +// int n = nums.length; +// // 去重 +// Set setNum = new HashSet<>(); +// +// // 遍历 +// for (int num : nums) { +// setNum.add(num); +// } +// +// // 定义最长 +// int lllll = 0; +// +// for (int num : setNum) { +// if (!setNum.contains(num - 1)) { +// int currN = num; +// int curr = 1; +// while (setNum.contains(currN + 1)){ +// currN++; +// curr++; +// } +// +// // 更新长度 +// lllll = Math.max(lllll,curr); +// } +// } +// return lllll; +// } +// +// +// public int longestConsecutive(int[] nums) { +// int n = nums.length; +// // 去重 +// Set setNum = new HashSet<>(); +// +// // 遍历 +// for (int num : nums) { +// setNum.add(num); +// } +// +// // 定义最长 +// int lllll = 0; +// +// for (int num : setNum) { +// if (!setNum.contains()) +// } +// } +// +// public int llosng(int[] nums) { +// // 哈希去重 +// Set setNum = new HashSet<>(); +// +// for (int num : nums) { +// setNum.add(num); +// } +// +// int length = 0; +// // 遍历去重 +// for (int num : setNum) { +// if (!setNum.contains(num - 1)) { +// // 当前长度和元素 +// int currentnum = num; +// int currentLength = 1; +// // 循环 +// while (setNum.contains(currentnum + 1)) { +// currentLength++; +// currentnum++; +// } +// // 更新最大长度 +// length = Math.max(length, currentLength); +// } +// } +// +// return length; +// } +// +// public int longest(int[] nums) { +// // 哈希去重 +// Set settt = new HashSet<>(); +// +// // 遍历 +// for (int num : nums) { +// settt.add(num); +// } +// +// // 定义 +// int maxL = 0; +// // 遍历 +// for (int num : settt) { +// if (!settt.contains(num - 1)) { +// int currentNum = num; +// int currentLength = 1; +// while (settt.contains(currentNum + 1)) { +// currentLength++; +// currentNum++; +// } +// maxL = Math.max(maxL, currentLength); +// } +// } +// return maxL; +// } +// +// /** +// * 双指针 +// * @param nums +// */ +// public void moveZero(int[] nums) { +// int n = nums.length; +// int left = 0; +// for (int right = 0; right < n; right++) { +// if (nums[right] != 0) { +// int temp = nums[left]; +// nums[left] = nums[right]; +// nums[right] = temp; +// left++; +// } +// } +// } +// +// public void moveZeroes(int[] nums) { +// int n = nums.length; +// int left = 0; +// for (int right = 0; right < n; right++) { +// if (nums[right] != 0) { +// int temp = nums[left]; +// nums[left] = nums[right]; +// nums[right] = temp; +// left++; +// } +// } +// } +// +// /** +// * 移动0 +// * @param nums +// */ +// public void moveZero(int[] nums) { +// int n = nums.length; +// // 定义左指针 +// int left = 0; +// // 遍历 +// for (int right = 0; right < n; right++) { +// if (nums[right] != 0) { +// // 交换 +// int temp = nums[left]; +// nums[left] = nums[right]; +// nums[right] = temp; +// left++; +// } +// } +// } +// +// public int maxWater(int[] height) { +// // 定义左右指针 +// int n = height.length; +// +// int left = 0; +// int right = n - 1; +// int maxWater = 0; +// // 遍历 +// while (left < right) { +// int minHeight = Math.min(height[left], height[right]); +// int currentWater = (right - left) * minHeight; +// maxWater = Math.max(maxWater, currentWater); +// if (height[left] < height[right]) { +// left++; +// } else { +// right--; +// } +// } +// return maxWater; +// } +// +// public int mammamax(int[] nums) { +// // 定义左右指针 +// int n = nums.length; +// int max = 0; +// int left = 0; +// int right = n - 1; +// // 遍历 +// while (left < right) { +// int min = Math.min(nums[left], nums[right]); +// int width = right - left; +// // 当前容量 +// int currentWater = min * width; +// // 更新最大值 +// max = Math.max(max, currentWater); +// if (nums[left] < nums[right]) { +// left++; +// } else { +// right--; +// } +// } +// return max; +// } +// +// /** +// * 盛水 +// * @param nums +// * @return +// */ +// public int maxWint(int[] nums) { +// int n = nums.length; +// +// // 定义指针 +// int left = 0; +// int right = n - 1; +// int max = 0; +// +// // 遍历 +// while (left < right) { +// int min = Math.min(nums[left], nums[right]); +// int width = right - left; +// // 当前容量 +// int currentWater = min * width; +// // 更新最大值 +// +// max = Math.max(max, currentWater); +// +// if (nums[left] < nums[right]) { +// left++; +// } else { +// right--; +// } +// } +// return max; +// } +// +// public List> three(int[] nums) { +// // 定义结果 +// List> result = new ArrayList<>(); +// +// // 排序 +// Arrays.sort(nums); +// +// int n = nums.length; +// +// for (int i = 0; i < n - 2; i++) { +// if (i > 0 && nums[i] == nums[i - 1]) { +// continue; +// } +// +// int left = i + 1; +// int right = n - 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])); +// while (left < right && nums[left] == nums[left + 1]) { +// left++; +// } +// +// while (left < right && nums[right] == nums[right - 1]) { +// right--; +// } +// +// left++; +// right--; +// } if (sum < 0) { +// left++; +// } else { +// right--; +// } +// } +// } +// return result; +// } +// +// +// public List> three(int[] nums) { +// List> result = new ArrayList<>(); +// // 排序 +// Arrays.sort(nums); +// int n = nums.length; +// // 遍历 +// for (int i = 0; i < n - 2; i++) { +// // 去重 +// if (i > 0 && nums[i] == nums[i - 1]) { +// continue; +// } +// // 定义指针 +// int left = i + 1; +// int right = n - 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])); +// // 去重 +// while (left < right && nums[left] == nums[left + 1]) { +// left++; +// } +// // 去重 +// while (left < right && nums[right] == nums[right - 1]) { +// right--; +// } +// left++; +// right--; +// } else if (sum < 0) { +// left++; +// } else { +// right--; +// } +// } +// } +// +// return result; +// } +// +// +// public int trap(int[] nums) { +// int n = nums.length; +// // 定义指针 +// int left = 0; +// int right = n - 1; +// // 定义最大值 +// int leftMax = 0; +// int rightMax = 0; +// int res = 0; +// // 遍历 +// while (left < right) { +// leftMax = Math.max(leftMax, nums[left]); +// rightMax = Math.max(rightMax, nums[right]); +// if (nums[left] < nums[right]) { +// res += leftMax - nums[left]; +// left++; +// } else { +// res += rightMax - nums[right]; +// right--; +// } +// } +// return res; +// } +// +// /** +// * 最长不重复子串 +// * @param s +// * @return +// */ +// public int lengrrh(String s) { +// int n = s.length(); +// // 去重 +// Set set = new HashSet<>(); +// // 滑动窗 +// int left = 0; +// int right = 0; +// int maxL = 0; +// // 滑动窗口 +// while (right < n) { +// if (!set.contains(s.charAt(right))) { +// set.add(s.charAt(right)); +// right++; +// maxL = Math.max(maxL, right - left); +// } else { +// set.remove(s.charAt(left)); +// left++; +// } +// } +// +// return maxL; +// } +// +// /** +// * 最长不重复子串 +// * @param s +// * @return +// */ +// public int longstetet(String s) { +// // 滑动窗口 +// int left = 0; +// int n = s.length(); +// int right = 0; +// int maxL = 0; +// Set set = new HashSet<>(); +// while (right < n) { +// if (!set.contains(s.charAt(right))) { +// set.add(s.charAt(right)); +// right++; +// maxL = Math.max(maxL, right - left); +// } +// else { +// set.remove(s.charAt(left)); +// left++; +// } +// } +// return maxL; +// } +// +// public List findd(String s, String p) { +// // 定义结果 +// List result = new ArrayList<>(); +// if (s == null || s.length() < p.length()) { +// return result; +// } +// +// int[] pc = new int[26]; +// int[] sc = new int[26]; +// +// int pl = p.length(); +// int sl = s.length(); +// for (int i = 0; i < pl; i++) { +// pc[p.charAt(i) - 'a']++; +// } +// +// // 遍历 +// for (int i = 0; i < sl; i++) { +// sc[s.charAt(i) - 'a']++; +// if (i >= pl) { +// sc[s.charAt(i - pl) - 'a']--; +// } +// +// if (Arrays.equals(pc, sc)) { +// result.add(i - pl + 1); +// } +// } +// return result; +// } +// +// /** +// * 子数组和为k的个数 +// * @param nums +// * @param k +// * @return +// */ +// public int suaaa(int[] nums, int k) { +// // 定义前缀和 +// Map pre = new HashMap<>(); +// +// // 添加 +// pre.put(0, 1); +// // 定义当前和 +// int cursum = 0; +// // 添加 +// int count = 0; +// // 遍历 +// for (int i = 0; i < nums.length; i++) { +// cursum += nums[i]; +// // 判断 +// if (pre.containsKey(cursum - k)) { +// count += pre.get(cursum - k); +// } +// // 添加 +// pre.put(cursum, pre.getOrDefault(cursum, 0) + 1); +// } +// // +// return count; +// +// } +// +// /** +// * 滑动窗口 +// * @param nums +// * @param k +// * @return +// */ +// public int[] maxSlding(int[] nums, int k) { +// int n = nums.length; +// // 定义结果 +// int[] res = new int[n - k + 1]; +// // 边界条件 +// if (nums == null || nums.length == 0 || k == 0) { +// return res; +// } +// +// // 定义双端队列 +// Deque deque = new LinkedList<>(); +// // 遍历 +// for (int i = 0; i < n ; i++) { +// // 移除滑动窗口外的元素索引 +// if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) { +// deque.pollFirst(); +// } +// // 移除所有小于当前元素的队列尾部元素 +// while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { +// deque.pollLast(); +// } +// // 添加当前元素下标到队列尾部 +// deque.offerLast(i); +// // 记录当前窗口的最大值 +// if (i >= k - 1) { +// // 队列头部索引对应的元素就是当前窗口的最大值 +// res[i - k + 1] = nums[deque.peekFirst()]; +// } +// } +// return res; +// } +// +// public static String minWindow(String s, String t) { +// if (s == null || s.length() == 0 || t == null || t.length() == 0) { +// return ""; +// } +// +// // 记录 t 中每个字符的需求数量 +// Map targetMap = new HashMap<>(); +// for (char c : t.toCharArray()) { +// targetMap.put(c, targetMap.getOrDefault(c, 0) + 1); +// } +// +// // 滑动窗口所需的变量 +// Map windowMap = new HashMap<>(); +// int left = 0, right = 0; // 左右指针,表示窗口的范围 +// int valid = 0; // 符合条件的字符数 +// int minLen = Integer.MAX_VALUE; // 最小长度 +// int start = 0; // 最小窗口的起始位置 +// +// while (right < s.length()) { +// char c = s.charAt(right); // 获取当前右边界字符 +// right++; // 扩展右边界 +// +// // 如果该字符在 t 中,则加入窗口进行处理 +// if (targetMap.containsKey(c)) { +// windowMap.put(c, windowMap.getOrDefault(c, 0) + 1); +// // 如果窗口中的该字符数量和 t 中需求一致,则有效字符数 +1 +// if (windowMap.get(c).equals(targetMap.get(c))) { +// valid++; +// } +// } +// +// // 当窗口内的字符已经满足 t 中所有字符时,尝试收缩左边界 +// while (valid == targetMap.size()) { +// // 更新最小窗口长度 +// if (right - left < minLen) { +// minLen = right - left; +// start = left; +// } +// +// char d = s.charAt(left); // 获取左边界字符 +// left++; // 收缩左边界 +// +// // 如果该字符在 t 中,收缩窗口时需要进行处理 +// if (targetMap.containsKey(d)) { +// // 如果该字符数量减少到不再满足需求,则有效字符数 -1 +// if (windowMap.get(d).equals(targetMap.get(d))) { +// valid--; +// } +// windowMap.put(d, windowMap.get(d) - 1); // 更新窗口中的字符数量 +// } +// } +// } +// +// // 如果 minLen 没有被更新过,说明没有找到符合条件的子串,返回空字符串 +// return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); +// } +// +// +// /** +// * 连续子数组的最大和 +// * @param nums +// * @return +// */ +// public int subsum(int[] nums) { +// int cur = nums[0]; +// int max = nums[0]; +// for (int i = 1; i < nums.length; i++) { +// cur = Math.max(nums[i], cur + nums[i]); +// max = Math.max(max, cur); +// } +// return max; +// } +// +// public int mama(int[] nums) { +// int cur = nums[0]; +// int max = nums[0]; +// for (int i = 1; i < nums.length; i++) { +// cur = Math.max(nums[i], cur + nums[i]); +// max = Math.max(max, cur); +// } +// } +// +// public int max(int[] nums) { +// int curN = nums[0]; +// int maxN = nums[0]; +// for (int i = 1; i < nums.length; i++) { +// curN = Math.max(nums[i], curN + nums[i]); +// maxN = Math.max(maxN, curN); +// } +// return maxN; +// } +// +// public int[][] merge(int[][] intervals) { +// if (intervals.length == 0) return new int[0][0]; +// Arrays.sort(intervals, (a, b) -> a[0] - b[0]); +// List merged = new ArrayList<>(); +// merged.add(intervals[0]); +// for (int i = 1; i < intervals.length; i++) { +// int[] cur = intervals[i]; +// int[] last = merged.get(merged.size() - 1); +// if (cur[0] <= last[1]) { +// last[1] = Math.max(last[1], cur[1]); +// } else { +// merged.add(cur); +// } +// } +// +// return merged.toArray(new int[merged.size()][]); +// } +// +// +// public static void reverse(int[] nums, int start, int end) { +// while (start < end) { +// int temp = nums[start]; +// nums[start] = nums[end]; +// nums[end] = temp; +// start++; +// end--; +// } +// } +// public static void re(int[] nums, int start, int end) { +// while (start < end) { +// int temp = nums[start]; +// nums[start] = nums[end]; +// nums[end] = temp; +// start++; +// end--; +// } +// } +// +// public static void rotate(int[] nums, int k) { +// int n = nums.length; +// k %= n; +// reverse(nums, 0, n - 1); +// reverse(nums, 0, k - 1); +// reverse(nums, k, n - 1); +// } +// +// public static void rotate1(int[] nums, int k) { +// int n = nums.length; +// k %= n; +// re(nums, 0, n - 1); +// re(nums, 0, k - 1); +// re(nums, k, n - 1); +// } +// +// public static int[] prroo(int[] nums) { +// int n = nums.length; +// int[] res = new int[n]; +// res[0] = 1; +// for (int i = 1; i < n; i++) { +// res[i] = res[i - 1] * nums[i - 1]; +// } +// int right = 1; +// for (int i = n - 1; i >= 0; i--) { +// res[i] *= right; +// right *= nums[i]; +// } +// return res; +// } +// +// public int mis(int[] nums) { +// int n = nums.length; +// // 遍历数组,将每个元素放到正确的位置 +// for (int i = 0; i < n; i++) { +// while (nums[i] > 0 && nums[i] <= n && nums[i] != nums[nums[i] - 1]) { +// swap(nums, i, nums[i] - 1); +// } +// } +// // 找到第一个不匹配的数字 +// for (int i = 0; i < n; i++) { +// if (nums[i] != i + 1) { +// return i + 1; +// } +// } +// // 如果没有缺失的数字,返回 n + 1 +// return n + 1; +// } +// +// /** +// * 交换两个元素 +// */ +// public static void swap(int[] nums, int i, int j) { +// int temp = nums[i]; +// nums[i] = nums[j]; +// nums[j] = temp; +// } +// +// +// public void setzeros(int[][] matrix) { +// int m = matrix.length; +// int n = matrix[0].length; +// +// boolean firstRowZero = false; +// boolean firstColZero = false; +// for (int i = 0; i < m; i++) { +// if (matrix[i][0] == 0) { +// firstColZero = true; +// break; +// } +// } +// for (int j = 0; j < n; j++) { +// if (matrix[0][j] == 0) { +// firstRowZero = true; +// break; +// } +// } +// for (int i = 1; i < m; i++) { +// for (int j = 1; j < n; j++) { +// if (matrix[i][j] == 0) { +// matrix[i][0] = 0; +// matrix[0][j] = 0; +// } +// } +// } +// for (int i = 1; i < m; i++) { +// for (int j = 1; j < n; j++) { +// if (matrix[i][0] == 0 || matrix[0][j] == 0) { +// matrix[i][j] = 0; +// } +// } +// } +// +// +// if (firstRowZero) { +// for (int j = 0; j < n; j++) { +// matrix[0][j] = 0; +// } +// } +// if (firstColZero) { +// for (int i = 0; i < m; i++) { +// matrix[i][0] = 0; +// } +// } +// } +// +// public static List spiralOrder(int[][] matrix) { +// List result = new ArrayList<>(); +// +// if (matrix == null || matrix.length == 0) { +// return result; +// } +// +// 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 i = left; i <= right; i++) { +// result.add(matrix[top][i]); +// } +// top++; // 上边界下移 +// +// // 从上到下遍历右边界 +// for (int i = top; i <= bottom; i++) { +// result.add(matrix[i][right]); +// } +// right--; // 右边界左移 +// +// // 从右到左遍历下边界 +// if (top <= bottom) { +// for (int i = right; i >= left; i--) { +// result.add(matrix[bottom][i]); +// } +// bottom--; // 下边界上移 +// } +// +// // 从下到上遍历左边界 +// if (left <= right) { +// for (int i = bottom; i >= top; i--) { +// result.add(matrix[i][left]); +// } +// left++; // 左边界右移 +// } +// } +// +// return result; +// } +// +// +// public static 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++; +// } +// } +// +// // 如果遍历完整个矩阵仍未找到目标值,返回 false +// return false; +// } +// +// +// public static class ListNode { +// int val; +// ListNode next; +// +// ListNode(int x) { +// val = x; +// } +// } +// public ListNode getIntersectionNode(ListNode headA, ListNode headB) { +// // 使用哈希集合存储链表A中的节点 +// Set visited = new HashSet(); +// // 定义临时变量,用于遍历链表A +// ListNode temp = headA; +// // 遍历链表A,将每个节点存入哈希集合 +// while (temp != null) { +// visited.add(temp); // 将当前节点加入集合 +// temp = temp.next; // 移动到下一个节点 +// } +// // 重置temp为链表B的头节点,开始遍历链表B +// temp = headB; +// // 遍历链表B,查找第一个与链表A相交的节点 +// while (temp != null) { +// if (visited.contains(temp)) { // 如果链表B的节点在链表A的集合中出现过,说明这是相交点 +// return temp; // 返回相交点 +// } +// temp = temp.next; // 继续遍历链表B的下一个节点 +// } +// // 如果没有找到相交节点,返回null +// return null; +// } +// +// public static class ListNode { +// int val; +// ListNode next; +// +// ListNode(int x) { +// val = x; +// } +// } +// +// public static ListNode reverseList(ListNode head) { +// ListNode prev = null; +// ListNode curr = head; +// +// while (curr != null) { +// ListNode next = curr.next; // 暂存下一个节点 +// curr.next = prev; // 当前节点的next指向前一个节点 +// prev = curr; // prev前移 +// curr = next; // curr前移 +// } +// +// return prev; // 返回新链表的头节点 +// } +// +// +// public static ListNode revr(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 boolean isPalindrome(ListNode head) { +// if (head == null) return true; +// +// // 使用快慢指针找到链表的中间节点 +// ListNode firstHalfEnd = endOfFirstHalf(head); +// ListNode secondHalfStart = reverseList(firstHalfEnd.next); +// +// // 检查链表是否回文 +// ListNode p1 = head; +// ListNode p2 = secondHalfStart; +// boolean result = true; +// while (result && p2 != null) { +// if (p1.val != p2.val) { +// result = false; +// } +// p1 = p1.next; +// p2 = p2.next; +// } +// +// // 恢复链表(可选) +// firstHalfEnd.next = reverseList(secondHalfStart); +// +// return result; +// } +// +// // 反转链表的方法 +// private ListNode reverseList(ListNode head) { +// ListNode prev = null; +// ListNode curr = head; +// while (curr != null) { +// ListNode next = curr.next; +// curr.next = prev; +// prev = curr; +// curr = next; +// } +// return prev; +// } +// +// // 使用快慢指针找到链表的中间节点 +// private ListNode endOfFirstHalf(ListNode head) { +// ListNode fast = head; +// ListNode slow = head; +// while (fast.next != null && fast.next.next != null) { +// slow = slow.next; +// fast = fast.next.next; +// } +// return slow; +// } +// +// public boolean hasCycle(ListNode head) { +// if (head == 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 ListNode detectCycle(ListNode head) { +// if (head == null || head.next == null) return null; +// +// // 1. 快慢指针寻找相遇点(若无环则返回 null) +// ListNode slow = head, fast = head; +// boolean hasCycle = false; +// while (fast != null && fast.next != null) { +// slow = slow.next; +// fast = fast.next.next; +// if (slow == fast) { +// hasCycle = true; +// break; +// } +// } +// if (!hasCycle) return null; +// +// // 2. 将一个指针移回链表头,两指针同速前进,相遇处即为入环节点 +// ListNode p1 = head, p2 = slow; +// while (p1 != p2) { +// p1 = p1.next; +// p2 = p2.next; +// } +// return p1; +// } +// +// +// public ListNode mergeTwoLists(ListNode l1, ListNode l2) { +// // 创建哨兵节点,简化边界条件的处理 +// ListNode dummy = new ListNode(0); +// 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 ListNode addTwoNumbers(ListNode l1, ListNode l2) { +// ListNode dummy = new ListNode(0); // 哑节点,用于简化链表操作 +// ListNode current = dummy; // 当前节点指针 +// int carry = 0; // 进位值 +// +// // 遍历两个链表 +// while (l1 != null || l2 != null || carry != 0) { +// int sum = carry; // 初始化和为进位 +// +// // 如果 l1 不为空,则加上 l1 的值 +// if (l1 != null) { +// sum += l1.val; +// l1 = l1.next; // 移动到下一个节点 +// } +// +// // 如果 l2 不为空,则加上 l2 的值 +// if (l2 != null) { +// sum += l2.val; +// l2 = l2.next; // 移动到下一个节点 +// } +// +// carry = sum / 10; // 计算新的进位 +// current.next = new ListNode(sum % 10); // 创建新节点,保存当前位的值 +// current = current.next; // 移动到下一个节点 +// } +// +// return dummy.next; // 返回结果链表 +// } +// +// public ListNode removeNthFromEnd(ListNode head, int n) { +// // 创建一个虚拟头节点(dummy node),便于处理边界情况 +// ListNode dummy = new ListNode(0); +// dummy.next = head; +// +// // 初始化两个指针,start和end,均指向虚拟头节点 +// ListNode start = dummy; +// ListNode end = dummy; +// +// // 将end指针向前移动n+1步,以便使start和end之间的距离为n +// for (int i = 0; i <= n; i++) { +// end = end.next; +// } +// +// // 将start和end指针同时向前移动,直到end指针到达链表末尾 +// while (end != null) { +// start = start.next; +// end = end.next; +// } +// +// // 删除start.next指向的节点 +// start.next = start.next.next; +// +// // 返回虚拟头节点的下一个节点,即新的链表头 +// return dummy.next; +// } +// +// public ListNode swapPairs(ListNode head) { +// // 虚拟头 +// ListNode dummy = new ListNode(0); +// dummy.next = head; +// +// // prev 指向待交换对的前驱 +// ListNode prev = dummy; +// while (prev.next != null && prev.next.next != null) { +// // 待交换的两个节点 +// ListNode a = prev.next; +// ListNode b = a.next; +// +// // 交换:prev->b->a->b.next +// prev.next = b; +// a.next = b.next; +// b.next = a; +// +// // 移动 prev 到下一对的前驱 +// prev = a; +// } +// +// return dummy.next; +// } +// +// +// public int find(int[] nums) { +// int slow = nums[0]; +// int fast = nums[0]; +// do { +// slow = nums[slow]; +// fast = nums[nums[fast]]; +// } while (slow != fast); +// +// slow = nums[0]; +// while (slow != fast) { +// slow = nums[slow]; +// fast = nums[fast]; +// } +// return slow; +// } +// +// public int find(int[] nums) { +// int left = 0; +// int right = nums.length - 1; +// while (left < right) { +// int mid = left + (right - left) / 2; +// +// int count = 0; +// for (int i = 0; i < nums.length; i++) { +// if (nums[i] <= mid) { +// count++; +// } +// } +// if (count <= mid) { +// left = mid + 1; +// } else { +// right = mid; +// } +// } +// return left; +// } +// +// +// +// public void next(int[] nums) { +// int n = nums.length; +// int i = n - 2; +// while (i >= 0 && nums[i] >= nums[i + 1]) { +// i--; +// } +// +// if (i >= 0) { +// int j = n - 1; +// while (nums[j]<= nums[i]) { +// j--; +// } +// swap(nums, i, j); +// } +// +// re(nums, i + 1, n - 1); +// } +// +// public void next(int[] nums) { +// // 下一个 +// int n = nums.length; +// int i = n - 2; +// while (i >= 0) && nums[i] >= nums[i + 1]) { +// i--; +// } +// +// if (i >= 0) { +// int j = n - 1; +// while (nums[j] <= nums[i]) { +// j--; +// +// swap(nums, i, j); +// } +// } +// +// re(nums, i +1 , n - 1); +// } +// +// +// public void sortColor(int[] nums) { +// int zero = 0; +// int two = nums.length - 1; +// int cur = 0; +// while (cur <= two) { +// if (nums[cur] == 0) { +// swap(nums, cur, zero); +// zero++; +// cur++; +// } else if (nums[cur] == 2) { +// swap(nums, cur, two); +// two--; +// } else { +// cur++; +// } +// } +// } +// +// public int fnnfnfn(int[] nums) { +// int can = 0; +// int count = 0; +// for (int num : nums) { +// if (count == 0) { +// can = num; +// } +// if (can == num) { +// count++; +// } else { +// count--; +// } +// } +// return can; +// } +// +// public int uqueu(int[] nums) { +// int unique = 0; +// for (int num : nums) { +// unique ^= num; +// } +// return unique; +// } +// +// public int min change(String s1, String s2) { +// int m = s1.length(); +// int n = s2.length(); +// +// int[][] dp = new int[m + 1][n + 1]; +// +// for (int i = 0; i <= m; i++) { +// dp[i][0] = i; +// } +// +// for (int i = 0; i <= n ; i++) { +// dp[0][i] = i; +// } +// +// for (int i = 1; i <= m; i++) { +// for (int j = 1; j <= n; j++) { +// if (s1.charAt(i - 1) == s2.charAt(j - 1)) { +// dp[i][j] = dp[i - 1][j - 1]; +// } else { +// dp[i][j] = Math.min(dp[i - 1][j - 1] + 1, Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); +// } +// } +// } +// +// return dp[m][n]; +// } +// +// public int lohshheh(String s1, String s2) { +// int m = s1.length(); +// int n = s2.length(); +// int[][] dp = new int[m + 1][n + 1]; +// for (int i = 1; i <= m; i++) { +// for (int j = 1; j <= n; j++) { +// if (s1.charAt(i - 1) == s2.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 String longggs(String s) { +// int n = s.length(); +// if (n == 0 || n == 1) return s; +// +// int start = 0; +// int end = 0; +// for (int i = 0; i < n; i++) { +// +// int len1 = expandAroundCenter(s, i, i); +// int len2 = expandAroundCenter(s, i, i + 1); +// int len = Math.max(len1, len2); +// if (len > end - start) { +// start = i - (len - 1) / 2; +// end = i + len / 2; +// } +// return s.substring(start, end + 1); +// } +// } +// +// public static int expandAroundCenter(String s, int left, int right) { +// int L = left; +// int R = right; +// while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { +// L--; +// R++; +// } +// return R - L - 1; +// } +// +// +// public int minPath(int[][] grid) { +// if (grid == null || grid.length == 0) return 0; +// int m = grid.length; +// int n = grid[0].length; +// int[][] dp = new int[m][n]; +// dp[0][0] = grid[0][0]; +// for (int i = 1; i < m; i++) { +// dp[i][0] = dp[i - 1][0] + grid[i][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++) { +// for (int j = 1; j < n; j++) { +// dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; +// } +// } +// +// return dp[m - 1][n - 1]; +// } +// +// +// public int uque(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; +// } +// 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 int longestValidParenthesesStack(String s) { +// if (s == null || s.length() <= 1) return 0; +// +// Stack stack = new Stack<>(); +// // 栈底初始化为-1,作为边界 +// stack.push(-1); +// int maxLength = 0; +// +// for (int i = 0; i < s.length(); i++) { +// if (s.charAt(i) == '(') { +// // 遇到'(',将其下标入栈 +// stack.push(i); +// } else { +// // 遇到')',弹出栈顶元素 +// stack.pop(); +// +// if (stack.isEmpty()) { +// // 如果栈为空,说明当前')'无法匹配 +// // 将其下标入栈作为新的边界 +// stack.push(i); +// } else { +// // 如果栈不为空,计算当前有效长度 +// int currentLength = i - stack.peek(); +// maxLength = Math.max(maxLength, currentLength); +// } +// } +// } +// +// return maxLength; +// } +// +// public boolean canpa(int[] nums) { +// int sum = 0; +// for (int num : nums) { +// sum += num; +// } +// if (sum % 2 != 0) { +// return false; +// } +// int target = sum / 2; +// boolean[] dp = new boolean[target + 1]; +// dp[0] = true; +// +// for (int num : nums) { +// for (int i = target; i >= num; i--) { +// dp[i] = dp[i] || dp[i - num]; +// } +// } +// return dp[target]; +// } +// +// public int maxProductStandard(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++) { +// // 计算三种可能的乘积 +// int currentMax = maxProduct * nums[i]; +// int currentMin = minProduct * nums[i]; +// +// // 更新最大乘积和最小乘积 +// maxProduct = Math.max(nums[i], Math.max(currentMax, currentMin)); +// minProduct = Math.min(nums[i], Math.min(currentMax, currentMin)); +// +// // 更新全局最大值 +// result = Math.max(result, maxProduct); +// } +// +// return result; +// } +// +// +// public static int lengthOfLIS(int[] nums) { +// // 边界情况:空数组 +// if (nums.length == 0) return 0; +// +// // dp[i] 表示以 nums[i] 结尾的最长严格递增子序列的长度 +// int[] dp = new int[nums.length]; +// +// // 初始化 dp 数组 +// // 每个元素自身就是一个长度为 1 的子序列 +// for (int i = 0; i < nums.length; i++) { +// dp[i] = 1; +// } +// +// // 动态规划填充 dp 数组 +// // 外层循环遍历每个元素 +// for (int i = 1; i < nums.length; i++) { +// // 内层循环检查所有在i之前的元素 +// for (int j = 0; j < i; j++) { +// // 只考虑严格递增的情况:nums[i] > nums[j] +// if (nums[i] > nums[j]) { +// // 状态转移方程: +// // 以nums[i]结尾的最长递增子序列长度 = +// // max(当前值, 以nums[j]结尾的最长递增子序列长度 + 1) +// dp[i] = Math.max(dp[i], dp[j] + 1); +// } +// } +// } +// +// // 找到 dp 数组中的最大值,即为最长递增子序列的长度 +// int maxLength = 0; +// for (int length : dp) { +// maxLength = Math.max(maxLength, length); +// } +// +// return maxLength; +// } +// +// public boolean wordBreakOptimized(String s, List wordDict) { +// // 将字典转换为哈希集合,提高查找效率 +// Set wordSet = new HashSet<>(wordDict); +// +// // 计算字典中最长单词的长度 +// int maxLength = 0; +// for (String word : wordDict) { +// maxLength = Math.max(maxLength, word.length()); +// } +// +// // dp[i] 表示字符串s的前i个字符能否被拆分 +// boolean[] dp = new boolean[s.length() + 1]; +// +// // 初始化:空字符串可以被拆分 +// dp[0] = true; +// +// // 动态规划填充DP数组 +// for (int i = 1; i <= s.length(); i++) { +// // 从i-1开始向前检查,但不超过最长单词长度 +// for (int j = i - 1; j >= 0 && i - j <= maxLength; j--) { +// // 如果前j个字符可以拆分,且从j到i的子串在字典中,则前i个字符可以拆分 +// if (dp[j] && wordSet.contains(s.substring(j, i))) { +// dp[i] = true; +// break; // 找到一种可行方案即可退出内层循环 +// } +// } +// } +// +// // 返回整个字符串是否可以被拆分 +// return dp[s.length()]; +// } +// +// public int[] hhhs9 +//} diff --git a/algorithm/ThreeSum15.java b/algorithm/ThreeSum15.java new file mode 100644 index 0000000000000..486a9897d3cd0 --- /dev/null +++ b/algorithm/ThreeSum15.java @@ -0,0 +1,304 @@ +package com.funian.algorithm.algorithm; + +import com.sun.jdi.connect.AttachingConnector; + +import java.util.*; + +/** + * 三数之和(LeetCode 15) + * + * 时间复杂度:O(n²) + * - 数组排序:O(n log n) + * - 外层循环:O(n) + * - 内层双指针:O(n) + * - 总体时间复杂度:O(n log n) + O(n) × O(n) = O(n²) + * + * 空间复杂度:O(1)(不考虑返回结果占用的空间) + * - 排序可能需要 O(log n) 的递归栈空间 + * - 使用了常数级别的额外变量空间 + */ +public class ThreeSum15 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("输入数组:"); + + // 读取一行输入 + String line = scanner.nextLine(); + + // 按空格分割字符串得到字符串数组 + String[] strs = line.split(" "); + + // 获取数组长度 + int n = strs.length; + + // 创建整型数组 + int[] nums = new int[n]; + + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(strs[i]); + } + + // 调用 threeSum 方法查找所有和为0的三元组 + List> result = threeSum(nums); + + // 输出结果 + System.out.println("结果为:" + result); + } + + /** + * 查找所有和为0的不重复三元组 + * 给定一个整数数组,判断是否存在三个元素 a, b, c, + * 使得 a + b + c = 0,找出所有满足条件且不重复的三元组。 + * + * 算法思路: + * 1. 首先对数组进行排序 + * 2. 固定第一个数,用双指针在剩余数组中查找两数之和等于第一个数的相反数 + * 3. 通过跳过重复元素避免重复三元组 + * + * 示例过程(以数组 [-1,0,1,2,-1,-4] 为例): + * + * 排序后: [-4, -1, -1, 0, 1, 2] + * + * i=0, nums[0]=-4: target=4 + * left=1, right=5: sum=-1+2=1 < 4, left++ + * left=2, right=5: sum=-1+2=1 < 4, left++ + * ...无法找到和为4的两个数 + * + * i=1, nums[1]=-1: target=1 + * left=2, right=5: sum=-1+2=1 = 1, 找到[-1,-1,2] + * 继续查找直到left>=right + * + * i=2, nums[2]=-1 = nums[1]: 跳过重复元素 + * + * i=3, nums[3]=0: target=0 + * left=4, right=5: sum=1+2=3 > 0, right-- + * left=4, right=4: 结束 + * + * 最终结果: [[-1,-1,2], [-1,0,1]] + * + * 时间复杂度分析: + * - 数组排序:O(n log n),其中n为输入数组`nums`的长度 + * - 外层循环:O(n-2) = O(n),遍历数组固定第一个数 + * - 内层双指针:O(n-i-1) = O(n),对于每个固定的第一个数,双指针最多移动n次 + * - 总体时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 排序递归栈空间:O(log n) + * - 结果列表:O(k),k为结果数量,最坏情况下k = O(n³) + * - 其他变量:O(1) + * - 总空间复杂度:O(log n) + * + * @param nums 输入的整数数组 + * @return 所有和为0的不重复三元组列表 + */ + public static List> threeSum(int[] nums) { + // 存储结果的列表 + List> result = new ArrayList<>(); + + // 对数组进行排序,为使用双指针做准备 + Arrays.sort(nums); + + // 获取数组长度 + int n = nums.length; + + // 外层循环固定第一个数,从索引0到n-3 + for (int i = 0; i < n - 2; i++) { + // 跳过重复元素,避免出现重复的三元组 + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + + // 设置双指针,left指向i+1,right指向数组末尾 + int left = i + 1; + int right = n - 1; + + // 双指针向中间移动寻找满足条件的组合 + while (left < right) { + // 计算三个数的和 + int sum = nums[i] + nums[left] + nums[right]; + + // 如果和为0,找到一个满足条件的三元组 + if (sum == 0) { + // 将三元组添加到结果列表中 + result.add(Arrays.asList(nums[i], nums[left], nums[right])); + + // 跳过重复元素,避免出现重复的三元组 + // 跳过左侧重复元素 + while (left < right && nums[left] == nums[left + 1]) { + left++; + } + // 跳过右侧重复元素 + while (left < right && nums[right] == nums[right - 1]) { + right--; + } + + // 继续寻找下一组可能的解 + left++; + right--; + } + // 如果和小于0,说明左侧元素太小,需要增大,移动左指针 + else if (sum < 0) { + left++; + } + // 如果和大于0,说明右侧元素太大,需要减小,移动右指针 + else { + right--; + } + } + } + + // 返回所有满足条件的三元组 + return result; + } + + /** + * 方法2:哈希表解法 + * + * 算法思路: + * 1. 固定第一个数 + * 2. 遍历第二个数,使用哈希表存储已访问的元素 + * 3. 对于每个第二个数,检查是否存在第三个数使得三数之和为0 + * + * 示例过程(以数组 [-1,0,1,2,-1,-4] 为例): + * + * 排序后: [-4, -1, -1, 0, 1, 2] + * + * i=0, nums[0]=-4: + * j=1, nums[1]=-1, complement=5, seen={-1} + * j=2, nums[2]=-1, complement=5, seen={-1} + * ...无匹配 + * + * i=1, nums[1]=-1: + * j=2, nums[2]=-1, complement=2, seen={-1} + * j=3, nums[3]=0, complement=1, seen={-1,0}, 1不在seen中 + * j=4, nums[4]=1, complement=0, seen={-1,0,1}, 0在seen中,找到[-1,0,1] + * j=5, nums[5]=2, complement=-1, seen={-1,0,1,2}, -1在seen中,找到[-1,-1,2] + * + * 时间复杂度分析: + * - 外层循环:O(n),其中n为输入数组`nums`的长度 + * - 内层循环:O(n-i-1) = O(n) + * - 哈希表操作:O(1),每次查找和插入操作 + * - 总时间复杂度:O(n²) + * + * 空间复杂度分析: + * - 哈希表存储:O(n-i-1) = O(n),最坏情况下存储n个元素 + * - 结果列表:O(k),k为结果数量 + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @return 所有和为0的不重复三元组列表 + */ + public static List> threeSumHash(int[] nums) { + List> result = new ArrayList<>(); + // 对数组进行排序,便于跳过重复元素 + Arrays.sort(nums); + + // 外层循环固定第一个数 + for (int i = 0; i < nums.length - 2; i++) { + // 跳过重复元素,避免出现重复的三元组 + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + + // 使用HashSet存储已访问的元素 + Set seen = new HashSet<>(); + + // 内层循环遍历第二个数 + for (int j = i + 1; j < nums.length; j++) { + // 计算需要的第三个数(补数) + int complement = -nums[i] - nums[j]; + + // 如果补数已在哈希表中,说明找到了一个三元组 + if (seen.contains(complement)) { + result.add(Arrays.asList(nums[i], complement, nums[j])); + + // 跳过重复元素 + while (j + 1 < nums.length && nums[j] == nums[j + 1]) { + j++; + } + } + + // 将当前元素添加到哈希表中 + seen.add(nums[j]); + } + } + + return result; + } + + /** + * 方法3:暴力解法(仅供学习对比,效率较低) + * + * 算法思路: + * 三重循环遍历所有可能的三元组组合,检查和是否为0 + * + * 示例过程(以数组 [-1,0,1,2,-1,-4] 为例): + * + * 排序后: [-4, -1, -1, 0, 1, 2] + * + * i=0, nums[0]=-4: + * j=1, nums[1]=-1: + * k=2, nums[2]=-1: sum=-4+(-1)+(-1)=-6 ≠ 0 + * k=3, nums[3]=0: sum=-4+(-1)+0=-5 ≠ 0 + * ... + * j=2, nums[2]=-1: + * k=3, nums[3]=0: sum=-4+(-1)+0=-5 ≠ 0 + * ... + * + * 需要检查 C(6,3) = 20 种组合 + * + * 时间复杂度分析: + * - 第一层循环:O(n),其中n为输入数组`nums`的长度 + * - 第二层循环:O(n-i-1) = O(n) + * - 第三层循环:O(n-j-1) = O(n) + * - 总时间复杂度:O(n³) + * + * 空间复杂度分析: + * - 结果列表:O(k),k为结果数量 + * - 其他变量:O(1) + * - 总空间复杂度:O(k) + * + * @param nums 输入的整数数组 + * @return 所有和为0的不重复三元组列表 + */ + public static List> threeSumBruteForce(int[] nums) { + List> result = new ArrayList<>(); + // 对数组进行排序,便于跳过重复元素 + Arrays.sort(nums); + + // 第一层循环固定第一个数 + for (int i = 0; i < nums.length - 2; i++) { + // 跳过重复元素 + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + + // 第二层循环固定第二个数 + for (int j = i + 1; j < nums.length - 1; j++) { + // 跳过重复元素 + if (j > i + 1 && nums[j] == nums[j - 1]) { + continue; + } + + // 第三层循环固定第三个数 + for (int k = j + 1; k < nums.length; k++) { + // 跳过重复元素 + if (k > j + 1 && nums[k] == nums[k - 1]) { + continue; + } + + // 检查三数之和是否为0 + if (nums[i] + nums[j] + nums[k] == 0) { + result.add(Arrays.asList(nums[i], nums[j], nums[k])); + } + } + } + } + + return result; + } +} diff --git a/algorithm/TopKFrequent347.java b/algorithm/TopKFrequent347.java new file mode 100644 index 0000000000000..aa77c85093f2f --- /dev/null +++ b/algorithm/TopKFrequent347.java @@ -0,0 +1,395 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.HashMap; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +/** + * 前K个高频元素(LeetCode 347) + * + * 时间复杂度: + * - 方法1(堆):O(n log k) + * 统计频率O(n),维护大小为k的堆O(n log k) + * - 方法2(桶排序):O(n) + * 统计频率O(n),桶排序O(n) + * + * 空间复杂度:O(n + k) + * - 哈希表存储频率O(n) + * - 堆或桶排序空间O(n + k) + */ +public class TopKFrequent347 { + + /** + * 方法1:最小堆解法 + * + * 算法思路: + * 1. 使用哈希表统计每个元素的频率 + * 2. 使用大小为k的最小堆维护前k个高频元素 + * 3. 遍历频率表,维护堆的大小为k + * 4. 堆中元素即为前k个高频元素 + * + * 执行过程分析(以`nums=[1,1,1,2,2,3]`, `k=2`为例): + * + * 第一步:统计频率 + * `frequencyMap` = {1:3, 2:2, 3:1} + * + * 第二步:维护大小为k的最小堆 + * 处理元素1,频率3:堆=[(1,3)] + * 处理元素2,频率2:堆=[(2,2), (1,3)] + * 处理元素3,频率1:堆=[(3,1), (2,2), (1,3)],大小超过k,弹出(3,1) + * 最终堆=[(2,2), (1,3)] + * + * 第三步:提取结果 + * 结果=[1,2](频率分别为3和2) + * + * 时间复杂度分析: + * - 统计频率:O(n) + * - 维护堆:O(n log k) + * - 提取结果:O(k log k) + * - 总时间复杂度:O(n log k) + * + * 空间复杂度分析: + * - frequencyMap:O(n) + * - minHeap:O(k) + * - result:O(k) + * - 总空间复杂度:O(n + k) + * + * @param nums 整数数组 + * @param k 前k个高频元素 + * @return 前k个高频元素数组 + */ + public int[] topKFrequentHeap(int[] nums, int k) { + // 统计每个元素的频率 + // Map frequencyMap = new HashMap<>() 创建频率映射 + Map frequencyMap = new HashMap<>(); + // for (int num : nums) 遍历数组统计频率 + for (int num : nums) { + // frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1) 更新元素频率 + frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); + } + + // 创建最小堆,按照频率排序 + // 堆中存储对 + // PriorityQueue> minHeap = new PriorityQueue<>((a, b) -> a.getValue() - b.getValue()) 创建最小堆 + PriorityQueue> minHeap = + new PriorityQueue<>((a, b) -> a.getValue() - b.getValue()); + + // 遍历频率表,维护大小为k的堆 + // for (Map.Entry entry : frequencyMap.entrySet()) 遍历频率映射 + for (Map.Entry entry : frequencyMap.entrySet()) { + // minHeap.offer(entry) 将元素加入堆 + minHeap.offer(entry); + + // 如果堆大小超过k,弹出频率最小的元素 + // if (minHeap.size() > k) 检查堆大小是否超过k + if (minHeap.size() > k) { + // minHeap.poll() 弹出堆顶元素 + minHeap.poll(); + } + } + + // 从堆中提取结果 + // int[] result = new int[k] 创建结果数组 + int[] result = new int[k]; + // for (int i = 0; i < k; i++) 提取前k个元素 + for (int i = 0; i < k; i++) { + // result[i] = minHeap.poll().getKey() 从堆中取出元素 + result[i] = minHeap.poll().getKey(); + } + + // return result 返回结果 + return result; + } + + /** + * 方法2:桶排序解法(更优解) + * + * 算法思路: + * 1. 使用哈希表统计每个元素的频率 + * 2. 创建桶数组,索引表示频率,值为具有该频率的元素列表 + * 3. 从高频率到低频率遍历桶,收集前k个元素 + * + * 执行过程分析(以`nums=[1,1,1,2,2,3]`, `k=2`为例): + * + * 第一步:统计频率 + * `frequencyMap` = {1:3, 2:2, 3:1} + * + * 第二步:创建桶数组(大小为`nums.length+1=7`) + * buckets[0] = [] + * buckets[1] = [3] + * buckets[2] = [2] + * buckets[3] = [1] + * buckets[4] = [] + * buckets[5] = [] + * buckets[6] = [] + * + * 第三步:从高频率到低频率收集元素 + * 从`buckets[6]`到`buckets[1]`遍历: + * buckets[3] = [1],收集1 + * buckets[2] = [2],收集2 + * 已收集2个元素,满足`k=2` + * + * 最终结果:[1,2] + * + * 时间复杂度分析: + * - 统计频率:O(n) + * - 初始化桶:O(n) + * - 放入桶:O(n) + * - 收集结果:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - frequencyMap:O(n) + * - buckets:O(n) + * - result:O(k) + * - 总空间复杂度:O(n + k) + * + * @param nums 整数数组 + * @param k 前k个高频元素 + * @return 前k个高频元素数组 + */ + public int[] topKFrequentBucketSort(int[] nums, int k) { + // 统计每个元素的频率 + // Map frequencyMap = new HashMap<>() 创建频率映射 + Map frequencyMap = new HashMap<>(); + // for (int num : nums) 遍历数组统计频率 + for (int num : nums) { + // frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1) 更新元素频率 + frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); + } + + // 创建桶数组,索引表示频率 + // 桶的数量为`nums.length+1`,因为频率最大为`nums.length` + // List[] buckets = new List[nums.length + 1] 创建桶数组 + List[] buckets = new List[nums.length + 1]; + + // 初始化桶 + // for (int i = 0; i <= nums.length; i++) 初始化每个桶 + for (int i = 0; i <= nums.length; i++) { + // buckets[i] = new ArrayList<>() 创建空列表 + buckets[i] = new ArrayList<>(); + } + + // 将元素放入对应频率的桶中 + // for (Map.Entry entry : frequencyMap.entrySet()) 遍历频率映射 + for (Map.Entry entry : frequencyMap.entrySet()) { + // int frequency = entry.getValue() 获取频率 + int frequency = entry.getValue(); + // int num = entry.getKey() 获取元素 + int num = entry.getKey(); + // buckets[frequency].add(num) 将元素加入对应频率的桶 + buckets[frequency].add(num); + } + + // 从高频率到低频率收集前k个元素 + // List result = new ArrayList<>() 创建结果列表 + List result = new ArrayList<>(); + // for (int i = buckets.length - 1; i >= 0 && result.size() < k; i--) 从高频率到低频率遍历桶 + for (int i = buckets.length - 1; i >= 0 && result.size() < k; i--) { + // result.addAll(buckets[i]) 将桶中所有元素加入结果 + result.addAll(buckets[i]); + } + + // 转换为数组并返回前k个元素 + // return result.stream().mapToInt(i -> i).toArray() 转换为int数组 + return result.stream().mapToInt(i -> i).toArray(); + } + + /** + * 方法3:快速选择解法 + * + * 算法思路: + * 1. 使用哈希表统计频率 + * 2. 将频率对转换为数组 + * 3. 使用快速选择算法找到第k大的频率 + * 4. 收集频率大于等于第k大频率的所有元素 + * + * 时间复杂度分析: + * - 统计频率:O(n) + * - 转换数组:O(n) + * - 快速选择:平均O(n),最坏O(n²) + * - 收集结果:O(n) + * - 总时间复杂度:平均O(n),最坏O(n²) + * + * 空间复杂度分析: + * - frequencyMap:O(n) + * - entries:O(n) + * - result:O(k) + * - 总空间复杂度:O(n + k) + * + * @param nums 整数数组 + * @param k 前k个高频元素 + * @return 前k个高频元素数组 + */ + public int[] topKFrequentQuickSelect(int[] nums, int k) { + // 统计频率 + // Map frequencyMap = new HashMap<>() 创建频率映射 + Map frequencyMap = new HashMap<>(); + // for (int num : nums) 遍历数组统计频率 + for (int num : nums) { + // frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1) 更新元素频率 + frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1); + } + + // 转换为数组 + // List> entries = new ArrayList<>(frequencyMap.entrySet()) 转换为列表 + List> entries = new ArrayList<>(frequencyMap.entrySet()); + + // 使用快速选择找到第k大的频率 + // int threshold = quickSelect(entries, 0, entries.size() - 1, k) 找到第k大的频率 + int threshold = quickSelect(entries, 0, entries.size() - 1, k); + + // 收集频率大于等于阈值的元素 + // List result = new ArrayList<>() 创建结果列表 + List result = new ArrayList<>(); + // for (Map.Entry entry : entries) 遍历所有元素 + for (Map.Entry entry : entries) { + // if (entry.getValue() >= threshold) 检查频率是否大于等于阈值 + if (entry.getValue() >= threshold) { + // result.add(entry.getKey()) 将元素加入结果 + result.add(entry.getKey()); + } + } + + // return result.stream().mapToInt(i -> i).toArray() 转换为int数组 + return result.stream().mapToInt(i -> i).toArray(); + } + + /** + * 快速选择辅助方法 + * + * 时间复杂度分析: + * - 平均情况:O(n) + * - 最坏情况:O(n²) + * + * 空间复杂度分析: + * - 递归调用栈:O(log n) + */ + private int quickSelect(List> entries, int left, int right, int k) { + // if (left == right) 递归终止条件 + if (left == right) return entries.get(left).getValue(); + + // java.util.Random random = new java.util.Random() 创建随机数生成器 + java.util.Random random = new java.util.Random(); + // int pivotIndex = left + random.nextInt(right - left + 1) 随机选择基准点 + int pivotIndex = left + random.nextInt(right - left + 1); + // pivotIndex = partition(entries, left, right, pivotIndex) 分区操作 + pivotIndex = partition(entries, left, right, pivotIndex); + + // if (pivotIndex == k - 1) 检查是否找到第k大元素 + if (pivotIndex == k - 1) { + // return entries.get(pivotIndex).getValue() 返回第k大频率 + return entries.get(pivotIndex).getValue(); + } else if (pivotIndex < k - 1) { + // return quickSelect(entries, pivotIndex + 1, right, k) 在右半部分查找 + return quickSelect(entries, pivotIndex + 1, right, k); + } else { + // return quickSelect(entries, left, pivotIndex - 1, k) 在左半部分查找 + return quickSelect(entries, left, pivotIndex - 1, k); + } + } + + /** + * 分区辅助方法 + * + * 时间复杂度分析: + * - O(n) + */ + private int partition(List> entries, int left, int right, int pivotIndex) { + // int pivotValue = entries.get(pivotIndex).getValue() 获取基准值 + int pivotValue = entries.get(pivotIndex).getValue(); + // Collections.swap(entries, pivotIndex, right) 将基准元素移到末尾 + Collections.swap(entries, pivotIndex, right); + + // int storeIndex = left 初始化存储索引 + int storeIndex = left; + // for (int i = left; i < right; i++) 遍历元素 + for (int i = left; i < right; i++) { + // if (entries.get(i).getValue() > pivotValue) 比较元素与基准值 + if (entries.get(i).getValue() > pivotValue) { + // Collections.swap(entries, storeIndex, i) 交换元素 + Collections.swap(entries, storeIndex, i); + // storeIndex++ 更新存储索引 + storeIndex++; + } + } + + // Collections.swap(entries, storeIndex, right) 将基准元素放到正确位置 + Collections.swap(entries, storeIndex, right); + // return storeIndex 返回基准元素最终位置 + return storeIndex; + } + + /** + * 辅助方法:读取用户输入的数组 + * + * 时间复杂度分析: + * - O(n) + * + * 空间复杂度分析: + * - O(n) + * + * @return 用户输入的整数数组 + */ + public static int[] readArray() { + // Scanner scanner = new Scanner(System.in) 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // System.out.println("请输入整数数组(用空格分隔):") 打印提示信息 + System.out.println("请输入整数数组(用空格分隔):"); + // String input = scanner.nextLine() 读取输入 + String input = scanner.nextLine(); + // String[] strArray = input.split(" ") 分割字符串 + String[] strArray = input.split(" "); + + // int[] nums = new int[strArray.length] 创建整数数组 + int[] nums = new int[strArray.length]; + // for (int i = 0; i < strArray.length; i++) 遍历字符串数组 + for (int i = 0; i < strArray.length; i++) { + // nums[i] = Integer.parseInt(strArray[i]) 转换为整数 + nums[i] = Integer.parseInt(strArray[i]); + } + + // return nums 返回整数数组 + return nums; + } + + /** + * 主函数:处理用户输入并找出前K个高频元素 + */ + public static void main(String[] args) { + // System.out.println("前K个高频元素") 打印标题 + System.out.println("前K个高频元素"); + + // 读取用户输入的数组 + // int[] nums = readArray() 读取数组 + int[] nums = readArray(); + // System.out.println("输入数组: " + java.util.Arrays.toString(nums)) 打印数组 + System.out.println("输入数组: " + java.util.Arrays.toString(nums)); + + // 读取k值 + // Scanner scanner = new Scanner(System.in) 创建Scanner对象 + Scanner scanner = new Scanner(System.in); + // System.out.print("请输入k值: ") 打印提示信息 + System.out.print("请输入k值: "); + // int k = scanner.nextInt() 读取k值 + int k = scanner.nextInt(); + + // 计算前k个高频元素 + // TopKFrequent347 solution = new TopKFrequent347() 创建解决方案对象 + TopKFrequent347 solution = new TopKFrequent347(); + // int[] result1 = solution.topKFrequentHeap(nums, k) 调用堆方法 + int[] result1 = solution.topKFrequentHeap(nums, k); + // int[] result2 = solution.topKFrequentBucketSort(nums, k) 调用桶排序方法 + int[] result2 = solution.topKFrequentBucketSort(nums, k); + + // 输出结果 + // System.out.println("堆方法结果: " + java.util.Arrays.toString(result1)) 打印堆方法结果 + System.out.println("堆方法结果: " + java.util.Arrays.toString(result1)); + // System.out.println("桶排序方法结果: " + java.util.Arrays.toString(result2)) 打印桶排序方法结果 + System.out.println("桶排序方法结果: " + java.util.Arrays.toString(result2)); + } +} diff --git a/algorithm/TrapWater42.java b/algorithm/TrapWater42.java new file mode 100644 index 0000000000000..1a18fd82ce18d --- /dev/null +++ b/algorithm/TrapWater42.java @@ -0,0 +1,287 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; + +/** + * 接雨水问题(LeetCode 42) + * + * 时间复杂度:O(n) + * - 使用双指针技术,每个元素最多被访问一次 + * - 总共需要遍历 n 个元素 + * + * 空间复杂度:O(1) + * - 只使用了常数级别的额外空间 + * - 没有使用与输入数组大小相关的额外存储空间 + */ +public class TrapWater42 { + public static void main(String[] args) { + // 创建 Scanner 对象读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入整数数组 + System.out.print("请输入柱子的高度数组(用空格分隔):"); + + // 读取用户输入的一行字符串 + String line = scanner.nextLine(); + + // 通过空格分隔字符串 + String[] str = line.split(" "); + + // 获取输入的柱子数量 + int n = str.length; + + // 创建高度数组 + int[] height = new int[n]; + + // 将每个字符串转换为整数并存入高度数组 + for (int i = 0; i < n; i++) { + height[i] = Integer.parseInt(str[i]); + } + + // 调用 trap 方法计算积水量 + int result = trap(height); + + // 输出结果 + System.out.println("可以接的雨水量为:" + result); + } + + /** + * 计算可以接住的雨水量 + * 使用双指针技术,从数组两端向中间遍历 + * + * 算法思路: + * 1. 使用两个指针分别从数组的两端开始 + * 2. 维护左侧最大高度和右侧最大高度 + * 3. 每次移动较短边的指针,因为积水高度由较短边决定 + * 4. 累加每个位置能够接住的雨水量 + * + * 核心原理: + * 每个位置能接的雨水量 = min(左侧最大高度, 右侧最大高度) - 当前位置高度 + * 使用双指针技巧,始终移动较短边的指针,因为我们知道该位置的积水只取决于较短边 + * + * 示例过程(以数组 [0,1,0,2,1,0,1,3,2,1,2,1] 为例): + * 索引: 0 1 2 3 4 5 6 7 8 9 10 11 + * 高度: 0 1 0 2 1 0 1 3 2 1 2 1 + * + * 初始状态: left=0, right=11, leftMax=0, rightMax=0, maxwater=0 + * + * 步骤1: height[0]=0 < height[11]=1 + * leftMax = max(0, 0) = 0 + * 积水 = leftMax - height[0] = 0 - 0 = 0 + * maxwater = 0 + 0 = 0 + * left++ → left=1 + * + * 步骤2: height[1]=1 > height[11]=1 + * rightMax = max(0, 1) = 1 + * 积水 = rightMax - height[11] = 1 - 1 = 0 + * maxwater = 0 + 0 = 0 + * right-- → right=10 + * + * 步骤3: height[1]=1 < height[10]=2 + * leftMax = max(0, 1) = 1 + * 积水 = leftMax - height[1] = 1 - 1 = 0 + * maxwater = 0 + 0 = 0 + * left++ → left=2 + * + * 步骤4: height[2]=0 < height[10]=2 + * leftMax = max(1, 0) = 1 + * 积水 = leftMax - height[2] = 1 - 0 = 1 + * maxwater = 0 + 1 = 1 + * left++ → left=3 + * + * ...继续执行直到 left >= right + * + * 时间复杂度分析: + * - 双指针遍历数组:O(n),其中n为输入数组`height`的长度 + * - 每次循环进行常数时间操作:O(1) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 只使用了几个变量存储指针和最大高度:O(1) + * - 没有使用额外数组:O(1) + * - 总空间复杂度:O(1) + * + * @param height 表示柱子高度的数组 + * @return 可以接住的雨水总量 + */ + public static int trap(int[] height) { + // 用于存储积水总量 + int maxwater = 0; + + // 初始化左右指针,分别指向数组的两端 + int left = 0, right = height.length - 1; + + // 左侧和右侧的最大高度初始化为0 + int leftMax = 0, rightMax = 0; + + // 当左右指针没有重合时继续循环 + while (left < right) { + // 更新左侧和右侧最大高度 + leftMax = Math.max(leftMax, height[left]); + rightMax = Math.max(rightMax, height[right]); + + // 如果左侧柱子的高度小于右侧柱子的高度 + if (height[left] < height[right]) { + // 计算左侧当前柱子可以接住的雨水量,等于leftMax - height[left] + // 由于左侧高度较小,当前位置的积水高度由左侧最大高度决定 + maxwater += leftMax - height[left]; + // 左指针右移,处理下一个柱子 + left++; + } else { + // 右侧柱子更高或相等,计算右侧当前柱子可以接住的雨水量,等于rightMax - height[right] + // 由于右侧高度较小或相等,当前位置的积水高度由右侧最大高度决定 + maxwater += rightMax - height[right]; + // 右指针左移,处理下一个柱子 + right--; + } + } + + // 返回最终的积水总量 + return maxwater; + } + + /** + * 方法2:动态规划解法 + * + * 算法思路: + * 1. 预处理:计算每个位置左侧和右侧的最大高度 + * 2. 对于每个位置,积水高度 = min(左侧最大高度, 右侧最大高度) - 当前位置高度 + * + * 示例过程(以数组 [0,1,0,2,1,0,1,3,2,1,2,1] 为例): + * + * 1. 计算左侧最大高度数组: + * leftMax[0] = 0 + * leftMax[1] = max(0,1) = 1 + * leftMax[2] = max(1,0) = 1 + * ... + * leftMax = [0,1,1,2,2,2,2,3,3,3,3,3] + * + * 2. 计算右侧最大高度数组: + * rightMax[11] = 1 + * rightMax[10] = max(1,2) = 2 + * rightMax[9] = max(2,1) = 2 + * ... + * rightMax = [3,3,3,3,3,3,3,3,2,2,2,1] + * + * 3. 计算每个位置的积水量: + * i=0: min(0,3)-0 = 0 + * i=1: min(1,3)-1 = 0 + * i=2: min(1,3)-0 = 1 + * ... + * 总积水量 = 0+0+1+0+1+2+1+0+0+1+0+0 = 6 + * + * 时间复杂度分析: + * - 计算左侧最大高度数组:O(n),其中n为输入数组`height`的长度 + * - 计算右侧最大高度数组:O(n) + * - 计算总积水量:O(n) + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 左侧最大高度数组:O(n),存储n个元素 + * - 右侧最大高度数组:O(n),存储n个元素 + * - 其他变量:O(1) + * - 总空间复杂度:O(n) + * + * @param height 表示柱子高度的数组 + * @return 可以接住的雨水总量 + */ + public static int trapDP(int[] height) { + if (height == null || height.length == 0) { + return 0; + } + + int n = height.length; + // leftMax[i] 表示位置i左侧(包括i)的最大高度 + int[] leftMax = new int[n]; + // rightMax[i] 表示位置i右侧(包括i)的最大高度 + int[] rightMax = new int[n]; + + // 计算每个位置左侧的最大高度 + leftMax[0] = height[0]; + for (int i = 1; i < n; i++) { + leftMax[i] = Math.max(leftMax[i - 1], height[i]); + } + + // 计算每个位置右侧的最大高度 + rightMax[n - 1] = height[n - 1]; + for (int i = n - 2; i >= 0; i--) { + rightMax[i] = Math.max(rightMax[i + 1], height[i]); + } + + // 计算总积水量 + int totalWater = 0; + for (int i = 0; i < n; i++) { + // 每个位置的积水 = min(左侧最大高度, 右侧最大高度) - 当前高度 + totalWater += Math.min(leftMax[i], rightMax[i]) - height[i]; + } + + return totalWater; + } + + /** + * 方法3:单调栈解法 + * + * 算法思路: + * 使用单调递减栈存储索引,当遇到较高柱子时计算积水 + * + * 示例过程(以数组 [0,1,0,2,1,0,1,3,2,1,2,1] 为例): + * + * 栈中存储的是索引,维护单调递减特性: + * + * i=0: height[0]=0, 栈为空,入栈 [0] + * i=1: height[1]=1 > height[0]=0, 形成凹槽 + * 弹出0作为底部,栈为空,无法计算积水,1入栈 [1] + * i=2: height[2]=0 < height[1]=1, 2入栈 [1,2] + * i=3: height[3]=2 > height[2]=0, 形成凹槽 + * 弹出2作为底部,栈顶1,计算积水: width=3-1-1=1, height=min(1,2)-0=1, area=1 + * 继续比较height[3]=2 > height[1]=1 + * 弹出1作为底部,栈为空,无法计算积水,3入栈 [3] + * ... + * + * 时间复杂度分析: + * - 每个元素最多入栈和出栈一次:O(n),其中n为输入数组`height`的长度 + * - 总时间复杂度:O(n) + * + * 空间复杂度分析: + * - 栈空间:最坏情况O(n),当数组单调递减时 + * - 其他变量:O(1) + * - 总空间复杂度:O(n) + * + * @param height 表示柱子高度的数组 + * @return 可以接住的雨水总量 + */ + public static int trapStack(int[] height) { + if (height == null || height.length == 0) { + return 0; + } + + java.util.Stack stack = new java.util.Stack<>(); + int totalWater = 0; + + for (int i = 0; i < height.length; i++) { + // 当栈不为空且当前柱子高度大于栈顶柱子高度时 + while (!stack.isEmpty() && height[i] > height[stack.peek()]) { + // 弹出栈顶元素作为底部 + int bottom = stack.pop(); + + // 如果栈为空,无法形成凹槽,跳出循环 + if (stack.isEmpty()) { + break; + } + + // 计算积水宽度和高度 + int width = i - stack.peek() - 1; + int minHeight = Math.min(height[stack.peek()], height[i]); + int waterHeight = minHeight - height[bottom]; + + // 累加积水量 + totalWater += width * waterHeight; + } + + // 将当前索引入栈 + stack.push(i); + } + + return totalWater; + } +} diff --git a/algorithm/TwoSum1.java b/algorithm/TwoSum1.java new file mode 100644 index 0000000000000..8074268229107 --- /dev/null +++ b/algorithm/TwoSum1.java @@ -0,0 +1,104 @@ +package com.funian.algorithm.algorithm; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * 两数之和 + * + * 时间复杂度:O(n) + * - 只需要遍历数组一次 + * - 哈希表的查找和插入操作平均时间复杂度为 O(1) + * + * 空间复杂度:O(n) + * - 需要使用哈希表存储数组元素及其索引,最坏情况下需要存储所有元素 + */ +public class TwoSum1 { + public static void main(String[] args) { + // 创建 Scanner 对象用于读取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入数组 + System.out.println("输出数组:"); + // 读取一行输入 + String line = scanner.nextLine(); + // 按空格分割字符串得到字符串数组 + String[] strs = line.split(" "); + + // 获取数组长度 + int n = strs.length; + // 创建整型数组 + int[] nums = new int[n]; + // 将字符串数组转换为整型数组 + for (int i = 0; i < n; i++) { + nums[i] = Integer.parseInt(strs[i]); + } + + // 提示用户输入目标值 + System.out.println("输入目标值:"); + // 读取目标值 + int target = scanner.nextInt(); + + // 调用 twoSum 方法查找两数之和 + int[] result = twoSum(nums, target); + // 输出结果 + System.out.println("结果为:" + result[0] + " " + result[1]); + } + + /** + * 两数之和 + * 在给定数组中找到两个数,使其和等于目标值 + *

+ * 算法思路: + * 使用哈希表存储已遍历过的元素及其索引 + * 对于每个元素,计算其补数(target - 当前元素) + * 如果补数已在哈希表中,则找到了答案 + * 否则将当前元素存入哈希表继续遍历 + *

+ * 示例过程(以数组 [2,7,11,15],target=9 为例): + *

+ * i=0: nums[0]=2, complement=9-2=7 + * map中不包含7,将(2,0)存入map + * map = {2:0} + *

+ * i=1: nums[1]=7, complement=9-7=2 + * map中包含2,返回[0,1] + *

+ * 结果:[0,1],因为nums[0]+nums[1]=2+7=9 + *

+ * 时间复杂度分析: + * - 数组遍历一次:O(n),其中n为输入数组nums的长度 + * - 哈希表查找和插入操作:平均O(1) + * - 总时间复杂度:O(n) + *

+ * 空间复杂度分析: + * - 哈希表存储空间:最坏情况下O(n),需要存储所有数组元素 + * - 其他变量占用常数空间:O(1) + * - 总空间复杂度:O(n) + * + * @param nums 输入的整数数组 + * @param target 目标和 + * @return 返回两个数的索引数组,如果未找到则返回 null + */ + public static int[] twoSum(int[] nums, int target) { + // 获取数组长度 + int n = nums.length; + // 创建哈希表存储数组元素和对应的索引 + Map map = new HashMap<>(); + // 遍历数组 + for (int i = 0; i < n; i++) { + // 计算当前元素的补数(目标值减去当前元素) + int complement = target - nums[i]; + // 如果哈希表中包含补数,说明找到了两个数 + if (map.containsKey(complement)) { + // 返回补数的索引和当前元素的索引 + return new int[]{map.get(complement), i}; + } + // 将当前元素和索引存入哈希表 + map.put(nums[i], i); + } + // 未找到符合条件的两个数,返回 null + return null; + } +} diff --git a/algorithm/UniquePaths62.java b/algorithm/UniquePaths62.java new file mode 100644 index 0000000000000..fb572acb64d09 --- /dev/null +++ b/algorithm/UniquePaths62.java @@ -0,0 +1,199 @@ +package com.funian.algorithm.algorithm; + +/** + * 不同路径(LeetCode 62)- 动态规划 + * + * 时间复杂度:O(m * n) + * - m是网格的行数,n是网格的列数 + * - 需要填充m*n的DP表 + * + * 空间复杂度:O(m * n) + * - 需要m*n的DP表存储中间结果 + * - 可以优化到O(n)或O(1)(使用数学公式) + */ +import java.util.Scanner; + +public class UniquePaths62 { + + /** + * 主函数:处理用户输入并计算不同路径的数量 + * + * 算法流程: + * 1. 读取用户输入的网格行数和列数 + * 2. 调用 [uniquePaths](file:///Users/funian/Documents/JavaProject/Algorithm/src/main/java/com/funian/algorithm/algorithm/UniquePaths62.java#L109-L144)方法计算不同路径的数量 + * 3. 输出结果 + */ + public static void main(String[] args) { + // 创建 Scanner 对象用于获取用户输入 + Scanner scanner = new Scanner(System.in); + + // 提示用户输入网格的行数和列数 + System.out.print("请输入网格的行数 m: "); + int m = scanner.nextInt(); + System.out.print("请输入网格的列数 n: "); + int n = scanner.nextInt(); + + // 调用 uniquePaths 方法计算不同路径的数量 + int result = uniquePaths(m, n); + // 输出结果 + System.out.println("从左上角到右下角的不同路径数量:" + result); + + // 演示其他解法 + UniquePaths62 solution = new UniquePaths62(); + int result2 = solution.uniquePathsOptimized(m, n); + int result3 = solution.uniquePathsMath(m, n); + System.out.println("空间优化版本结果:" + result2); + System.out.println("数学公式版本结果:" + result3); + } + + /** + * 计算从网格左上角到右下角的不同路径数量 + * + * 算法思路: + * 使用动态规划,定义`dp[i][j]`表示从起点(0,0)到位置(i,j)的不同路径数量 + * 状态转移方程: + * `dp[i][j] = dp[i-1][j] + dp[i][j-1]` + * (当前位置的路径数 = 从上方来的路径数 + 从左方来的路径数) + * + * 边界条件: + * 1. 第一行:`dp[0][j] = 1`(只能一直向右) + * 2. 第一列:`dp[i][0] = 1`(只能一直向下) + * + * 执行过程分析(以m=3, n=3为例): + * + * 初始化DP表: + * 1 1 1 + * 1 0 0 + * 1 0 0 + * + * 填充DP表: + * `dp[1][1] = dp[0][1] + dp[1][0] = 1 + 1 = 2` + * `dp[1][2] = dp[0][2] + dp[1][1] = 1 + 2 = 3` + * `dp[2][1] = dp[1][1] + dp[2][0] = 2 + 1 = 3` + * `dp[2][2] = dp[1][2] + dp[2][1] = 3 + 3 = 6` + * + * 最终DP表: + * 1 1 1 + * 1 2 3 + * 1 3 6 + * + * 结果:`dp[2][2] = 6` + * + * 路径示意图: + * 右→右→下→下 + * 右→下→右→下 + * 右→下→下→右 + * 下→右→右→下 + * 下→右→下→右 + * 下→下→右→右 + * + * 时间复杂度分析: + * - 初始化边界:O(m + n) + * - 填充DP表:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - DP表存储空间:O(m * n) + * + * @param m 网格行数 + * @param n 网格列数 + * @return 不同路径的数量 + */ + public static int uniquePaths(int m, int n) { + // 创建一个二维数组 dp,大小为 m x n + // dp[i][j] 表示从起点到位置(i,j)的不同路径数量 + 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]; + } + + /** + * 空间优化版本:只使用一维数组 + * + * 算法思路: + * 观察DP表的填充过程,发现每次只需要前一行的信息 + * 可以用一维数组代替二维数组 + * + * 时间复杂度分析: + * - 初始化数组:O(n) + * - 填充数组:O(m * n) + * - 总时间复杂度:O(m * n) + * + * 空间复杂度分析: + * - 一维数组存储空间:O(n) + * + * @param m 网格行数 + * @param n 网格列数 + * @return 不同路径的数量 + */ + public int uniquePathsOptimized(int m, int n) { + // 只需要一维数组,长度为列数 + int[] dp = new int[n]; + + // 初始化数组为1(第一行的值) + for (int j = 0; j < n; j++) { + dp[j] = 1; + } + + // 填充数组 + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + // dp[j] = dp[j] (上方的值) + dp[j-1] (左方的值) + dp[j] += dp[j - 1]; + } + } + + return dp[n - 1]; + } + + /** + * 数学公式版本:组合数学解法 + * + * 算法思路: + * 从(0,0)到(m-1,n-1)总共需要走(m-1)+(n-1) = m+n-2步 + * 其中需要向右走(n-1)步,向下走(m-1)步 + * 问题转化为:在m+n-2步中选择n-1步向右走的方案数 + * 即组合数C(m+n-2, n-1) + * + * 时间复杂度分析: + * - 计算组合数:O(min(m,n)) + * + * 空间复杂度分析: + * - 只使用常数额外空间:O(1) + * + * @param m 网格行数 + * @param n 网格列数 + * @return 不同路径的数量 + */ + public int uniquePathsMath(int m, int n) { + // 计算组合数C(m+n-2, min(m-1,n-1)) + // 为了避免计算大数阶乘,我们计算C(m+n-2, min(m-1,n-1)) + int totalSteps = m + n - 2; + int rightSteps = Math.min(m - 1, n - 1); + + long result = 1; + for (int i = 1; i <= rightSteps; i++) { + // 逐步计算组合数 + result = result * (totalSteps - rightSteps + i) / i; + } + + return (int) result; + } +} diff --git a/algorithm/WordBreak139.java b/algorithm/WordBreak139.java new file mode 100644 index 0000000000000..d22e2f6f65c08 --- /dev/null +++ b/algorithm/WordBreak139.java @@ -0,0 +1,257 @@ +package com.funian.algorithm.algorithm; + +import java.util.Scanner; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; + +/** + * 单词拆分(LeetCode 139)- 动态规划 + * + * 时间复杂度:O(n² * m) + * - n是字符串`s`的长度 + * - m是单词字典的大小 + * - 外层循环运行n次,内层循环运行n次,每次需要检查子串是否在字典中(最坏情况O(n)) + * + * 空间复杂度:O(n + m) + * - 需要长度为n+1的DP数组和存储字典的哈希集合 + */ +public class WordBreak139 { + + /** + * 判断字符串是否能被字典中的单词拆分 + * + * 算法思路: + * 使用动态规划,定义`dp[i]`表示字符串`s`的前i个字符能否被拆分 + * 状态转移方程: + * `dp[i] = OR(dp[j] && s.substring(j, i) in wordDict)` for all j where 0 <= j < i + * + * 执行过程分析(以`s="leetcode"`, `wordDict=["leet","code"]`为例): + * + * 初始化DP数组(n=8): + * dp = [T, F, F, F, F, F, F, F, F] + * 索引: 0 1 2 3 4 5 6 7 8 + * + * 填充DP数组: + * i=1: 检查子串"l",不在字典中,dp[1]=F + * i=2: 检查子串"le","l",都不在字典中,dp[2]=F + * i=3: 检查子串"lee","le","l",都不在字典中,dp[3]=F + * i=4: 检查子串"leet"(j=0)、"eet"(j=1)、"et"(j=2)、"t"(j=3) + * dp[0]=T且"leet"在字典中,dp[4]=T + * i=5: 检查子串"leetc"(j=0到4),都不满足条件,dp[5]=F + * i=6: 检查子串"leetco"(j=0到5),都不满足条件,dp[6]=F + * i=7: 检查子串"leetcod"(j=0到6),都不满足条件,dp[7]=F + * i=8: 检查子串"leetcode"(j=0到7) + * j=4时,dp[4]=T且"code"在字典中,dp[8]=T + * + * 最终结果:dp[8] = T,可以拆分 + * + * 时间复杂度分析: + * - 初始化DP数组:O(1) + * - 填充DP数组:O(n² * m) + * - 总时间复杂度:O(n² * m) + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * - 哈希集合存储空间:O(m) + * - 总空间复杂度:O(n + m) + * + * @param s 待拆分的字符串 + * @param wordDict 单词字典 + * @return 如果能被拆分返回true,否则返回false + */ + public boolean wordBreak(String s, List wordDict) { + // 将字典转换为哈希集合,提高查找效率 + Set wordSet = new HashSet<>(wordDict); + + // dp[i] 表示字符串s的前i个字符能否被拆分 + boolean[] dp = new boolean[s.length() + 1]; + + // 初始化:空字符串可以被拆分 + dp[0] = true; + + // 动态规划填充DP数组 + for (int i = 1; i <= s.length(); i++) { + // 尝试所有可能的分割点 + for (int j = 0; j < i; j++) { + // 如果前j个字符可以拆分,且从j到i的子串在字典中,则前i个字符可以拆分 + if (dp[j] && wordSet.contains(s.substring(j, i))) { + dp[i] = true; + break; // 找到一种可行方案即可退出内层循环 + } + } + } + + // 返回整个字符串是否可以被拆分 + return dp[s.length()]; + } + + /** + * 方法2:优化版本(提前终止) + * + * 算法思路: + * 记录字典中最长单词的长度,避免检查过长的子串 + * + * 时间复杂度分析: + * - 计算最长单词长度:O(m) + * - 填充DP数组:O(n * maxLen * m) + * - 总时间复杂度:O(n * maxLen * m),其中maxLen为字典中最长单词长度 + * + * 空间复杂度分析: + * - DP数组存储空间:O(n) + * - 哈希集合存储空间:O(m) + * - 总空间复杂度:O(n + m) + * + * @param s 待拆分的字符串 + * @param wordDict 单词字典 + * @return 如果能被拆分返回true,否则返回false + */ + public boolean wordBreakOptimized(String s, List wordDict) { + // 将字典转换为哈希集合,提高查找效率 + Set wordSet = new HashSet<>(wordDict); + + // 计算字典中最长单词的长度 + int maxLength = 0; + for (String word : wordDict) { + maxLength = Math.max(maxLength, word.length()); + } + + // dp[i] 表示字符串s的前i个字符能否被拆分 + boolean[] dp = new boolean[s.length() + 1]; + + // 初始化:空字符串可以被拆分 + dp[0] = true; + + // 动态规划填充DP数组 + for (int i = 1; i <= s.length(); i++) { + // 从i-1开始向前检查,但不超过最长单词长度 + for (int j = i - 1; j >= 0 && i - j <= maxLength; j--) { + // 如果前j个字符可以拆分,且从j到i的子串在字典中,则前i个字符可以拆分 + if (dp[j] && wordSet.contains(s.substring(j, i))) { + dp[i] = true; + break; // 找到一种可行方案即可退出内层循环 + } + } + } + + // 返回整个字符串是否可以被拆分 + return dp[s.length()]; + } + + /** + * 方法3:记忆化递归解法 + * + * 算法思路: + * 使用递归方式从字符串开头开始尝试所有可能的拆分, + * 并使用记忆化数组避免重复计算相同子问题 + * + * 时间复杂度分析: + * - 每个子问题只计算一次:O(n) + * - 每个子问题需要尝试所有可能的单词:O(m * n) + * - 总时间复杂度:O(n² * m) + * + * 空间复杂度分析: + * - 递归调用栈:O(n) + * - 记忆化数组:O(n) + * - 哈希集合存储空间:O(m) + * - 总空间复杂度:O(n + m) + * + * @param s 待拆分的字符串 + * @param wordDict 单词字典 + * @return 如果能被拆分返回true,否则返回false + */ + public boolean wordBreakMemo(String s, List wordDict) { + Set wordSet = new HashSet<>(wordDict); + Boolean[] memo = new Boolean[s.length() + 1]; + return wordBreakHelper(s, wordSet, 0, memo); + } + + /** + * 记忆化递归辅助方法 + * + * 算法思路: + * 递归地检查从start位置开始的子串是否可以被拆分 + * + * @param s 待拆分的字符串 + * @param wordSet 单词集合 + * @param start 当前检查的起始位置 + * @param memo 记忆化数组 + * @return 从start位置开始的子串是否可以被拆分 + */ + private boolean wordBreakHelper(String s, Set wordSet, int start, Boolean[] memo) { + if (start == s.length()) { + return true; + } + + if (memo[start] != null) { + return memo[start]; + } + + for (int end = start + 1; end <= s.length(); end++) { + if (wordSet.contains(s.substring(start, end)) && + wordBreakHelper(s, wordSet, end, memo)) { + memo[start] = true; + return true; + } + } + + memo[start] = false; + return false; + } + + /** + * 辅助方法:读取用户输入的字符串列表 + * + * 时间复杂度分析: + * - 读取和处理输入:O(k),k为输入字符串数量 + * + * 空间复杂度分析: + * - 存储字符串列表:O(k) + * + * @param prompt 提示信息 + * @return 字符串列表 + */ + public static List readStringList(String prompt) { + Scanner scanner = new Scanner(System.in); + System.out.println(prompt); + String input = scanner.nextLine(); + String[] strArray = input.split(" "); + + List list = new ArrayList<>(); + for (String str : strArray) { + list.add(str); + } + + return list; + } + + /** + * 主函数:处理用户输入并判断单词拆分 + */ + public static void main(String[] args) { + System.out.println("单词拆分问题"); + + // 读取用户输入的字符串 + Scanner scanner = new Scanner(System.in); + System.out.print("请输入待拆分的字符串: "); + String s = scanner.nextLine(); + + // 读取单词字典 + List wordDict = readStringList("请输入单词字典(用空格分隔):"); + System.out.println("待拆分字符串: \"" + s + "\""); + System.out.println("单词字典: " + wordDict); + + // 判断是否可以拆分 + WordBreak139 solution = new WordBreak139(); + boolean result1 = solution.wordBreak(s, wordDict); + boolean result2 = solution.wordBreakOptimized(s, wordDict); + boolean result3 = solution.wordBreakMemo(s, wordDict); + + // 输出结果 + System.out.println("动态规划方法结果: " + result1); + System.out.println("优化动态规划方法结果: " + result2); + System.out.println("记忆化递归方法结果: " + result3); + } +}