diff --git a/src/main/java/array/MaxSubArray.java b/src/main/java/array/MaxSubArray.java index d7252bc..1405a86 100644 --- a/src/main/java/array/MaxSubArray.java +++ b/src/main/java/array/MaxSubArray.java @@ -30,4 +30,31 @@ public class MaxSubArray { } return maxsum; } + //假设还要同时返回起始结束下标: + public int[] maxSubArray2(int[] nums) { + int tempsum = nums[0]; // 当前子数组的和 + int maxsum = nums[0]; // 最大子数组和 + int start = 0; // 最大子数组的起始下标 + int end = 0; // 最大子数组的结束下标 + int tempStart = 0; // 当前子数组的起始下标 + + for (int i = 1; i < nums.length; i++) { + if (tempsum < 0) { + tempsum = nums[i]; // 如果当前子数组和为负数,则重新从当前元素开始 + tempStart = i; // 更新当前子数组的起始下标 + } else { + tempsum += nums[i]; // 否则,继续扩展当前子数组 + } + + // 如果当前的子数组和大于之前的最大和,则更新最大和和区间的下标 + if (tempsum > maxsum) { + maxsum = tempsum; + start = tempStart; // 记录最大子数组的起始下标 + end = i; // 记录最大子数组的结束下标 + } + } + + // 返回最大子数组的和以及起始和结束下标 + return new int[] {maxsum, start, end}; + } } diff --git a/src/main/java/array/MaxSubarraySumCircular.java b/src/main/java/array/MaxSubarraySumCircular.java new file mode 100644 index 0000000..aab964d --- /dev/null +++ b/src/main/java/array/MaxSubarraySumCircular.java @@ -0,0 +1,56 @@ +package array; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * 题目: 918. 环形子数组的最大和 (maxSubarraySumCircular) + * 描述:给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。 + * + * 环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。 + * + * 子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。 + * + 示例 1: + 输入:nums = [1,-2,3,-2] + 输出:3 + 解释:从子数组 [3] 得到最大和 3 + * + * 链接:https://leetcode.cn/problems/maximum-sum-circular-subarray/ + + */ +//不会 +public class MaxSubarraySumCircular { + //很巧妙的思路 + public int maxSubarraySumCircular(int[] nums) { + int totalSum = 0; // 数组元素总和 + int currMax = 0; // Kadane 当前最大子数组和 + int maxSum = Integer.MIN_VALUE; // 全局最大子数组和 + + int currMin = 0; // Kadane 当前最小子数组和 + int minSum = Integer.MAX_VALUE; // 全局最小子数组和 + + for (int x : nums) { + // 累加总和 + totalSum += x; + + // —— 求最大子数组和(标准 Kadane) —— + // 要么把 x 加到前面的子数组上,要么从 x 重新开始 + currMax = Math.max(currMax + x, x); + maxSum = Math.max(maxSum, currMax); + + // —— 求最小子数组和(相反 Kadane) —— + currMin = Math.min(currMin + x, x); + minSum = Math.min(minSum, currMin); + } + + // 如果所有元素都为负数,maxSum 会是最大的负数, + // totalSum - minSum 会变成 0(等价于丢弃整个数组),不合法 + if (maxSum < 0) { + return maxSum; + } + + // 否则比较非环形和环形两种情况 + return Math.max(maxSum, totalSum - minSum); + } +} diff --git a/src/main/java/graph/FindOrder.java b/src/main/java/graph/FindOrder.java new file mode 100644 index 0000000..7a06ccb --- /dev/null +++ b/src/main/java/graph/FindOrder.java @@ -0,0 +1,55 @@ +package graph; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; + +/** + * 题目: 210. 课程表 II (findOrder) + * 描述:现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。 + * 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。 + * 返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。 + * + 示例 1: + 输入:numCourses = 2, prerequisites = [[1,0]] + 输出:[0,1] + 解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。 + + * 链接:https://leetcode.cn/problems/course-schedule-ii/ + + */ +public class FindOrder { + public int[] findOrder(int numCourses, int[][] prerequisites) { + int[] indegree=new int[numCourses]; + int[] res=new int[numCourses]; + int index=0; + // 构建图:graph.get(i) 存储课程 i 的后续课程 + List> graph = new ArrayList<>(); + // 初始化图 + for (int i = 0; i < numCourses; i++) { + graph.add(new ArrayList<>()); + } + Queuequeue=new ArrayDeque<>(); + for (int[] prerequisite : prerequisites) { + int course=prerequisite[0]; + int precourse=prerequisite[1]; + graph.get(precourse).add(course); + indegree[course]++; + } + for (int i = 0; i < numCourses; i++) { + if(indegree[i]==0)queue.offer(i); + } + while (!queue.isEmpty()){ + int cur=queue.poll(); + res[index++]=cur; + Listtp=graph.get(cur); + for (Integer course : tp) { + indegree[course]--; + if(indegree[course]==0) queue.offer(course); + } + } + if(index==numCourses)return res; + else return new int[0]; + } +} diff --git a/src/main/java/graph/LadderLength.java b/src/main/java/graph/LadderLength.java new file mode 100644 index 0000000..f7341ab --- /dev/null +++ b/src/main/java/graph/LadderLength.java @@ -0,0 +1,57 @@ +package graph; + +import java.util.*; + +/** + * 题目: 127. 单词接龙 (ladderLength) + * 描述:字典 wordList 中从单词 beginWord 到 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk: + * + * 每一对相邻的单词只差一个字母。 + * 对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。 + * sk == endWord + * 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。 + * + 示例 1: + 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] + 输出:5 + 解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。 + + * 链接:https://leetcode.cn/problems/word-ladder/ + + */ +//和433. 最小基因变化雷同 +public class LadderLength { + public int ladderLength(String beginWord, String endWord, List wordList) { + Setset=new HashSet<>(wordList); + Setvisited=new HashSet<>(); + Queue queue=new ArrayDeque<>(); + queue.add(beginWord); + visited.add(beginWord); + if(!set.contains(endWord))return 0; + int steps=1; + while (!queue.isEmpty()){ + int sz= queue.size(); + for (int i = 0; i < sz; i++) { + String cur=queue.poll(); + if(cur.equals(endWord)) + return steps; + char[] currArr = cur.toCharArray(); + for (int pos = 0; pos < currArr.length; pos++) { + char old=currArr[pos]; + for (int j = 0; j < 26; j++) { + char g= (char) ('a'+j); + currArr[pos]=g; + String next=new String(currArr); + if(set.contains(next)&&!visited.contains(next)){ + visited.add(next); + queue.offer(next); + } + } + currArr[pos]=old; + } + } + steps++; + } + return 0; + } +} diff --git a/src/main/java/graph/MinMutation.java b/src/main/java/graph/MinMutation.java new file mode 100644 index 0000000..a1054cc --- /dev/null +++ b/src/main/java/graph/MinMutation.java @@ -0,0 +1,100 @@ +package graph; + +import java.util.*; + +/** + * 题目: 433. 最小基因变化 (minMutation) + * 描述:基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。 + * 假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。 + * 例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。 + * 另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中) + * 给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。 + * 注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。 + * + 示例 1: + 输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"] + 输出:2 + + * 链接:https://leetcode.cn/problems/minimum-genetic-mutation/ + + */ +//不会 +public class MinMutation { + /** + * 准备工作 + * + * 将基因库 bank 中的有效基因串存入一个 Set valid,用于 O(1) 检查。 + * 如果 endGene 不在 valid 中,说明无法达到目标,直接返回 –1。 + * BFS 初始化 + * + * 使用队列 Queue 存放当前层(当前步数)所有可达的基因串。 + * 另用一个 Set visited 记录已经访问过的基因串,避免重复遍历。 + * 将起点 startGene 入队、标记已访问;将步数 steps 置 0。 + * 层次遍历 + * + * 每次遍历一层时,取出当前队列大小 sz,表示这一层共有 sz 个节点要处理; + * 对每个节点(基因串)进行: + * 若它等于 endGene,则返回当前 steps。 + * 否则,尝试对这条基因串的 8 个位置,逐个替换为 'A' / 'C' / 'G' / 'T' 中另一种字符,生成所有 “相差 1 位” 的新基因串 next; + * 如果 next 在 valid 且未被访问过,就将其入队并标记访问。 + * 步数递增 + * + * 当前层的所有节点都处理完后,steps++,进入下一层。 + * 直到队列为空仍未找到 endGene,返回 –1。 + * @param startGene + * @param endGene + * @param bank + * @return + */ + public int minMutation(String startGene, String endGene, String[] bank) { + // 1. 将 bank 放入 Set,便于快速查验 + Set valid = new HashSet<>(Arrays.asList(bank)); + // 如果目标不在 bank 中,无法达成 + if (!valid.contains(endGene)) { + return -1; + } + + // 2. BFS 初始化 + char[] genes = new char[] {'A', 'C', 'G', 'T'}; + Queue queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + + queue.offer(startGene); + visited.add(startGene); + int steps = 0; + + // 3. 开始层次遍历 + while (!queue.isEmpty()) { + int sz = queue.size(); + // 处理当前层中的所有节点 + for (int i = 0; i < sz; i++) { + String curr = queue.poll(); + // 找到目标,返回步数 + if (curr.equals(endGene)) { + return steps; + } + // 生成所有一位变异的基因串 + char[] currArr = curr.toCharArray(); + for (int pos = 0; pos < currArr.length; pos++) { + char old = currArr[pos]; + for (char g : genes) { + if (g == old) continue; // 跳过相同字符 + currArr[pos] = g; + String next = new String(currArr); + // 只有在 bank 且未访问过,才入队 + if (valid.contains(next) && !visited.contains(next)) { + queue.offer(next); + visited.add(next); + } + } + currArr[pos] = old; // 恢复现场,不然每个pos都变了 + } + } + // 当前层遍历完,步数+1 + steps++; + } + + // 队列空了也没找到,返回 -1 + return -1; + } +} diff --git a/src/main/java/graph/NumIslands.java b/src/main/java/graph/NumIslands.java index 3c303a4..e5f1227 100644 --- a/src/main/java/graph/NumIslands.java +++ b/src/main/java/graph/NumIslands.java @@ -1,7 +1,5 @@ package graph; -import java.util.Map; - /** * 题目: 200. 岛屿数量 (numIslands) * 描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 diff --git a/src/main/java/graph/Trie.java b/src/main/java/graph/Trie.java index a43b1cf..a3c7e35 100644 --- a/src/main/java/graph/Trie.java +++ b/src/main/java/graph/Trie.java @@ -12,47 +12,98 @@ package graph; */ //没搞懂 学习一下 + +/** + * 节点结构 + * + * 每个 Trie 对象代表一棵子树的根节点,内部维护一个 children[26] 数组,对应 26 个字母。 + * isEnd 标识该节点是否是某个已插入单词的末尾。 + * 插入 (insert) + * 从根节点出发,依次读取单词每个字符,计算索引 ch - 'a'。 + * 若对应子节点为空则新建,最后将游标节点标记为单词结尾。 + * + * 查找完整单词 (search) + * 调用内部方法 searchPrefix,找到单词走到的最后一个节点; + * 再检查该节点的 isEnd,为 true 时说明完整单词存在。 + * + * 前缀判断 (startsWith) + * 仍用 searchPrefix 定位到最后一个节点; + * 不需要检查 isEnd,只要节点不为 null 就说明有该前缀。 + * + * 共用方法 (searchPrefix) + * 将插入和查找里的公共逻辑抽取出来:遍历路径、空指针检查、节点推进。 + */ +//https://leetcode.cn/problems/implement-trie-prefix-tree/solutions/98390/trie-tree-de-shi-xian-gua-he-chu-xue-zhe-by-huwt/?envType=study-plan-v2&envId=top-interview-150 class Trie { + // children 数组保存当前节点的所有子节点,索引 0-25 分别对应 'a'-'z' private Trie[] children; + // isEnd 标志用于标识当前节点是否为一个完整单词的结尾 private boolean isEnd; + /** + * Initialize your data structure here. + */ public Trie() { - children = new Trie[26]; //这行代码只是 创建了一个长度为 26 的数组,对应 a ~ z。但是!数组里的元素初始都是 null,只有当需要用到某个字符时,才会创建对应的 Trie 节点。 + // 初始化 children 数组,初始时所有子节点都为 null + children = new Trie[26]; + // 初始化单词结尾标志为 false isEnd = false; } + /** + * Inserts a word into the trie. + */ public void insert(String word) { - Trie node = this; + Trie node = this; // 从根节点开始 for (int i = 0; i < word.length(); i++) { - char ch = word.charAt(i); - int index = ch - 'a'; + char ch = word.charAt(i); // 当前字符 + int index = ch - 'a'; // 计算在 children 数组中的索引 + // 如果对应字符的子节点不存在,则新建一个节点 if (node.children[index] == null) { node.children[index] = new Trie(); } + // 移动到子节点,继续处理下一个字符 node = node.children[index]; } + // 遍历完所有字符后,将当前节点标记为单词结尾 node.isEnd = true; } + /** + * Returns true if the word is in the trie. + */ public boolean search(String word) { + // 尝试查找完整单词对应的节点 Trie node = searchPrefix(word); + // 只有节点不为 null 且 isEnd 为 true 才说明单词存在 return node != null && node.isEnd; } + /** + * Returns true if there is any word in the trie that starts with the given prefix. + */ public boolean startsWith(String prefix) { + // 只要能找到 prefix 对应的最后一个节点,就说明存在该前缀 return searchPrefix(prefix) != null; } + /** + * 查找给定字符串对应的最后一个节点,若路径中断则返回 null。 + */ private Trie searchPrefix(String prefix) { - Trie node = this; + Trie node = this; // 从根节点开始 for (int i = 0; i < prefix.length(); i++) { char ch = prefix.charAt(i); - int index = ch - 'a'; + int index = ch - 'a'; // 计算字符索引 + // 若对应子节点不存在,则前缀/单词不在 Trie 中 if (node.children[index] == null) { return null; } + // 移动到子节点,继续检查下一个字符 node = node.children[index]; } + // 成功遍历所有字符,返回最后停留的节点 return node; } } + diff --git a/src/main/java/tree/WordDictionary.java b/src/main/java/tree/WordDictionary.java new file mode 100644 index 0000000..cc37845 --- /dev/null +++ b/src/main/java/tree/WordDictionary.java @@ -0,0 +1,87 @@ +package tree; + +import java.util.ArrayList; +import java.util.List; + +/** + * 题目: 添加与搜索单词 - 数据结构设计 (WordDictionary) + * 描述:请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。 + * + * 实现词典类 WordDictionary : + * + * WordDictionary() 初始化词典对象 + * void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配 + * bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。 + * + 示例 1: + 输入: + ["WordDictionary","addWord","addWord","addWord","search","search","search","search"] + [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]] + 输出: + [null,null,null,null,false,true,true,true] + + 解释: + WordDictionary wordDictionary = new WordDictionary(); + wordDictionary.addWord("bad"); + wordDictionary.addWord("dad"); + wordDictionary.addWord("mad"); + wordDictionary.search("pad"); // 返回 False + wordDictionary.search("bad"); // 返回 True + wordDictionary.search(".ad"); // 返回 True + wordDictionary.search("b.."); // 返回 True + + * 链接:https://leetcode.cn/problems/design-add-and-search-words-data-structure/ + + */ +public class WordDictionary { + // Trie 树的节点 + private WordDictionary[] children; + private boolean isEnd; + + public WordDictionary() { + children = new WordDictionary[26]; // 存储 26 个字母的子节点 + isEnd = false; // 默认节点不是单词的结尾 + } + + // 添加单词 + public void addWord(String word) { + WordDictionary node = this; + for (int i = 0; i < word.length(); i++) { + int index = word.charAt(i) - 'a'; // 计算字符的索引 + if (node.children[index] == null) { + node.children[index] = new WordDictionary(); // 如果子节点不存在,就创建一个新节点 + } + node = node.children[index]; // 移动到当前字符的子节点 + } + node.isEnd = true; // 单词的结尾标志 + } + + // 搜索单词,支持通配符 "." + public boolean search(String word) { + return searchInNode(word, this); // 从根节点开始搜索 + } + + // 辅助方法,递归遍历 Trie + private boolean searchInNode(String word, WordDictionary node) { + if (word.length() == 0) { + return node.isEnd; // 如果单词已遍历完,检查当前节点是否是一个完整单词 + } + + char firstChar = word.charAt(0); + if (firstChar == '.') { + // 如果是通配符 '.', 检查每个子节点 + for (int i = 0; i < 26; i++) { + if (node.children[i] != null && searchInNode(word.substring(1), node.children[i])) { //word.substring(1):从 word 字符串的第二个字符 + return true; // 如果任意一个子节点能匹配,返回 true + } + } + return false; // 如果所有子节点都无法匹配,返回 false + } else { + int index = firstChar - 'a'; // 计算字母的索引 + if (node.children[index] == null) { + return false; // 如果当前字符对应的子节点为空,返回 false + } + return searchInNode(word.substring(1), node.children[index]); // 向下递归检查 + } + } +}