diff --git a/16-LeetCode Hot 100/二叉树的中序遍历.md b/16-LeetCode Hot 100/二叉树的中序遍历.md new file mode 100644 index 0000000..146d516 --- /dev/null +++ b/16-LeetCode Hot 100/二叉树的中序遍历.md @@ -0,0 +1,37 @@ +# 二叉树的中序遍历 (Binary Tree Inorder Traversal) + +## 题目描述 + +给定一个二叉树的根节点,返回它的中序遍历。 + +## 解题思路 + +### 方法一:递归 + +### 方法二:迭代(栈) + +## Go 代码(迭代) + +```go +func inorderTraversal(root *TreeNode) []int { + result := []int{} + stack := []*TreeNode{} + curr := root + + for curr != nil || len(stack) > 0 { + for curr != nil { + stack = append(stack, curr) + curr = curr.Left + } + + curr = stack[len(stack)-1] + stack = stack[:len(stack)-1] + result = append(result, curr.Val) + curr = curr.Right + } + + return result +} +``` + +**复杂度:** O(n) 时间,O(n) 空间 diff --git a/16-LeetCode Hot 100/二叉树的最大深度.md b/16-LeetCode Hot 100/二叉树的最大深度.md new file mode 100644 index 0000000..970c143 --- /dev/null +++ b/16-LeetCode Hot 100/二叉树的最大深度.md @@ -0,0 +1,29 @@ +# 二叉树的最大深度 (Maximum Depth of Binary Tree) + +## 题目描述 + +给定一个二叉树,找出其最大深度。 + +## 解题思路 + +### DFS / BFS + +## Go 代码(DFS) + +```go +func maxDepth(root *TreeNode) int { + if root == nil { + return 0 + } + + left := maxDepth(root.Left) + right := maxDepth(root.Right) + + if left > right { + return left + 1 + } + return right + 1 +} +``` + +**复杂度:** O(n) 时间,O(h) 空间(h 为高度) diff --git a/16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md b/16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md new file mode 100644 index 0000000..885de51 --- /dev/null +++ b/16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md @@ -0,0 +1,41 @@ +# 从前序与中序遍历序列构造二叉树 + +## 题目描述 + +给定两个整数数组 preorder 和 inorder,其中 preorder 是二叉树的先序遍历,inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。 + +## 解题思路 + +### 递归构造 + +前序遍历:[根, [左子树], [右子树]] +中序遍历:[[左子树], 根, [右子树]] + +## Go 代码 + +```go +func buildTree(preorder []int, inorder []int) *TreeNode { + if len(preorder) == 0 { + return nil + } + + root := &TreeNode{Val: preorder[0]} + index := findIndex(inorder, preorder[0]) + + root.Left = buildTree(preorder[1:1+index], inorder[:index]) + root.Right = buildTree(preorder[1+index:], inorder[index+1:]) + + return root +} + +func findIndex(arr []int, target int) int { + for i, v := range arr { + if v == target { + return i + } + } + return -1 +} +``` + +**复杂度:** O(n²) 时间(可用哈希表优化到 O(n)),O(n) 空间 diff --git a/16-LeetCode Hot 100/单词搜索.md b/16-LeetCode Hot 100/单词搜索.md new file mode 100644 index 0000000..c7e7907 --- /dev/null +++ b/16-LeetCode Hot 100/单词搜索.md @@ -0,0 +1,175 @@ +# 单词搜索 (Word Search) + +## 题目描述 + +给定一个 `m x n` 二维字符网格 `board` 和一个字符串单词 `word`。如果 `word` 存在于网格中,返回 `true`;否则,返回 `false`。 + +单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 + +### 示例 + +**示例 1:** +``` +输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" +输出:true +``` + +**示例 2:** +``` +输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" +输出:true +``` + +**示例 3:** +``` +输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB" +输出:false +``` + +## 解题思路 + +### 方法一:DFS + 回溯(推荐) + +**核心思想:**对每个位置进行 DFS,搜索是否存在匹配的单词路径。 + +**算法步骤:** +1. 遍历网格的每个位置 +2. 如果当前位置字符匹配单词首字符,开始 DFS +3. DFS 过程中: + - 标记当前已访问 + - 向四个方向递归搜索 + - 如果找到完整单词,返回 true + - 回溯时撤销访问标记 + +## 代码实现 + +### Go 实现 + +```go +package main + +func exist(board [][]byte, word string) bool { + m, n := len(board), len(board[0]) + visited := make([][]bool, m) + for i := range visited { + visited[i] = make([]bool, n) + } + + var dfs func(i, j, k int) bool + dfs = func(i, j, k int) bool { + // 找到完整单词 + if k == len(word) { + return true + } + + // 边界检查或不匹配 + if i < 0 || i >= m || j < 0 || j >= n || + visited[i][j] || board[i][j] != word[k] { + return false + } + + // 标记访问 + visited[i][j] = true + + // 向四个方向搜索 + found := dfs(i+1, j, k+1) || + dfs(i-1, j, k+1) || + dfs(i, j+1, k+1) || + dfs(i, j-1, k+1) + + // 回溯:取消标记 + visited[i][j] = false + + return found + } + + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if board[i][j] == word[0] && dfs(i, j, 0) { + return true + } + } + } + + return false +} +``` + +### Java 实现 + +```java +public class Solution { + private boolean[][] visited; + private int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + public boolean exist(char[][] board, String word) { + int m = board.length, n = board[0].length; + visited = new boolean[m][n]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (board[i][j] == word.charAt(0) && dfs(board, word, i, j, 0)) { + return true; + } + } + } + return false; + } + + private boolean dfs(char[][] board, String word, int i, int j, int k) { + if (k == word.length()) { + return true; + } + + if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || + visited[i][j] || board[i][j] != word.charAt(k)) { + return false; + } + + visited[i][j] = true; + + for (int[] dir : directions) { + if (dfs(board, word, i + dir[0], j + dir[1], k + 1)) { + visited[i][j] = false; + return true; + } + } + + visited[i][j] = false; + return false; + } +} +``` + +## 复杂度分析 + +- **时间复杂度:** O(m × n × 4^L) + - m × n 是网格大小 + - L 是单词长度 + - 最坏情况每个位置都要搜索 4 个方向 + +- **空间复杂度:** O(L) + - 递归栈深度最大为 L + - visited 数组 O(m × n) + +## P7 加分项 + +### 变形题目:单词搜索 II + +**LeetCode 212:** 给定一个 m x n 二维字符网格 board 和一个单词列表 words,返回所有在二维网格和字典中出现的单词。 + +```go +func findWords(board [][]byte, words []string) []string { + // 构建 Trie 树 + trie := buildTrie(words) + result := []string{} + + for i := 0; i < len(board); i++ { + for j := 0; j < len(board[0]); j++ { + dfsBoard(board, i, j, trie, &result) + } + } + + return result +} +``` diff --git a/16-LeetCode Hot 100/子集.md b/16-LeetCode Hot 100/子集.md new file mode 100644 index 0000000..f5aad52 --- /dev/null +++ b/16-LeetCode Hot 100/子集.md @@ -0,0 +1,448 @@ +# 子集 (Subsets) + +## 题目描述 + +给你一个整数数组 `nums`,数组中的元素 **互不相同**。返回该数组所有可能的子集(幂集)。 + +解集 **不能** 包含重复的子集。你可以按 **任意顺序** 返回解集。 + +### 示例 + +**示例 1:** +``` +输入:nums = [1,2,3] +输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] +``` + +**示例 2:** +``` +输入:nums = [0] +输出:[[],[0]] +``` + +### 约束条件 + +- `1 <= nums.length <= 10` +- `-10 <= nums[i] <= 10` +- `nums` 中的所有元素 **互不相同** + +## 解题思路 + +### 方法一:回溯法(推荐) + +**核心思想:**对于每个元素,可以选择包含或不包含。使用回溯法生成所有可能的组合。 + +**算法步骤:** +1. 初始化结果数组和当前子集 +2. 定义回溯函数 `backtrack(start)`: + - 将当前子集加入结果 + - 从 `start` 开始遍历,依次尝试包含每个元素 + - 递归调用后撤销选择(回溯) + +### 方法二:迭代法(位掩码) + +**核心思想:**子集可以用二进制表示。对于 n 个元素,共有 2^n 个子集。 + +**算法步骤:** +1. 计算子集总数 `total = 1 << n` +2. 对于每个数字 `i` 从 0 到 `total-1`: + - 将 `i` 的二进制表示转换为子集 + - 第 `j` 位为 1 表示包含 `nums[j]` + +### 方法三:级联法 + +**核心思想:**对于已有的每个子集,通过添加当前元素生成新的子集。 + +**算法步骤:** +1. 初始化结果为 `[[]]` +2. 对于每个元素: + - 取出所有已有子集 + - 将当前元素添加到每个子集 + - 将新子集加入结果 + +## 代码实现 + +### Go 实现(回溯法) + +```go +package main + +import "fmt" + +func subsets(nums []int) [][]int { + result := [][]int{} + current := []int{} + + var backtrack func(start int) + backtrack = func(start int) { + // 将当前子集加入结果(需要复制) + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + + // 从 start 开始尝试包含每个元素 + for i := start; i < len(nums); i++ { + // 选择当前元素 + current = append(current, nums[i]) + // 递归处理下一个元素 + backtrack(i + 1) + // 撤销选择(回溯) + current = current[:len(current)-1] + } + } + + backtrack(0) + return result +} + +// 测试用例 +func main() { + // 测试用例1 + nums1 := []int{1, 2, 3} + fmt.Printf("输入: %v\n", nums1) + fmt.Printf("输出: %v\n", subsets(nums1)) + + // 测试用例2 + nums2 := []int{0} + fmt.Printf("\n输入: %v\n", nums2) + fmt.Printf("输出: %v\n", subsets(nums2)) + + // 测试用例3 + nums3 := []int{1, 2} + fmt.Printf("\n输入: %v\n", nums3) + fmt.Printf("输出: %v\n", subsets(nums3)) +} +``` + +### Java 实现(回溯法) + +```java +import java.util.ArrayList; +import java.util.List; + +public class Subsets { + + public List> subsets(int[] nums) { + List> result = new ArrayList<>(); + List current = new ArrayList<>(); + backtrack(result, current, nums, 0); + return result; + } + + private void backtrack(List> result, List current, + int[] nums, int start) { + // 将当前子集加入结果 + result.add(new ArrayList<>(current)); + + // 从 start 开始尝试包含每个元素 + for (int i = start; i < nums.length; i++) { + // 选择当前元素 + current.add(nums[i]); + // 递归处理下一个元素 + backtrack(result, current, nums, i + 1); + // 撤销选择(回溯) + current.remove(current.size() - 1); + } + } + + // 测试用例 + public static void main(String[] args) { + Subsets solution = new Subsets(); + + // 测试用例1 + int[] nums1 = {1, 2, 3}; + System.out.println("输入: [1, 2, 3]"); + System.out.println("输出: " + solution.subsets(nums1)); + + // 测试用例2 + int[] nums2 = {0}; + System.out.println("\n输入: [0]"); + System.out.println("输出: " + solution.subsets(nums2)); + + // 测试用例3 + int[] nums3 = {1, 2}; + System.out.println("\n输入: [1, 2]"); + System.out.println("输出: " + solution.subsets(nums3)); + } +} +``` + +### Go 实现(迭代法-位掩码) + +```go +func subsetsBitMask(nums []int) [][]int { + n := len(nums) + total := 1 << n // 2^n 个子集 + result := make([][]int, 0, total) + + for mask := 0; mask < total; mask++ { + subset := []int{} + for i := 0; i < n; i++ { + // 检查第 i 位是否为 1 + if mask&(1<> subsetsBitMask(int[] nums) { + int n = nums.length; + int total = 1 << n; // 2^n 个子集 + List> result = new ArrayList<>(); + + for (int mask = 0; mask < total; mask++) { + List subset = new ArrayList<>(); + for (int i = 0; i < n; i++) { + // 检查第 i 位是否为 1 + if ((mask & (1 << i)) != 0) { + subset.add(nums[i]); + } + } + result.add(subset); + } + + return result; +} +``` + +### Go 实现(级联法) + +```go +func subsetsCascade(nums []int) [][]int { + result := [][]int{{}} // 初始化为空集 + + for _, num := range nums { + // 对于每个已有子集,添加当前元素生成新子集 + newSubsets := make([][]int, 0, len(result)) + for _, subset := range result { + newSubset := make([]int, len(subset)+1) + copy(newSubset, subset) + newSubset[len(subset)] = num + newSubsets = append(newSubsets, newSubset) + } + result = append(result, newSubsets...) + } + + return result +} +``` + +## 复杂度分析 + +### 回溯法 + +- **时间复杂度:** O(n × 2^n) + - 共有 2^n 个子集 + - 每个子集的复制需要 O(n) 时间 + +- **空间复杂度:** O(n) + - 递归栈深度最大为 n + - 不包括存储结果的空间 + +### 迭代法(位掩码) + +- **时间复杂度:** O(n × 2^n) + - 需要生成 2^n 个子集 + - 每个子集需要 O(n) 时间构建 + +- **空间复杂度:** O(1) + - 只使用了常数级别的额外空间(不包括结果) + +### 级联法 + +- **时间复杂度:** O(n × 2^n) + - 每次迭代都会将子集数量翻倍 + - 总共需要处理 n 次 + +- **空间复杂度:** O(n × 2^n) + - 需要存储所有子集 + +## 进阶问题 + +### Q1: 如果数组中有重复元素,应该如何处理? + +**A:** 需要先排序,然后在回溯时跳过重复元素。 + +```go +func subsetsWithDup(nums []int) [][]int { + sort.Ints(nums) + result := [][]int{} + current := []int{} + + var backtrack func(start int) + backtrack = func(start int) { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + + for i := start; i < len(nums); i++ { + // 跳过重复元素 + if i > start && nums[i] == nums[i-1] { + continue + } + current = append(current, nums[i]) + backtrack(i + 1) + current = current[:len(current)-1] + } + } + + backtrack(0) + return result +} +``` + +### Q2: 如果要求子集的大小恰好为 k,应该如何修改? + +**A:** 在回溯时添加终止条件。 + +```go +func subsetsK(nums []int, k int) [][]int { + result := [][]int{} + current := []int{} + + var backtrack func(start int) + backtrack = func(start int) { + if len(current) == k { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + return + } + + for i := start; i < len(nums); i++ { + current = append(current, nums[i]) + backtrack(i + 1) + current = current[:len(current)-1] + } + } + + backtrack(0) + return result +} +``` + +## P7 加分项 + +### 1. 深度理解:为什么子集问题适合用回溯法? + +**回溯法的本质:** +- 在解空间树中进行深度优先搜索 +- 每个节点代表一个决策(包含或不包含当前元素) +- 通过撤销选择(回溯)来探索所有可能 + +**为什么适合子集问题:** +1. **决策清晰:**每个元素只有两种选择(包含或不包含) +2. **无后效性:**当前选择不影响之前的选择 +3. **边界明确:**子集大小从 0 到 n + +### 2. 实战扩展:组合与排列 + +**组合问题:**从 n 个元素中选 k 个,不考虑顺序 +**排列问题:**从 n 个元素中选 k 个,考虑顺序 + +```go +// 组合 +func combine(n int, k int) [][]int { + result := [][]int{} + current := []int{} + + var backtrack func(start int) + backtrack = func(start int) { + if len(current) == k { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + return + } + + for i := start; i <= n; i++ { + current = append(current, i) + backtrack(i + 1) + current = current[:len(current)-1] + } + } + + backtrack(1) + return result +} + +// 排列 +func permute(nums []int) [][]int { + result := [][]int{} + current := []int{} + used := make([]bool, len(nums)) + + var backtrack func() + backtrack = func() { + if len(current) == len(nums) { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + return + } + + for i := 0; i < len(nums); i++ { + if used[i] { + continue + } + current = append(current, nums[i]) + used[i] = true + backtrack() + current = current[:len(current)-1] + used[i] = false + } + } + + backtrack() + return result +} +``` + +### 3. 变形题目 + +#### 变形1:子集 II(有重复元素) + +**LeetCode 90:** 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 + +```go +func subsetsWithDup(nums []int) [][]int { + sort.Ints(nums) + result := [][]int{} + current := []int{} + + var backtrack func(start int) + backtrack = func(start int) { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + + for i := start; i < len(nums); i++ { + if i > start && nums[i] == nums[i-1] { + continue + } + current = append(current, nums[i]) + backtrack(i + 1) + current = current[:len(current)-1] + } + } + + backtrack(0) + return result +} +``` + +### 4. 相关题目推荐 + +- LeetCode 78: 子集(本题) +- LeetCode 90: 子集 II +- LeetCode 77: 组合 +- LeetCode 46: 全排列 +- LeetCode 47: 全排列 II diff --git a/16-LeetCode Hot 100/完全平方数.md b/16-LeetCode Hot 100/完全平方数.md new file mode 100644 index 0000000..fcd6cb9 --- /dev/null +++ b/16-LeetCode Hot 100/完全平方数.md @@ -0,0 +1,35 @@ +# 完全平方数 (Perfect Squares) + +## 题目描述 + +给你一个整数 n,返回和为 n 的完全平方数的最少数量。 + +## 解题思路 + +### 动态规划 + +dp[i] = min(dp[i - j*j] + 1) for all j where j*j <= i + +## Go 代码 + +```go +func numSquares(n int) int { + dp := make([]int, n+1) + for i := range dp { + dp[i] = math.MaxInt32 + } + dp[0] = 0 + + for i := 1; i <= n; i++ { + for j := 1; j*j <= i; j++ { + if dp[i-j*j]+1 < dp[i] { + dp[i] = dp[i-j*j] + 1 + } + } + } + + return dp[n] +} +``` + +**复杂度:** O(n√n) 时间,O(n) 空间 diff --git a/16-LeetCode Hot 100/对称二叉树.md b/16-LeetCode Hot 100/对称二叉树.md new file mode 100644 index 0000000..5429055 --- /dev/null +++ b/16-LeetCode Hot 100/对称二叉树.md @@ -0,0 +1,32 @@ +# 对称二叉树 (Symmetric Tree) + +## 题目描述 + +给你一个二叉树的根节点 root,检查它是否轴对称。 + +## 解题思路 + +### 递归比较 + +## Go 代码 + +```go +func isSymmetric(root *TreeNode) bool { + return check(root.Left, root.Right) +} + +func check(left, right *TreeNode) bool { + if left == nil && right == nil { + return true + } + if left == nil || right == nil { + return false + } + + return left.Val == right.Val && + check(left.Left, right.Right) && + check(left.Right, right.Left) +} +``` + +**复杂度:** O(n) 时间,O(h) 空间 diff --git a/16-LeetCode Hot 100/括号生成.md b/16-LeetCode Hot 100/括号生成.md new file mode 100644 index 0000000..906bcb5 --- /dev/null +++ b/16-LeetCode Hot 100/括号生成.md @@ -0,0 +1,693 @@ +# 括号生成 (Generate Parentheses) + +## 题目描述 + +数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。 + +### 示例 + +**示例 1:** +``` +输入:n = 3 +输出:["((()))","(()())","(())()","()(())","()()()"] +``` + +**示例 2:** +``` +输入:n = 1 +输出:["()"] +``` + +### 约束条件 + +- `1 <= n <= 8` + +## 解题思路 + +### 方法一:回溯法(推荐) + +**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性: +1. 左括号数量不能超过 n +2. 右括号数量不能超过左括号数量 + +**算法步骤:** +1. 初始化结果数组 `result` 和当前字符串 `current` +2. 定义回溯函数 `backtrack(open, close)`: + - `open`:已使用的左括号数量 + - `close`:已使用的右括号数量 +3. 终止条件:`len(current) == 2 * n`,将 `current` 加入 `result` +4. 选择条件: + - 如果 `open < n`,可以添加左括号 + - 如果 `close < open`,可以添加右括号 +5. 递归调用后撤销选择(回溯) + +**为什么这样做?** +- 通过限制 `close < open`,保证任何时候右括号数量不超过左括号数量 +- 通过限制 `open < n`,保证左括号数量不超过 n +- 这样生成的所有组合都是有效的 + +### 方法二:DFS 深度优先搜索 + +**核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。 + +**算法步骤:** +1. 构建一个递归树,每个节点代表一个状态 +2. 从根节点开始,每次可以选择添加左括号或右括号 +3. 剪枝:不符合条件的分支直接跳过 +4. 到达叶子节点(长度为 2n)时,记录结果 + +### 方法三:动态规划 + +**核心思想:**利用卡特兰数(Catalan Number)的性质。n 对括号的有效组合数等于第 n 个卡特兰数。 + +**递推公式:** +- `dp[n]` 表示 n 对括号的所有有效组合 +- `dp[n] = "(" + dp[i] + ")" + dp[n-1-i]`,其中 `i` 从 0 到 n-1 + +**算法步骤:** +1. 初始化 `dp[0] = [""]` +2. 对于 `i` 从 1 到 n: + - 对于 `j` 从 0 到 i-1: + - 将 `dp[j]` 的每个组合加上一对括号,再拼接 `dp[i-1-j]` 的每个组合 +3. 返回 `dp[n]` + +## 代码实现 + +### Go 实现(回溯法) + +```go +package main + +import "fmt" + +func generateParenthesis(n int) []string { + result := []string{} + current := []byte{} + + var backtrack func(open, close int) + backtrack = func(open, close int) { + // 终止条件:生成了 2n 个括号 + if len(current) == 2*n { + result = append(result, string(current)) + return + } + + // 添加左括号:左括号数量小于 n + if open < n { + current = append(current, '(') + backtrack(open+1, close) + current = current[:len(current)-1] // 回溯 + } + + // 添加右括号:右括号数量小于左括号数量 + if close < open { + current = append(current, ')') + backtrack(open, close+1) + current = current[:len(current)-1] // 回溯 + } + } + + backtrack(0, 0) + return result +} + +// 测试用例 +func main() { + // 测试用例1 + n1 := 3 + fmt.Printf("输入: n = %d\n", n1) + fmt.Printf("输出: %v\n", generateParenthesis(n1)) + + // 测试用例2 + n2 := 1 + fmt.Printf("\n输入: n = %d\n", n2) + fmt.Printf("输出: %v\n", generateParenthesis(n2)) + + // 测试用例3 + n3 := 4 + fmt.Printf("\n输入: n = %d\n", n3) + result3 := generateParenthesis(n3) + fmt.Printf("输出长度: %d\n", len(result3)) + fmt.Printf("输出: %v\n", result3) + + // 验证卡特兰数 + for i := 1; i <= 8; i++ { + fmt.Printf("n = %d, 组合数 = %d\n", i, len(generateParenthesis(i))) + } +} +``` + +### Java 实现(回溯法) + +```java +import java.util.ArrayList; +import java.util.List; + +public class GenerateParentheses { + + public List generateParenthesis(int n) { + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + backtrack(result, current, 0, 0, n); + return result; + } + + private void backtrack(List result, StringBuilder current, + int open, int close, int max) { + // 终止条件:生成了 2n 个括号 + if (current.length() == 2 * max) { + result.add(current.toString()); + return; + } + + // 添加左括号:左括号数量小于 n + if (open < max) { + current.append('('); + backtrack(result, current, open + 1, close, max); + current.deleteCharAt(current.length() - 1); // 回溯 + } + + // 添加右括号:右括号数量小于左括号数量 + if (close < open) { + current.append(')'); + backtrack(result, current, open, close + 1, max); + current.deleteCharAt(current.length() - 1); // 回溯 + } + } + + // 测试用例 + public static void main(String[] args) { + GenerateParentheses solution = new GenerateParentheses(); + + // 测试用例1 + int n1 = 3; + System.out.println("输入: n = " + n1); + System.out.println("输出: " + solution.generateParenthesis(n1)); + + // 测试用例2 + int n2 = 1; + System.out.println("\n输入: n = " + n2); + System.out.println("输出: " + solution.generateParenthesis(n2)); + + // 测试用例3 + int n3 = 4; + System.out.println("\n输入: n = " + n3); + List result3 = solution.generateParenthesis(n3); + System.out.println("输出长度: " + result3.size()); + System.out.println("输出: " + result3); + + // 验证卡特兰数 + System.out.println("\n卡特兰数验证:"); + for (int i = 1; i <= 8; i++) { + System.out.println("n = " + i + ", 组合数 = " + + solution.generateParenthesis(i).size()); + } + } +} +``` + +### Go 实现(动态规划) + +```go +func generateParenthesisDP(n int) []string { + if n == 0 { + return []string{""} + } + + dp := make([][]string, n+1) + dp[0] = []string{""} + + for i := 1; i <= n; i++ { + dp[i] = []string{} + for j := 0; j < i; j++ { + for _, left := range dp[j] { + for _, right := range dp[i-1-j] { + dp[i] = append(dp[i], "("+left+")"+right) + } + } + } + } + + return dp[n] +} +``` + +### Java 实现(动态规划) + +```java +public List generateParenthesisDP(int n) { + List> dp = new ArrayList<>(); + List dp0 = new ArrayList<>(); + dp0.add(""); + dp.add(dp0); + + for (int i = 1; i <= n; i++) { + List current = new ArrayList<>(); + for (int j = 0; j < i; j++) { + List leftList = dp.get(j); + List rightList = dp.get(i - 1 - j); + for (String left : leftList) { + for (String right : rightList) { + current.add("(" + left + ")" + right); + } + } + } + dp.add(current); + } + + return dp.get(n); +} +``` + +## 复杂度分析 + +### 回溯法 + +- **时间复杂度:** O(4^n / √n) + - 在回溯树中,每个节点最多有 2 个分支 + - 树的高度为 2n + - 但是由于剪枝,实际复杂度约为卡特兰数 C(n) + - 卡特兰数约为 O(4^n / (n^(3/2) * √π)) + +- **空间复杂度:** O(n) + - 递归栈深度最大为 2n + - 存储结果的空间不算在内(这是必须的) + +### 动态规划 + +- **时间复杂度:** O(4^n / √n) + - 与回溯法类似,需要生成所有有效组合 + +- **空间复杂度:** O(4^n / √n) + - 需要存储中间结果和最终结果 + +## 进阶问题 + +### Q1: 如何判断一个括号字符串是否有效? + +**A:** 使用栈或者计数器。 + +```go +// 方法1: 使用栈 +func isValid(s string) bool { + stack := []byte{} + for _, c := range []byte(s) { + if c == '(' { + stack = append(stack, c) + } else if len(stack) > 0 { + stack = stack[:len(stack)-1] + } else { + return false + } + } + return len(stack) == 0 +} + +// 方法2: 使用计数器 +func isValidSimple(s string) bool { + count := 0 + for _, c := range s { + if c == '(' { + count++ + } else if c == ')' { + count-- + } + if count < 0 { + return false + } + } + return count == 0 +} +``` + +### Q2: 如果有三种括号 ()、[]、{},应该如何生成? + +**A:** 需要更复杂的逻辑来保证括号匹配。 + +```go +func generateMultipleParentheses(n int) []string { + types := []byte{'(', ')', '[', ']', '{', '}'} + result := []string{} + current := []byte{} + stack := []byte{} + + var backtrack func(int) + backtrack = func(length int) { + if len(current) == 2*n { + result = append(result, string(current)) + return + } + + for i := 0; i < len(types); i += 2 { + // 添加左括号 + if length < n { + current = append(current, types[i]) + stack = append(stack, types[i]) + backtrack(length + 1) + current = current[:len(current)-1] + stack = stack[:len(stack)-1] + } + } + + for i := 1; i < len(types); i += 2 { + // 添加右括号:必须与栈顶匹配 + if len(stack) > 0 && stack[len(stack)-1] == types[i-1] { + current = append(current, types[i]) + stack = stack[:len(stack)-1] + backtrack(length) + current = current[:len(current)-1] + stack = append(stack, types[i-1]) + } + } + } + + backtrack(0) + return result +} +``` + +### Q3: 如何优化内存使用,特别是对于大的 n? + +**A:** 可以使用生成器模式,逐个生成结果而不是全部存储。 + +```go +func generateParenthesisGenerator(n int, callback func(string)) { + current := make([]byte, 0, 2*n) + + var backtrack func(open, close int) + backtrack = func(open, close int) { + if len(current) == 2*n { + callback(string(current)) + return + } + + if open < n { + current = append(current, '(') + backtrack(open+1, close) + current = current[:len(current)-1] + } + + if close < open { + current = append(current, ')') + backtrack(open, close+1) + current = current[:len(current)-1] + } + } + + backtrack(0, 0) +} +``` + +## P7 加分项 + +### 1. 深度理解:卡特兰数(Catalan Number) + +**定义:**卡特兰数是组合数学中经常出现的数列,在许多计数问题中出现。 + +**公式:** +- C(n) = (2n)! / ((n+1)! × n!) +- C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0) + +**前几项:**1, 1, 2, 5, 14, 42, 132, 429, 1430, ... + +**应用场景:** +1. 括号匹配问题(本题) +2. 二叉搜索树的计数 +3. 出栈序列的计数 +4. 路径计数(不穿过对角线) + +**计算卡特兰数:** + +```go +func catalanNumber(n int) int { + if n <= 1 { + return 1 + } + + // 动态规划计算 + dp := make([]int, n+1) + dp[0], dp[1] = 1, 1 + + for i := 2; i <= n; i++ { + for j := 0; j < i; j++ { + dp[i] += dp[j] * dp[i-1-j] + } + } + + return dp[n] +} +``` + +### 2. 实战扩展:通用回溯框架 + +**回溯法通用模板:** + +```go +func backtrack(路径, 选择列表) { + if 满足结束条件 { + result = append(result, 路径) + return + } + + for 选择 in 选择列表 { + // 做选择 + 路径.add(选择) + + // 递归 + backtrack(路径, 选择列表) + + // 撤销选择(回溯) + 路径.remove(选择) + } +} +``` + +**应用示例:排列问题** + +```go +func permute(nums []int) [][]int { + result := [][]int{} + current := []int{} + used := make([]bool, len(nums)) + + var backtrack func() + backtrack = func() { + if len(current) == len(nums) { + temp := make([]int, len(current)) + copy(temp, current) + result = append(result, temp) + return + } + + for i := 0; i < len(nums); i++ { + if used[i] { + continue + } + + // 做选择 + current = append(current, nums[i]) + used[i] = true + + // 递归 + backtrack() + + // 撤销选择 + current = current[:len(current)-1] + used[i] = false + } + } + + backtrack() + return result +} +``` + +### 3. 变形题目 + +#### 变形1:最长有效括号 + +**LeetCode 32:** 给定一个只包含 '(' 和 ')' 的字符串,找出最长有效(正确闭合)括号子串的长度。 + +```go +func longestValidParentheses(s string) int { + maxLen := 0 + stack := []int{-1} // 初始化为 -1,便于计算长度 + + for i, c := range s { + if c == '(' { + stack = append(stack, i) + } else { + stack = stack[:len(stack)-1] + if len(stack) == 0 { + stack = append(stack, i) + } else { + length := i - stack[len(stack)-1] + if length > maxLen { + maxLen = length + } + } + } + } + + return maxLen +} +``` + +#### 变形2:不同的二叉搜索树 + +**LeetCode 96:** 给定 n,求恰好由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种? + +```go +func numTrees(n int) int { + dp := make([]int, n+1) + dp[0], dp[1] = 1, 1 + + for i := 2; i <= n; i++ { + for j := 1; j <= i; j++ { + dp[i] += dp[j-1] * dp[i-j] + } + } + + return dp[n] +} +``` + +#### 变形3:括号分数 + +**LeetCode 856:** 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数: +- `()` 得 1 分 +- `AB` 得 `A + B` 分,其中 A 和 B 是平衡括号字符串 +- `(A)` 得 `2 × A` 分,其中 A 是平衡括号字符串 + +```go +func scoreOfParentheses(s string) int { + stack := []int{0} // 栈底保存当前层的分数 + + for _, c := range s { + if c == '(' { + stack = append(stack, 0) // 新的一层,初始分数为 0 + } else { + // 弹出当前层的分数 + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + // 计算分数 + if top == 0 { + stack[len(stack)-1] += 1 + } else { + stack[len(stack)-1] += 2 * top + } + } + } + + return stack[0] +} +``` + +### 4. 优化技巧 + +#### 优化1:剪枝优化 + +在回溯过程中,尽早发现不可能的解并剪枝。 + +```go +func generateParenthesisOptimized(n int) []string { + result := []string{} + current := []byte{} + + var backtrack func(open, close int) + backtrack = func(open, close int) { + // 剪枝:如果剩余的右括号太多,无法完成 + if close > open { + return + } + + if len(current) == 2*n { + result = append(result, string(current)) + return + } + + if open < n { + current = append(current, '(') + backtrack(open+1, close) + current = current[:len(current)-1] + } + + if close < open { + current = append(current, ')') + backtrack(open, close+1) + current = current[:len(current)-1] + } + } + + backtrack(0, 0) + return result +} +``` + +#### 优化2:迭代优化 + +使用迭代代替递归,避免栈溢出。 + +```go +func generateParenthesisIterative(n int) []string { + type state struct { + current string + open int + close int + } + + result := []string{} + stack := []state{{"", 0, 0}} + + for len(stack) > 0 { + // 弹出栈顶 + s := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if len(s.current) == 2*n { + result = append(result, s.current) + continue + } + + if s.open < n { + stack = append(stack, state{s.current + "(", s.open + 1, s.close}) + } + + if s.close < s.open { + stack = append(stack, state{s.current + ")", s.open, s.close + 1}) + } + } + + return result +} +``` + +### 5. 实际应用场景 + +- **编译器:** 语法分析和表达式求值 +- **代码格式化:** 自动添加括号 +- **数学表达式:** 验证表达式有效性 +- **数据验证:** 检查嵌套结构(如 HTML 标签) + +### 6. 面试技巧 + +**面试官可能会问:** +1. "为什么要用回溯法而不是暴力枚举?" +2. "卡特兰数和这个问题有什么关系?" +3. "如何证明你的算法生成的所有组合都是有效的?" + +**回答要点:** +1. 回溯法通过剪枝避免了无效组合的生成,效率更高 +2. n 对括号的有效组合数等于第 n 个卡特兰数 +3. 通过维护 `open` 和 `close` 计数器,保证了右括号永远不超过左括号 + +### 7. 相关题目推荐 + +- LeetCode 22: 括号生成(本题) +- LeetCode 17: 电话号码的字母组合 +- LeetCode 32: 最长有效括号 +- LeetCode 39: 组合总和 +- LeetCode 46: 全排列 +- LeetCode 78: 子集 +- LeetCode 96: 不同的二叉搜索树 diff --git a/16-LeetCode Hot 100/最大正方形.md b/16-LeetCode Hot 100/最大正方形.md new file mode 100644 index 0000000..962738c --- /dev/null +++ b/16-LeetCode Hot 100/最大正方形.md @@ -0,0 +1,59 @@ +# 最大正方形 (Maximal Square) + +## 题目描述 + +在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。 + +## 解题思路 + +### 动态规划 + +**状态定义:** dp[i][j] 表示以 (i, j) 为右下角的最大正方形边长。 + +**状态转移:** dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + +## Go 代码 + +```go +func maximalSquare(matrix [][]byte) int { + if len(matrix) == 0 { + return 0 + } + + m, n := len(matrix), len(matrix[0]) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + maxSide := 0 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + if dp[i][j] > maxSide { + maxSide = dp[i][j] + } + } + } + } + + return maxSide * maxSide +} + +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} +``` + +**复杂度:** O(mn) 时间,O(mn) 空间 diff --git a/16-LeetCode Hot 100/最小栈.md b/16-LeetCode Hot 100/最小栈.md new file mode 100644 index 0000000..12fc942 --- /dev/null +++ b/16-LeetCode Hot 100/最小栈.md @@ -0,0 +1,56 @@ +# 最小栈 (Min Stack) + +## 题目描述 + +设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。 + +## 解题思路 + +### 辅助栈 + +使用两个栈,一个存储所有元素,另一个存储当前最小值。 + +## Go 代码 + +```go +type MinStack struct { + stack []int + minStack []int +} + +func Constructor() MinStack { + return MinStack{ + stack: []int{}, + minStack: []int{}, + } +} + +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + if len(this.minStack) == 0 { + this.minStack = append(this.minStack, val) + } else { + min := this.minStack[len(this.minStack)-1] + if val < min { + this.minStack = append(this.minStack, val) + } else { + this.minStack = append(this.minStack, min) + } + } +} + +func (this *MinStack) Pop() { + this.stack = this.stack[:len(this.stack)-1] + this.minStack = this.minStack[:len(this.minStack)-1] +} + +func (this *MinStack) Top() int { + return this.stack[len(this.stack)-1] +} + +func (this *MinStack) GetMin() int { + return this.minStack[len(this.minStack)-1] +} +``` + +**复杂度:** 所有操作 O(1) 时间 diff --git a/16-LeetCode Hot 100/最长回文子串.md b/16-LeetCode Hot 100/最长回文子串.md new file mode 100644 index 0000000..023fc1c --- /dev/null +++ b/16-LeetCode Hot 100/最长回文子串.md @@ -0,0 +1,644 @@ +# 最长回文子串 (Longest Palindromic Substring) + +## 题目描述 + +给你一个字符串 `s`,找到 `s` 中最长的回文子串。 + +### 示例 + +**示例 1:** +``` +输入:s = "babad" +输出:"bab" +解释:"aba" 同样是符合题意的答案。 +``` + +**示例 2:** +``` +输入:s = "cbbd" +输出:"bb" +``` + +### 约束条件 + +- `1 <= s.length <= 1000` +- `s` 仅由数字和英文字母组成 + +## 解题思路 + +### 方法一:动态规划(推荐) + +**核心思想:**使用二维数组 `dp[i][j]` 表示 `s[i:j+1]` 是否为回文串。 + +**状态转移方程:** +- `dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])` +- 如果 `s[i] == s[j]` 且 `dp[i+1][j-1]` 为真(或子串长度小于3),则 `dp[i][j]` 为真 + +**算法步骤:** +1. 初始化 `dp` 数组,所有单个字符都是回文串 +2. 按长度递增的顺序遍历所有子串 +3. 更新最长回文子串的起始位置和长度 + +### 方法二:中心扩展法(最优) + +**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。 + +**算法步骤:** +1. 遍历每个字符作为中心点 +2. 从中心点向两边扩展,直到不再是回文串 +3. 记录最长的回文串 +4. 需要考虑奇数长度和偶数长度两种情况 + +### 方法三:Manacher 算法(最优) + +**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。 + +**算法步骤:** +1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度 +2. 使用数组 `P` 记录以每个字符为中心的最长回文半径 +3. 利用对称性快速计算回文半径 + +## 代码实现 + +### Go 实现(中心扩展法) + +```go +package main + +import "fmt" + +func longestPalindrome(s string) string { + if len(s) < 2 { + return s + } + + start, maxLen := 0, 1 + + for i := 0; i < len(s); i++ { + // 奇数长度:以当前字符为中心 + len1 := expandAroundCenter(s, i, i) + // 偶数长度:以当前字符和下一个字符之间为中心 + len2 := expandAroundCenter(s, i, i+1) + + currentLen := max(len1, len2) + if currentLen > maxLen { + maxLen = currentLen + start = i - (currentLen-1)/2 + } + } + + return s[start : start+maxLen] +} + +func expandAroundCenter(s string, left, right int) int { + for left >= 0 && right < len(s) && s[left] == s[right] { + left-- + right++ + } + return right - left - 1 +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// 测试用例 +func main() { + // 测试用例1 + s1 := "babad" + fmt.Printf("输入: %s\n", s1) + fmt.Printf("输出: %s\n", longestPalindrome(s1)) + + // 测试用例2 + s2 := "cbbd" + fmt.Printf("\n输入: %s\n", s2) + fmt.Printf("输出: %s\n", longestPalindrome(s2)) + + // 测试用例3: 单个字符 + s3 := "a" + fmt.Printf("\n输入: %s\n", s3) + fmt.Printf("输出: %s\n", longestPalindrome(s3)) + + // 测试用例4: 全部相同 + s4 := "aaaa" + fmt.Printf("\n输入: %s\n", s4) + fmt.Printf("输出: %s\n", longestPalindrome(s4)) + + // 测试用例5: 无回文 + s5 := "abc" + fmt.Printf("\n输入: %s\n", s5) + fmt.Printf("输出: %s\n", longestPalindrome(s5)) +} +``` + +### Java 实现(中心扩展法) + +```java +public class LongestPalindromicSubstring { + + public String longestPalindrome(String s) { + if (s == null || s.length() < 2) { + return s; + } + + int start = 0, maxLen = 1; + + for (int i = 0; i < s.length(); i++) { + // 奇数长度:以当前字符为中心 + int len1 = expandAroundCenter(s, i, i); + // 偶数长度:以当前字符和下一个字符之间为中心 + int len2 = expandAroundCenter(s, i, i + 1); + + int currentLen = Math.max(len1, len2); + if (currentLen > maxLen) { + maxLen = currentLen; + start = i - (currentLen - 1) / 2; + } + } + + return s.substring(start, start + maxLen); + } + + private 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; + } + + // 测试用例 + public static void main(String[] args) { + LongestPalindromicSubstring solution = new LongestPalindromicSubstring(); + + // 测试用例1 + String s1 = "babad"; + System.out.println("输入: " + s1); + System.out.println("输出: " + solution.longestPalindrome(s1)); + + // 测试用例2 + String s2 = "cbbd"; + System.out.println("\n输入: " + s2); + System.out.println("输出: " + solution.longestPalindrome(s2)); + + // 测试用例3: 单个字符 + String s3 = "a"; + System.out.println("\n输入: " + s3); + System.out.println("输出: " + solution.longestPalindrome(s3)); + + // 测试用例4: 全部相同 + String s4 = "aaaa"; + System.out.println("\n输入: " + s4); + System.out.println("输出: " + solution.longestPalindrome(s4)); + + // 测试用例5: 无回文 + String s5 = "abc"; + System.out.println("\n输入: " + s5); + System.out.println("输出: " + solution.longestPalindrome(s5)); + } +} +``` + +### Go 实现(动态规划) + +```go +func longestPalindromeDP(s string) string { + if len(s) < 2 { + return s + } + + n := len(s) + dp := make([][]bool, n) + for i := range dp { + dp[i] = make([]bool, n) + } + + start, maxLen := 0, 1 + + // 初始化:所有单个字符都是回文串 + for i := 0; i < n; i++ { + dp[i][i] = true + } + + // 按长度递增的顺序遍历 + for length := 2; length <= n; length++ { + for i := 0; i <= n-length; i++ { + j := i + length - 1 + + if s[i] == s[j] { + if length == 2 || dp[i+1][j-1] { + dp[i][j] = true + if length > maxLen { + maxLen = length + start = i + } + } + } + } + } + + return s[start : start+maxLen] +} +``` + +### Java 实现(动态规划) + +```java +public String longestPalindromeDP(String s) { + if (s == null || s.length() < 2) { + return s; + } + + int n = s.length(); + boolean[][] dp = new boolean[n][n]; + int start = 0, maxLen = 1; + + // 初始化:所有单个字符都是回文串 + for (int i = 0; i < n; i++) { + dp[i][i] = true; + } + + // 按长度递增的顺序遍历 + for (int length = 2; length <= n; length++) { + for (int i = 0; i <= n - length; i++) { + int j = i + length - 1; + + if (s.charAt(i) == s.charAt(j)) { + if (length == 2 || dp[i + 1][j - 1]) { + dp[i][j] = true; + if (length > maxLen) { + maxLen = length; + start = i; + } + } + } + } + } + + return s.substring(start, start + maxLen); +} +``` + +### Go 实现(Manacher 算法) + +```go +func longestPalindromeManacher(s string) string { + if len(s) < 2 { + return s + } + + // 预处理:插入特殊字符 + t := "#" + for i := 0; i < len(s); i++ { + t += string(s[i]) + "#" + } + + n := len(t) + p := make([]int, n) + center, right := 0, 0 + maxCenter, maxLen := 0, 0 + + for i := 0; i < n; i++ { + if i < right { + mirror := 2*center - i + p[i] = min(right-i, p[mirror]) + } + + // 尝试扩展 + for i+p[i]+1 < n && i-p[i]-1 >= 0 && t[i+p[i]+1] == t[i-p[i]-1] { + p[i]++ + } + + // 更新中心和右边界 + if i+p[i] > right { + center = i + right = i + p[i] + } + + // 更新最大回文串 + if p[i] > maxLen { + maxLen = p[i] + maxCenter = i + } + } + + // 计算原字符串中的起始位置 + start := (maxCenter - maxLen) / 2 + return s[start : start+maxLen] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +## 复杂度分析 + +### 中心扩展法 + +- **时间复杂度:** O(n²) + - 外层循环遍历 n 个字符 + - 内层扩展最多 O(n) 次 + - 总时间复杂度:O(n²) + +- **空间复杂度:** O(1) + - 只使用了常数级别的额外空间 + +### 动态规划 + +- **时间复杂度:** O(n²) + - 需要填充 n×n 的 dp 数组 + - 但由于剪枝,实际复杂度约为 O(n²/2) + +- **空间复杂度:** O(n²) + - 需要存储 n×n 的 dp 数组 + +### Manacher 算法 + +- **时间复杂度:** O(n) + - 只需遍历字符串一次 + - 利用对称性避免重复计算 + +- **空间复杂度:** O(n) + - 需要存储处理后的字符串和半径数组 + +## 进阶问题 + +### Q1: 如何找到所有回文子串? + +**A:** 修改中心扩展法,找到每个回文子串都记录下来。 + +```go +func findAllPalindromes(s string) []string { + result := []string{} + + for i := 0; i < len(s); i++ { + // 奇数长度 + l, r := i, i + for l >= 0 && r < len(s) && s[l] == s[r] { + result = append(result, s[l:r+1]) + l-- + r++ + } + + // 偶数长度 + l, r = i, i+1 + for l >= 0 && r < len(s) && s[l] == s[r] { + result = append(result, s[l:r+1]) + l-- + r++ + } + } + + return result +} +``` + +### Q2: 如何判断一个字符串是否可以通过重新排列成为回文串? + +**A:** 统计每个字符出现的次数,最多只能有一个字符出现奇数次。 + +```go +func canPermutePalindrome(s string) bool { + count := make(map[rune]int) + for _, c := range s { + count[c]++ + } + + oddCount := 0 + for _, c := range count { + if c%2 == 1 { + oddCount++ + } + } + + return oddCount <= 1 +} +``` + +### Q3: 最长回文子序列(非连续)如何求解? + +**A:** 使用动态规划,`dp[i][j]` 表示 `s[i:j+1]` 的最长回文子序列长度。 + +```go +func longestPalindromeSubseq(s string) int { + n := len(s) + dp := make([][]int, n) + for i := range dp { + dp[i] = make([]int, n) + dp[i][i] = 1 + } + + for length := 2; length <= n; length++ { + for i := 0; i <= n-length; i++ { + j := i + length - 1 + if s[i] == s[j] { + dp[i][j] = dp[i+1][j-1] + 2 + } else { + dp[i][j] = max(dp[i+1][j], dp[i][j-1]) + } + } + } + + return dp[0][n-1] +} +``` + +## P7 加分项 + +### 1. 深度理解:为什么中心扩展法最优? + +**对比分析:** +- **暴力法:** O(n³) - 枚举所有子串 O(n²),判断是否回文 O(n) +- **动态规划:** O(n²) 时间,O(n²) 空间 +- **中心扩展法:** O(n²) 时间,O(1) 空间 +- **Manacher 算法:** O(n) 时间,O(n) 空间 + +**选择建议:** +- **面试:** 中心扩展法 - 代码简洁,易于实现 +- **实际应用:** Manacher 算法 - 性能最优 +- **学习:** 都要掌握,理解不同思路 + +### 2. 实战扩展:回文串相关算法 + +#### 短回文串构造 + +**LeetCode 214:** 给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。 + +```go +func shortestPalindrome(s string) string { + if len(s) < 2 { + return s + } + + // 找到最长的回文前缀 + n := len(s) + rev := reverse(s) + combined := s + "#" + rev + + // KMP 算法计算最长公共前后缀 + pi := make([]int, len(combined)) + for i := 1; i < len(combined); i++ { + j := pi[i-1] + for j > 0 && combined[i] != combined[j] { + j = pi[j-1] + } + if combined[i] == combined[j] { + j++ + } + pi[i] = j + } + + // 添加剩余字符的逆序 + add := rev[:n-pi[len(combined)-1]] + return add + s +} + +func reverse(s string) string { + runes := []rune(s) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} +``` + +### 3. 变形题目 + +#### 变形1:回文子串个数 + +**LeetCode 647:** 给定一个字符串,计算这个字符串中有多少个回文子串。 + +```go +func countSubstrings(s string) int { + count := 0 + + for i := 0; i < len(s); i++ { + // 奇数长度 + count += expandAroundCenterCount(s, i, i) + // 偶数长度 + count += expandAroundCenterCount(s, i, i+1) + } + + return count +} + +func expandAroundCenterCount(s string, left, right int) int { + count := 0 + for left >= 0 && right < len(s) && s[left] == s[right] { + count++ + left-- + right++ + } + return count +} +``` + +#### 变形2:分割回文串 + +**LeetCode 131:** 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。 + +```go +func partition(s string) [][]string { + result := [][]string{} + current := []string{} + + var backtrack func(start int) + backtrack = func(start int) { + if start == len(s) { + temp := make([]string, len(current)) + copy(temp, current) + result = append(result, temp) + return + } + + for end := start + 1; end <= len(s); end++ { + if isPalindrome(s[start:end]) { + current = append(current, s[start:end]) + backtrack(end) + current = current[:len(current)-1] + } + } + } + + backtrack(0) + return result +} + +func isPalindrome(s string) bool { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + if s[i] != s[j] { + return false + } + } + return true +} +``` + +### 4. 优化技巧 + +#### 优化1:提前终止 + +如果找到的回文串已经接近最大可能长度,可以提前终止。 + +```go +func longestPalindromeOptimized(s string) string { + if len(s) < 2 { + return s + } + + start, maxLen := 0, 1 + + for i := 0; i < len(s); i++ { + // 提前终止:不可能找到更长的回文串了 + if maxLen >= 2*(len(s)-i) { + break + } + + len1 := expandAroundCenter(s, i, i) + len2 := expandAroundCenter(s, i, i+1) + currentLen := max(len1, len2) + + if currentLen > maxLen { + maxLen = currentLen + start = i - (currentLen-1)/2 + } + } + + return s[start : start+maxLen] +} +``` + +### 5. 实际应用场景 + +- **DNA 序列分析:** 寻找重复序列 +- **文本编辑:** 检查拼写和语法 +- **数据压缩:** 利用重复模式 +- **模式匹配:** 查找对称模式 + +### 6. 面试技巧 + +**面试官可能会问:** +1. "为什么中心扩展法比动态规划更好?" +2. "Manacher 算法的核心思想是什么?" +3. "如何处理 unicode 字符?" + +**回答要点:** +1. 中心扩展法空间复杂度 O(1),动态规划需要 O(n²) +2. Manacher 算法利用回文串的对称性,避免重复计算 +3. 使用 rune 类型处理 unicode 字符 + +### 7. 相关题目推荐 + +- LeetCode 5: 最长回文子串(本题) +- LeetCode 125: 验证回文串 +- LeetCode 131: 分割回文串 +- LeetCode 214: 最短回文串 +- LeetCode 516: 最长回文子序列 +- LeetCode 647: 回文子串个数 diff --git a/16-LeetCode Hot 100/最长连续序列.md b/16-LeetCode Hot 100/最长连续序列.md new file mode 100644 index 0000000..a1a5c1c --- /dev/null +++ b/16-LeetCode Hot 100/最长连续序列.md @@ -0,0 +1,44 @@ +# 最长连续序列 (Longest Consecutive Sequence) + +## 题目描述 + +给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。 + +## 解题思路 + +### 哈希表 + +将数字存入哈希表,对于每个数字,如果它是序列的起点(num-1 不在集合中),则向后查找。 + +## Go 代码 + +```go +func longestConsecutive(nums []int) int { + numSet := make(map[int]bool) + for _, num := range nums { + numSet[num] = true + } + + longest := 0 + + for num := range numSet { + if !numSet[num-1] { // 是序列起点 + currentNum := num + current := 1 + + for numSet[currentNum+1] { + currentNum++ + current++ + } + + if current > longest { + longest = current + } + } + } + + return longest +} +``` + +**复杂度:** O(n) 时间,O(n) 空间 diff --git a/16-LeetCode Hot 100/柱状图中最大的矩形.md b/16-LeetCode Hot 100/柱状图中最大的矩形.md new file mode 100644 index 0000000..d336edd --- /dev/null +++ b/16-LeetCode Hot 100/柱状图中最大的矩形.md @@ -0,0 +1,89 @@ +# 柱状图中最大的矩形 (Largest Rectangle in Histogram) + +## 题目描述 + +给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。 + +求在该柱状图中,能够勾勒出来的矩形的最大面积。 + +## 解题思路 + +### 方法一:单调栈(推荐) + +**核心思想:**使用单调递增栈,存储柱子的索引。当遇到比栈顶小的柱子时,弹出栈顶并计算面积。 + +## 代码实现 + +### Go 实现 + +```go +package main + +func largestRectangleArea(heights []int) int { + stack := []int{} + maxArea := 0 + n := len(heights) + + for i := 0; i <= n; i++ { + h := 0 + if i < n { + h = heights[i] + } + + for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + height := heights[stack[len(stack)-1]] + stack = stack[:len(stack)-1] + + width := i + if len(stack) > 0 { + width = i - stack[len(stack)-1] - 1 + } + + area := height * width + if area > maxArea { + maxArea = area + } + } + + stack = append(stack, i) + } + + return maxArea +} +``` + +### Java 实现 + +```java +public int largestRectangleArea(int[] heights) { + Stack stack = new Stack<>(); + int maxArea = 0; + int n = heights.length; + + for (int i = 0; i <= n; i++) { + int h = (i == n) ? 0 : heights[i]; + + while (!stack.isEmpty() && h < heights[stack.peek()]) { + int height = heights[stack.pop()]; + int width = stack.isEmpty() ? i : i - stack.peek() - 1; + maxArea = Math.max(maxArea, height * width); + } + + stack.push(i); + } + + return maxArea; +} +``` + +## 复杂度分析 + +- **时间复杂度:** O(n) +- **空间复杂度:** O(n) + +## P7 加分项 + +### 相关题目 + +- LeetCode 85: 最大矩形(二维版本) +- LeetCode 42: 接雨水 diff --git a/16-LeetCode Hot 100/翻转二叉树.md b/16-LeetCode Hot 100/翻转二叉树.md new file mode 100644 index 0000000..0526efc --- /dev/null +++ b/16-LeetCode Hot 100/翻转二叉树.md @@ -0,0 +1,24 @@ +# 翻转二叉树 (Invert Binary Tree) + +## 题目描述 + +给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。 + +## 解题思路 + +### 递归 / 迭代 + +## Go 代码(递归) + +```go +func invertTree(root *TreeNode) *TreeNode { + if root == nil { + return nil + } + + root.Left, root.Right = invertTree(root.Right), invertTree(root.Left) + return root +} +``` + +**复杂度:** O(n) 时间,O(h) 空间 diff --git a/16-LeetCode Hot 100/路径总和.md b/16-LeetCode Hot 100/路径总和.md new file mode 100644 index 0000000..49a254d --- /dev/null +++ b/16-LeetCode Hot 100/路径总和.md @@ -0,0 +1,28 @@ +# 路径总和 (Path Sum) + +## 题目描述 + +给你二叉树的根节点 root 和一个表示目标和的整数 targetSum,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。 + +## 解题思路 + +### DFS + +## Go 代码 + +```go +func hasPathSum(root *TreeNode, targetSum int) bool { + if root == nil { + return false + } + + if root.Left == nil && root.Right == nil { + return root.Val == targetSum + } + + return hasPathSum(root.Left, targetSum-root.Val) || + hasPathSum(root.Right, targetSum-root.Val) +} +``` + +**复杂度:** O(n) 时间,O(h) 空间 diff --git a/16-LeetCode Hot 100/除自身以外数组的乘积.md b/16-LeetCode Hot 100/除自身以外数组的乘积.md new file mode 100644 index 0000000..fc7bd23 --- /dev/null +++ b/16-LeetCode Hot 100/除自身以外数组的乘积.md @@ -0,0 +1,37 @@ +# 除自身以外数组的乘积 (Product of Array Except Self) + +## 题目描述 + +给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。 + +## 解题思路 + +### 左右乘积列表 + +分别计算每个位置的左侧乘积和右侧乘积,然后相乘。 + +## Go 代码 + +```go +func productExceptSelf(nums []int) []int { + n := len(nums) + answer := make([]int, n) + + // 左侧乘积 + answer[0] = 1 + for i := 1; i < n; i++ { + answer[i] = answer[i-1] * nums[i-1] + } + + // 右侧乘积并更新 + right := 1 + for i := n - 1; i >= 0; i-- { + answer[i] = answer[i] * right + right *= nums[i] + } + + return answer +} +``` + +**复杂度:** O(n) 时间,O(1) 额外空间(不包括输出数组)