目录

算法刷题记录——LeetCode篇(2.4) [第131~140题](持续更新)

更新时间: 2025-04-04


131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

示例 1:

复制代码
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

复制代码
输入:s = "a"
输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

方法:动态规划预处理 + 回溯法

通过动态规划预处理所有回文子串信息,再利用回溯法生成所有可能的分割方案,有效避免重复计算。

  1. 动态规划预处理 :构建二维数组 dp[i][j],表示子串 s[i...j] 是否为回文。通过从后向前遍历,确保计算 dp[i][j]dp[i+1][j-1] 已确定。
  2. 回溯法生成方案 :从起始位置 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 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0n-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 或指向链表中的节点

方法一:哈希表映射法

通过哈希表建立原节点与复制节点的映射关系。第一次遍历创建所有新节点,第二次遍历设置指针,通过哈希表快速定位对应的复制节点。

  1. 创建节点映射:遍历原链表,创建每个节点的复制节点并存入哈希表。
  2. 设置指针 :再次遍历原链表,通过哈希表获取复制节点,设置其nextrandom指针。

代码实现(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指针,第三次拆分恢复原链表并构建新链表。

  1. 复制插入节点:将每个复制节点插入原节点之后,形成交替链表。
  2. 设置random指针:利用原节点的位置关系,设置复制节点的random。
  3. 拆分链表:恢复原链表的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 数组。

  1. 字典预处理 :将字典存入 HashSet 实现 O(1) 时间查询,同时记录字典中最长单词长度 maxLen,减少不必要的子串检查。
  2. 动态规划填充
    • 初始化dp[0] = true 表示空字符串可拆分。
    • 遍历每个位置 i:从 1n(字符串长度),检查所有可能的拆分点 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 为字典中不同单词的个数。

声明

  1. 本文版权归 CSDN 用户 Allen Wurlitzer 所有,遵循CC-BY-SA协议发布,转载请注明出处。
  2. 本文题目来源 力扣-LeetCode ,著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
Echo``25 分钟前
2:QT联合HALCON编程—图像显示放大缩小
开发语言·c++·图像处理·qt·算法
.似水30 分钟前
2025.4.22_C_可变参数列表
java·c语言·算法
天真小巫41 分钟前
2025.4.26总结
职场和发展
Felven1 小时前
A. Ideal Generator
java·数据结构·算法
懒懒小徐1 小时前
大厂面试-框架篇
面试·职场和发展
MoonBit月兔1 小时前
双周报Vol.70: 运算符重载语义变化、String API 改动、IDE Markdown 格式支持优化...多项更新升级!
ide·算法·哈希算法
How_doyou_do1 小时前
树状数组底层逻辑探讨 / 模版代码-P3374-P3368
数据结构·算法·树状数组
小鹿鹿啊2 小时前
C语言编程--14.电话号码的字母组合
c语言·开发语言·算法
小O的算法实验室2 小时前
2024年ESWA SCI1区TOP:量子计算蜣螂算法QHDBO,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
C语言魔术师2 小时前
509. 斐波那契数
算法·动态规划