更新时间: 2025-04-04
- 算法题解目录汇总 :算法刷题记录------题解目录汇总
- 技术博客总目录 :计算机技术系列博客------目录页
131. 分割回文串
给你一个字符串 s
,请你将 s
分割成一些 子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:[["a"]]
提示:
1 <= s.length <= 16
s 仅由小写英文字母组成
方法:动态规划预处理 + 回溯法
通过动态规划预处理所有回文子串信息,再利用回溯法生成所有可能的分割方案,有效避免重复计算。
- 动态规划预处理 :构建二维数组
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^4
Node.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 <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s 和 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
,著作权归领扣网络
所有。商业转载请联系官方授权,非商业转载请注明出处。