当前位置: 首页 > news >正文

【LeetCode 热题 100】139. 单词拆分——(解法一)记忆化搜索

Problem: 139. 单词拆分

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N * L^2)
    • 空间复杂度:O(N + D)

整体思路

这段代码旨在解决经典的 “单词拆分” (Word Break) 问题。问题要求判断一个给定的非空字符串 s 是否可以被分割成一个或多个在字典 wordDict 中出现的单词。

该算法采用的是一种 自顶向下(Top-Down)的动态规划 方法,即 记忆化搜索 (Memoization)。它将“字符串s能否被拆分”的大问题,分解为“s的某个前缀能否被拆分”的子问题,并通过递归解决,同时缓存子问题的解以避免重复计算。

算法的核心逻辑步骤如下:

  1. 预处理

    • 建立快速查询字典:将输入的 List<String> wordDict 转换为一个 HashSet<String> words。这是一个关键的性能优化,因为它将查询一个单词是否在字典中的时间复杂度从 O(N*L)(遍历列表)降低到了平均 O(L)(哈希查找,L为单词长度)。
    • 计算最大词长 maxLen:遍历字典,找出其中最长单词的长度。这个 maxLen 将用于后续的剪枝优化。
  2. 状态定义与递归关系

    • 算法的核心是递归函数 dfs(i),其状态定义为:
      dfs(i) = 字符串 s 的前 i 个字符(即 s.substring(0, i))是否可以被成功拆分
    • 为了判断 s 的前 i 个字符能否被拆分,算法会尝试所有的最后一个单词的可能性。它会从后往前扫描,寻找一个切分点 j (j < i),使得:
      a. 后半部分 s.substring(j, i) 是一个在字典中的单词。
      b. 前半部分 s.substring(0, j) 可以被成功拆分(这通过递归调用 dfs(j) 来判断)。
    • 只要能找到任何一个满足上述两个条件的切分点 j,就说明 s 的前 i 个字符是可以被拆分的。
  3. 记忆化与剪枝

    • 记忆化 (Memoization):使用一个 memo 数组来存储每个子问题 dfs(i) 的计算结果。memo[i] 的值有三种状态:-1(未计算),0(不可拆分),1(可拆分)。在 dfs(i) 的开头,先检查 memo[i],如果不是-1,就直接返回已存的结果,避免重复的递归树展开。
    • 剪枝 (Pruning):在 dfs(i) 内部的循环中,利用预计算的 maxLen 来缩小搜索范围。切分点 j 无需从 i-1 一直回溯到 0,只需回溯到 i - maxLen 即可。因为如果 i-j > maxLen,那么 s.substring(j, i) 的长度必然大于字典中最长的单词,它不可能是字典的一部分。这个剪枝显著减少了不必要的 substringHashSet.contains 操作。
  4. 基础情况 (Base Case)

    • dfs(0) 代表对空字符串的判断。一个空字符串可以被视作成功拆分(因为它不需要任何单词),所以 dfs(0) 返回 1(代表true)。这是递归的终点。

完整代码

