更新时间: 2025-04-04
- 算法题解目录汇总 :算法刷题记录------题解目录汇总
- 技术博客总目录 :计算机技术系列博客------目录页
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
1 <= s.length <= 16s 仅由小写英文字母组成
方法:动态规划预处理 + 回溯法
通过动态规划预处理所有回文子串信息,再利用回溯法生成所有可能的分割方案,有效避免重复计算。
- 动态规划预处理 :构建二维数组
dp[i][j],表示子串s[i...j]是否为回文。通过从后向前遍历,确保计算dp[i][j]时dp[i+1][j-1]已确定。 - 回溯法生成方案 :从起始位置
start开始,枚举所有可能的结束位置end。若s[start...end]是回文,则将其加入路径,并递归处理剩余子串s[end+1...],回溯时移除最后添加的子串。
代码实现(Java):
java
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
if (s == null || s.isEmpty()) return res;
int n = s.length();
boolean[][] dp = new boolean[n][n];
char[] arr = s.toCharArray();
// 预处理回文判断矩阵
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (i == j) {
dp[i][j] = true;
} else if (arr[i] == arr[j]) {
dp[i][j] = (j - i == 1) || dp[i + 1][j - 1];
}
}
}
backtrack(s, 0, new ArrayList<>(), res, dp);
return res;
}
private void backtrack(String s, int start, List<String> path,
List<List<String>> res, boolean[][] dp) {
if (start == s.length()) {
res.add(new ArrayList<>(path));
return;
}
for (int end = start; end < s.length(); end++) {
if (dp[start][end]) {
path.add(s.substring(start, end + 1));
backtrack(s, end + 1, path, res, dp);
path.remove(path.size() - 1);
}
}
}
}
复杂度分析
时间复杂度: 预处理阶段 O(n²),回溯阶段最坏情况 O(n·2ⁿ);综合时间复杂度为 O(n·2ⁿ),其中 n 为字符串长度。
空间复杂度: 预处理矩阵 O(n²),递归栈深度 O(n);综合空间复杂度 O(n²)。
138. 随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝 。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
提示:
0 <= n <= 1000-10^4 <= Node.val <= 10^4Node.random 为 null 或指向链表中的节点
方法一:哈希表映射法
通过哈希表建立原节点与复制节点的映射关系。第一次遍历创建所有新节点,第二次遍历设置指针,通过哈希表快速定位对应的复制节点。
- 创建节点映射:遍历原链表,创建每个节点的复制节点并存入哈希表。
- 设置指针 :再次遍历原链表,通过哈希表获取复制节点,设置其
next和random指针。
代码实现(Java):
java
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
Map<Node, Node> map = new HashMap<>();
Node curr = head;
// 创建所有新节点
while (curr != null) {
map.put(curr, new Node(curr.val));
curr = curr.next;
}
// 设置指针
curr = head;
while (curr != null) {
Node clone = map.get(curr);
clone.next = map.get(curr.next);
clone.random = map.get(curr.random);
curr = curr.next;
}
return map.get(head);
}
}
复杂度分析
- 时间复杂度 :
O(n),两次线性遍历。 - 空间复杂度 :
O(n),哈希表存储所有节点映射。
方法二:原地复制拆分法
不借助额外空间,通过三次遍历完成复制。第一次复制节点并插入原链表,第二次设置random指针,第三次拆分恢复原链表并构建新链表。
- 复制插入节点:将每个复制节点插入原节点之后,形成交替链表。
- 设置random指针:利用原节点的位置关系,设置复制节点的random。
- 拆分链表:恢复原链表的next,同时构建复制链表。
代码实现(Java):
java
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 插入复制节点
Node curr = head;
while (curr != null) {
Node clone = new Node(curr.val);
clone.next = curr.next;
curr.next = clone;
curr = clone.next;
}
// 设置random指针
curr = head;
while (curr != null) {
Node clone = curr.next;
clone.random = (curr.random != null) ? curr.random.next : null;
curr = clone.next;
}
// 拆分链表
Node newHead = head.next;
curr = head;
while (curr != null) {
Node clone = curr.next;
curr.next = clone.next; // 恢复原链表
if (clone.next != null) {
clone.next = clone.next.next; // 构建新链表
}
curr = curr.next;
}
return newHead;
}
}
复杂度分析
- 时间复杂度 :
O(n),三次线性遍历。 - 空间复杂度 :
O(1),仅使用常量额外空间。
对比总结
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 哈希表映射法 | 逻辑清晰,实现简单 | 需要O(n)额外空间 | 常规场景,快速实现 |
| 原地复制拆分法 | 空间效率高,无需额外存储 | 修改原链表结构 | 空间敏感,允许修改原链表 |
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s 和 wordDict[i] 仅由小写英文字母组成wordDict 中的所有字符串互不相同
方法:动态规划
使用动态规划数组 dp[i] 表示字符串前 i 个字符能否被字典中的单词拆分。通过遍历字符串的每个位置,并检查所有可能的子串是否存在于字典中,逐步填充 dp 数组。
- 字典预处理 :将字典存入
HashSet实现O(1)时间查询,同时记录字典中最长单词长度maxLen,减少不必要的子串检查。 - 动态规划填充 :
- 初始化 :
dp[0] = true表示空字符串可拆分。 - 遍历每个位置
i:从1到n(字符串长度),检查所有可能的拆分点j。 - 剪枝优化 :
start = Math.max(0, i - maxLen)确保仅检查长度不超过maxLen的子串,避免全量遍历。 - 状态转移 :若
dp[j]为true且子串s.substring(j, i)存在于字典中,则dp[i] = true。
- 初始化 :
代码实现(Java):
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
int maxLen = 0;
for (String word : wordDict) {
maxLen = Math.max(maxLen, word.length());
}
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // 空字符串默认可以拆分
for (int i = 1; i <= n; i++) {
// 仅检查长度不超过 maxLen 的子串
int start = Math.max(0, i - maxLen);
for (int j = start; j < i; j++) {
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
}
复杂度分析
- 时间复杂度 :预处理字典:
O(M),其中M为字典总字符数;动态规划循环:O(n * maxLen);总时间复杂度:O(M + n * maxLen),其中n是字符串长度,maxLen是字典中最长单词长度。 - 空间复杂度 :
HashSet存储字典:O(K);dp数组:O(n);总空间复杂度:O(K + n),K为字典中不同单词的个数。
声明
- 本文版权归
CSDN用户Allen Wurlitzer所有,遵循CC-BY-SA协议发布,转载请注明出处。- 本文题目来源
力扣-LeetCode,著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。