import java.util.*;class Solution {/*** 判断字符串 s 是否可以被字典中的单词拆分。* @param s 目标字符串* @param wordDict 字典单词列表* @return 如果可以拆分则返回 true,否则返回 false*/public boolean wordBreak(String s, List<String> wordDict) {// 1. 预处理:计算字典中单词的最大长度,用于后续剪枝。int maxLen = 0;for (String word : wordDict) {maxLen = Math.max(maxLen, word.length());}// 2. 预处理:将 List 转换为 HashSet,以实现 O(1) 平均时间复杂度的单词查询。Set<String> words = new HashSet<>(wordDict);int n = s.length();// memo: 记忆化数组。memo[i] 存储 dfs(i) 的结果。// -1: 未计算, 0: false (不可拆分), 1: true (可拆分)。int[] memo = new int[n + 1];Arrays.fill(memo, -1);// 启动对整个字符串 s (长度为 n) 的递归求解。return dfs(n, maxLen, s, words, memo) == 1;}/*** 记忆化搜索函数。* @param i 当前要判断的前缀的长度,即 s.substring(0, i)* @param maxLen 字典中单词的最大长度(用于剪枝)* @param s 原始字符串* @param words 字典的 HashSet* @param memo 记忆化数组* @return 1 表示可以拆分,0 表示不可以*/private int dfs(int i, int maxLen, String s, Set<String> words, int[] memo) {// 基础情况:长度为 0 的前缀(空字符串)总是可以被“拆分”的。if (i == 0) {return 1;}// 记忆化检查:如果该子问题已经计算过,直接返回结果。if (memo[i] != -1) {return memo[i];}// 核心循环:尝试所有可能的最后一个单词。// j 是切分点,s.substring(j, i) 是尝试的最后一个单词。// 剪枝优化:j 的下界被 maxLen 限制,避免了不必要的检查。for (int j = i - 1; j >= Math.max(i - maxLen, 0); j--) {// 检查两个条件:// 1. s.substring(j, i) 是否在字典中。// 2. 剩余的前缀 s.substring(0, j) 是否也可以被拆分(递归调用)。if (words.contains(s.substring(j, i)) && dfs(j, maxLen, s, words, memo) == 1) {// 如果找到一种可行的拆分方式,记录结果 1 并立即返回。return memo[i] = 1;}}// 如果循环结束后仍未找到任何可行的拆分方式,记录结果 0 并返回。return memo[i] = 0;}
}

时空复杂度

时间复杂度:O(N * L^2)

  • N 是字符串 s 的长度。
  • L 是字典 wordDict 中单词的最大长度 (maxLen)。
  1. 状态数量:由于记忆化的存在,每个子问题 dfs(i)i0N)只会被实际计算一次。总共有 O(N) 个不同的状态。
  2. 每个状态的计算时间:在 dfs(i) 函数内部,主要的开销来自 for 循环。
    • 循环最多执行 L 次(因为 j 的范围被 maxLen 限制)。
    • 在循环内部,s.substring(j, i) 操作需要 O(L) 的时间,因为它需要复制一个长度最多为 L 的子串。
    • words.contains(...)HashSet 上的操作,其时间复杂度与被检查字符串的长度成正比(用于计算哈希值),因此也是 O(L)。
    • 因此,循环内部单次迭代的复杂度是 O(L)。
    • 总的来说,dfs(i) 的计算时间是 L * O(L) = O(L^2)
  3. 综合分析
    总时间复杂度 = (状态数量) × (每个状态的计算时间) = O(N) * O(L^2)
    预处理部分(构建 HashSet)的时间复杂度为 O(M*k)M为字典词数,k为平均词长),通常被 O(N*L^2) 主导。

空间复杂度:O(N + D)

  • N 是字符串 s 的长度。
  • D 是字典中所有字符的总数。
  1. 记忆化数组 memo:创建了一个大小为 N+1 的数组,占用 O(N) 空间。
  2. 递归调用栈:递归的最大深度可以达到 N(例如,当 s = "aaaa..."dict = ["a"] 时)。因此,递归栈占用的空间是 O(N)
  3. 字典 wordsHashSet 需要存储字典中的所有单词。如果字典中有 M 个单词,平均长度为 k,则其空间为 O(M*k),我们记为 D

综合分析
算法所需的总空间是 O(N) (memo) + O(N) (stack) + O(D) (set)。因此,最终的空间复杂度为 O(N + D)

参考灵神

http://www.dtcms.com/a/345072.html

相关文章:

  • Vue 插槽(Slots)全解析1
  • 所做过的笔试真题
  • 阿里云RDS MySQL数据归档全攻略:方案选择指南
  • (LeetCode 面试经典 150 题) 124. 二叉树中的最大路径和 (深度优先搜索dfs)
  • 大麦盒子DM4036刷包推荐
  • 停车场道闸的常见形式
  • 【会议跟踪】Model-Based Systems Engineering (MBSE) in Practice 2025
  • 场景题:内存溢出 和 内存泄漏 有啥区别?
  • Python-UV
  • Android夜间模式切换及自定义夜间模式切换按钮实现快速适配夜间模式
  • LeetCode Hot 100 第一天
  • 《器件在EMC中的应用》---TVS在EMC中的应用
  • 中国大学MOOC--C语言第十一周结构类型
  • 开源版CRM客户关系管理系统源码包+搭建部署教程
  • 3D打印小批量低成本打印玩具工艺品模型-中科米堆CASAIM
  • MTK Linux DRM分析(十三)- Mediatek KMS实现mtk_drm_drv.c(Part.1)
  • 深入解析TCP/UDP协议与网络编程
  • LeetCode100-239滑动窗口最大值
  • 利用DeepSeek编写从xlsx数据源调用duckdb执行已保存的查询SQL语句,并把查询结果保存到xlsx文件的程序
  • 电机驱动实现插补算法之脉冲和方向接收(以stm32主控为例)
  • 飞算JavaAI开发助手: 新手开发任务管理系统实战流程
  • STM32G4-比较器
  • Autosar之Com模块
  • Redis面试精讲 Day 27:Redis 7.0/8.0新特性深度解析
  • 基于STM32+Python+MySQL实现在线温度计设计和制作
  • 【高等数学笔记-极限(4)】极限的运算法则
  • 大麦盒子DM4036-精简固件包及教程
  • Vue2+Vue3前端开发_Day7
  • [TG开发]部署机器人
  • Java多线程编程与锁机制全解析(覆盖Java到Spring)